From d59e57e1dadfafccaf72e09d2728a17b70b38210 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Wed, 14 Aug 2024 13:22:08 -0700 Subject: [PATCH 01/39] poc --- .../services/amazonq/QOpenPanelAction.kt | 5 + .../amazonq/toolwindow/AmazonQToolWindow.kt | 9 + .../services/cwc/commands/ActionRegistrar.kt | 4 +- .../cwc/commands/ContextMenuActionMessage.kt | 2 +- .../services/cwc/controller/ChatController.kt | 6 + .../chat/userIntent/UserIntentRecognizer.kt | 1 + .../project/ProjectContextEditorListener.kt | 4 +- .../services/cwc/inline/ChatCaretListener.kt | 92 +++++++ .../services/cwc/inline/ChatInputInlay.kt | 226 ++++++++++++++++++ .../cwc/inline/InlineChatFileListener.kt | 17 ++ 10 files changed, 362 insertions(+), 4 deletions(-) create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatInputInlay.kt create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt index 5d249301c82..06779df4e88 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt @@ -21,3 +21,8 @@ class QOpenPanelAction : AnAction(message("action.q.openchat.text"), null, AwsIc ToolWindowManager.getInstance(project).getToolWindow(AMAZON_Q_WINDOW_ID)?.activate(null, true) } } + +private fun isQSupportedInThisVersion(): Boolean { + return true +} +p diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt index e5673cc6dae..76092fda4b9 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt @@ -10,6 +10,7 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.runInEdt import com.intellij.openapi.components.Service import com.intellij.openapi.components.service +import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.wm.ToolWindowManager @@ -29,6 +30,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.webview.FqnWebviewAdapte import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.EditorThemeAdapter import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable +import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatFileListener import javax.swing.JComponent @Service(Service.Level.PROJECT) @@ -46,6 +48,8 @@ class AmazonQToolWindow private constructor( private val appConnections = mutableListOf() + private var listener : InlineChatFileListener? = null + init { initConnections() connectUi() @@ -97,6 +101,11 @@ class AmazonQToolWindow private constructor( messageTypeRegistry = connection.messageTypeRegistry, fqnWebviewAdapter = fqnWebviewAdapter, ) + if (listener == null) { + listener = InlineChatFileListener(initContext).apply { + project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this) + } + } // Connect the app to the UI connection.app.init(initContext) // Dispose of the app when the tool window is disposed. diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt index 71110cba420..dddffaad5ab 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt @@ -14,8 +14,8 @@ class ActionRegistrar { private val _messages by lazy { MutableSharedFlow(extraBufferCapacity = 10) } val flow = _messages.asSharedFlow() - fun reportMessageClick(command: EditorContextCommand, project: Project) { - _messages.tryEmit(ContextMenuActionMessage(command, project)) + fun reportMessageClick(command: EditorContextCommand, project: Project, message: String? = null) { + _messages.tryEmit(ContextMenuActionMessage(command, project, message)) } fun reportMessageClick(command: EditorContextCommand, issue: MutableMap, project: Project) { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ContextMenuActionMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ContextMenuActionMessage.kt index d7ae565d277..dbe37326efb 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ContextMenuActionMessage.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ContextMenuActionMessage.kt @@ -9,4 +9,4 @@ import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage /** * Event emitted for context menu editor actions */ -data class ContextMenuActionMessage(val command: EditorContextCommand, val project: Project) : AmazonQMessage +data class ContextMenuActionMessage(val command: EditorContextCommand, val project: Project, val message: String? = null) : AmazonQMessage diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt index 2513bb5d4c6..846bc3476eb 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt @@ -66,7 +66,9 @@ import software.aws.toolkits.jetbrains.services.cwc.editor.context.ActiveFileCon import software.aws.toolkits.jetbrains.services.cwc.editor.context.ActiveFileContextExtractor import software.aws.toolkits.jetbrains.services.cwc.editor.context.ExtractionTriggerType import software.aws.toolkits.jetbrains.services.cwc.editor.context.project.ProjectContextController +import software.aws.toolkits.jetbrains.services.cwc.editor.context.project.ProjectContextEditorListener import software.aws.toolkits.jetbrains.services.cwc.editor.context.project.RelevantDocument +import software.aws.toolkits.jetbrains.services.cwc.inline.ChatCaretListener import software.aws.toolkits.jetbrains.services.cwc.messages.AuthNeededException import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessage import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType @@ -89,6 +91,8 @@ class ChatController private constructor( private val contextExtractor: ActiveFileContextExtractor, private val intentRecognizer: UserIntentRecognizer, private val authController: AuthController, +// private val editorListener: ProjectContextEditorListener, +// private val caretListener: ChatCaretListener, ) : InboundAppMessagesHandler { private val messagePublisher: MessagePublisher = context.messagesFromAppToUi @@ -101,6 +105,8 @@ class ChatController private constructor( contextExtractor = ActiveFileContextExtractor.create(fqnWebviewAdapter = context.fqnWebviewAdapter, project = context.project), intentRecognizer = UserIntentRecognizer(), authController = AuthController(), +// editorListener = ProjectContextEditorListener(context), +// caretListener = ChatCaretListener(context.project, context), ) override suspend fun processClearQuickAction(message: IncomingCwcMessage.ClearChat) { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/userIntent/UserIntentRecognizer.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/userIntent/UserIntentRecognizer.kt index f15f0125e9f..414fa12f438 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/userIntent/UserIntentRecognizer.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/userIntent/UserIntentRecognizer.kt @@ -18,6 +18,7 @@ class UserIntentRecognizer { EditorContextCommand.ExplainCodeScanIssue -> UserIntent.EXPLAIN_CODE_SELECTION EditorContextCommand.GenerateUnitTests -> UserIntent.GENERATE_UNIT_TESTS EditorContextCommand.SendToPrompt -> null + EditorContextCommand.SendToChat -> null } fun getUserIntentFromPromptChatMessage(prompt: String, startUrl: String?) = when { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/project/ProjectContextEditorListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/project/ProjectContextEditorListener.kt index 8997254f7d0..8c8cda911a0 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/project/ProjectContextEditorListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/project/ProjectContextEditorListener.kt @@ -5,9 +5,11 @@ package software.aws.toolkits.jetbrains.services.cwc.editor.context.project import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.vfs.VirtualFile +import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererSettings +import software.aws.toolkits.jetbrains.services.cwc.inline.ChatCaretListener -class ProjectContextEditorListener : FileEditorManagerListener { +class ProjectContextEditorListener() : FileEditorManagerListener { override fun fileClosed(source: FileEditorManager, file: VirtualFile) { if (CodeWhispererSettings.getInstance().isProjectContextEnabled()) { ProjectContextController.getInstance(source.project).updateIndex(file.path) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt new file mode 100644 index 00000000000..7301637ab81 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt @@ -0,0 +1,92 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cwc.inline +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionGroup +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.editor.event.CaretEvent +import com.intellij.openapi.editor.event.CaretListener +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.openapi.editor.markup.HighlighterLayer +import com.intellij.openapi.editor.markup.HighlighterTargetArea +import com.intellij.openapi.editor.markup.MarkupModel +import com.intellij.openapi.editor.markup.RangeHighlighter +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext +import javax.swing.Icon + +class ChatCaretListener(private val project: Project, private val context: AmazonQAppInitContext) : CaretListener { + private var currentHighlighter: RangeHighlighter? = null + private var currentInlay: ChatInputInlay? = null + init { + val editor = FileEditorManager.getInstance(project).selectedTextEditor + editor?.caretModel?.addCaretListener(this) + } + + override fun caretPositionChanged(event: CaretEvent) { + val editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return + val lineNumber = event.newPosition.line + val startOffset = editor.document.getLineStartOffset(lineNumber) + val endOffset = editor.document.getLineEndOffset(lineNumber) + val markupModel: MarkupModel = editor.markupModel + val gutterIconRenderer = ChatGutterIconRenderer(AllIcons.Actions.Lightning).apply { + setClickAction { + currentInlay?.hidePopup() + currentInlay = ChatInputInlay(editor, event.newPosition, context) + } + } + + if (event.oldPosition.line != event.newPosition.line) { + currentHighlighter?.let { + editor.markupModel.removeHighlighter(it) + } + markupModel.apply { + val highlighter = addRangeHighlighter( + startOffset, + endOffset, + HighlighterLayer.CARET_ROW, + null, + HighlighterTargetArea.LINES_IN_RANGE + ) + currentHighlighter = highlighter + highlighter.gutterIconRenderer = gutterIconRenderer + } + } + } +} + +private class ChatGutterIconRenderer(private val icon: Icon) : GutterIconRenderer() { + private var clickAction: (() -> Unit)? = null + override fun equals(other: Any?): Boolean { + if (other is ChatGutterIconRenderer) { + return icon == other.icon + } + return false + } + + override fun hashCode(): Int = icon.hashCode() + + override fun getIcon(): Icon = icon + + override fun getTooltipText(): String = "Ask Amazon Q" + + override fun isNavigateAction(): Boolean = false + + override fun getClickAction(): AnAction = object : AnAction() { + // bring up the chat inputbox + override fun actionPerformed(e: AnActionEvent) = clickAction?.invoke() ?: Unit + override fun update(e: AnActionEvent) = Unit + } + + fun setClickAction (action: () -> Unit) { + clickAction = action + } + + override fun getPopupMenuActions(): ActionGroup? = null + + override fun getAlignment(): Alignment = Alignment.LEFT +} + diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatInputInlay.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatInputInlay.kt new file mode 100644 index 00000000000..cb1ae34a7ad --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatInputInlay.kt @@ -0,0 +1,226 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cwc.inline + +import com.intellij.openapi.Disposable +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorCustomElementRenderer +import com.intellij.openapi.editor.Inlay +import com.intellij.openapi.editor.InlayModel +import com.intellij.openapi.editor.LogicalPosition +import com.intellij.openapi.editor.VisualPosition +import com.intellij.openapi.editor.event.EditorMouseEvent +import com.intellij.openapi.editor.event.EditorMouseMotionListener +import com.intellij.openapi.editor.ex.EditorGutterComponentEx +import com.intellij.openapi.editor.markup.HighlighterLayer +import com.intellij.openapi.editor.markup.HighlighterTargetArea +import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.ui.IdeBorderFactory +import com.intellij.ui.awt.RelativePoint +import com.intellij.ui.components.JBTextField +import com.intellij.util.ui.UIUtil +import com.intellij.xdebugger.ui.DebuggerColors +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope +import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext +import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage +import software.aws.toolkits.jetbrains.services.amazonq.messages.MessageConnector +import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AMAZON_Q_WINDOW_ID +import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindow +import software.aws.toolkits.jetbrains.services.amazonq.webview.BrowserConnector +import software.aws.toolkits.jetbrains.services.codewhisperer.inlay.CodeWhispererInlayInlineRenderer +import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContext +import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext +import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupListener +import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererScrollListener +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_DIM_HEX +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_HOVER +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_PANEL_SEPARATOR +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.POPUP_BUTTON_TEXT_SIZE +import software.aws.toolkits.jetbrains.services.cwc.commands.ActionRegistrar +import software.aws.toolkits.jetbrains.services.cwc.commands.ContextMenuActionMessage +import software.aws.toolkits.jetbrains.services.cwc.commands.EditorContextCommand +import software.aws.toolkits.jetbrains.services.cwc.messages.EditorContextCommandMessage +import software.aws.toolkits.jetbrains.services.cwc.messages.IncomingCwcMessage +import software.aws.toolkits.resources.message +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.Graphics +import java.awt.GridBagLayout +import java.awt.Point +import java.awt.Rectangle +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.awt.event.MouseMotionListener +import java.util.UUID +import javax.swing.BorderFactory +import javax.swing.BoxLayout +import javax.swing.JButton +import javax.swing.JPanel +import javax.swing.JTextArea +import javax.swing.JTextField +import javax.swing.event.DocumentEvent +import javax.swing.event.DocumentListener + +class ChatInputInlay( + private val editor: Editor, + private val position: LogicalPosition, + private val context: AmazonQAppInitContext +) : Disposable { + private var inlay: Inlay? = null + private var currentPopup: JBPopup? = null + + init { + createPopup() + } + + private fun createPopup() { + val popupPanel = ChatInputPopupPanel().apply { + setTextChangeListener { + println("Text changed: $it") + } + + setSubmitClickListener { + val text = textField.text + ActionRegistrar.instance.reportMessageClick(EditorContextCommand.SendToChat, context.project, text) +// println("Submitted text: $text") + hidePopup() + ToolWindowManager.getInstance(context.project).getToolWindow(AMAZON_Q_WINDOW_ID)?.activate(null, true) + } + } + val popup = initPopup(popupPanel) + val popupPoint = editor.visualPositionToXY(editor.caretModel.currentCaret.visualPosition) + popup.setLocation(popupPoint) + popup.showInBestPositionFor(editor) + popupPanel.textField.requestFocusInWindow() + currentPopup = popup + } + +// private fun createInlay() { +// val renderer = object : EditorCustomElementRenderer { +// override fun calcWidthInPixels(inlay: Inlay<*>): Int { +// return 500 +// } +// +// override fun paint(inlay: Inlay<*>, g: Graphics, targetRegion: Rectangle, textAttributes: TextAttributes) { +//// val point = editor.logicalPositionToXY(position) +// val attributes = editor.colorsScheme.getAttributes(DebuggerColors.INLINED_VALUES_EXECUTION_LINE) ?: return +// val fgColor = attributes.foregroundColor ?: return +// +// val popupPanel = ChatInputPopupPanel().apply { +//// add(acceptButton) +// setTextChangeListener { +// println("Text changed: $it") +// } +// +// setSubmitClickListener { +// val text = textField.text +// ActionRegistrar.instance.reportMessageClick(EditorContextCommand.SendToChat, context.project, text) +// println("Submitted text: $text") +// hidePopup() +// } +// } +// val popup = initPopup(popupPanel) +// val popupPoint = editor.visualPositionToXY(editor.caretModel.currentCaret.visualPosition) +// popup.setLocation(popupPoint) +// popup.showInBestPositionFor(editor) +// popupPanel.textField.requestFocusInWindow() +// currentPopup = popup +// } +// } +// val inlayModel = editor.inlayModel +// inlay = inlayModel.addBlockElement( +// position.line, +// true, +// true, +// 0, +// renderer +// ) +// } + + fun hidePopup() { + currentPopup?.dispose() + currentPopup = null + } + + private fun initPopup(panel: ChatInputPopupPanel): JBPopup { + val popup = JBPopupFactory.getInstance() + .createComponentPopupBuilder(panel, panel.textField) +// .setMovable(true) +// .setResizable(true) + .setTitle("Ask Amazon Q") + .setAlpha(0.2F) + .setCancelOnClickOutside(true) + .setCancelOnOtherWindowOpen(true) + .setCancelKeyEnabled(true) + .setFocusable(true) + .setRequestFocus(true) + .setLocateWithinScreenBounds(true) + .setCancelOnWindowDeactivation(true) + .createPopup() + return popup + } + + class ChatInputPopupPanel : JPanel() { + val textField = JTextField(50) + private val submitButton = JButton("Confirm") + private var textChangeListener: ((String) -> Unit)? = null + private var submitClickListener: (() -> Unit)? = null + + + init { + layout = BorderLayout() + val inputPanel = JPanel(BorderLayout()) + inputPanel.add(textField, BorderLayout.WEST) + submitButton.preferredSize = Dimension(80, 30) + inputPanel.add(submitButton, BorderLayout.EAST) + add(inputPanel, BorderLayout.NORTH) + val listener = object : DocumentListener { + override fun insertUpdate(e: DocumentEvent) { + updateText() + } + + override fun removeUpdate(e: DocumentEvent) { + updateText() + } + + override fun changedUpdate(e: DocumentEvent) { + updateText() + } + + private fun updateText() { + val newText = textField.text + textChangeListener?.invoke(newText) + } + } + // Add a document listener to the text field + textField.document.addDocumentListener(listener) + + submitButton.addActionListener { + submitClickListener?.invoke() + } + } + + fun setTextChangeListener(listener: (String) -> Unit) { + textChangeListener = listener + } + + fun setSubmitClickListener(listener: () -> Unit) { + submitClickListener = listener + } + } + + override fun dispose() { + hidePopup() + } +} + diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt new file mode 100644 index 00000000000..0f04e4604eb --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt @@ -0,0 +1,17 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cwc.inline + +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.FileEditorManagerListener +import com.intellij.openapi.vfs.VirtualFile +import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext +import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererSettings +import software.aws.toolkits.jetbrains.services.cwc.editor.context.project.ProjectContextController + +class InlineChatFileListener(private val context: AmazonQAppInitContext) : FileEditorManagerListener { + override fun fileOpened(source: FileEditorManager, file: VirtualFile) { + ChatCaretListener(source.project, context) + } +} From e053541929cb0b1c475c44faae923dba0262361c Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Thu, 22 Aug 2024 11:10:25 -0700 Subject: [PATCH 02/39] initial e2e poc --- ...otlin-compiler-10146723276903801803.salive | 0 .../chat/jetbrains-community/build.gradle.kts | 1 + .../services/amazonq/QOpenPanelAction.kt | 1 - .../services/cwc/inline/ChatCaretListener.kt | 6 +- .../services/cwc/inline/ChatInputInlay.kt | 226 ----------- .../cwc/inline/InlineChatFileListener.kt | 9 +- .../services/cwc/inline/InlineChatPopup.kt | 374 ++++++++++++++++++ 7 files changed, 385 insertions(+), 232 deletions(-) create mode 100644 .kotlin/sessions/kotlin-compiler-10146723276903801803.salive delete mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatInputInlay.kt create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopup.kt diff --git a/.kotlin/sessions/kotlin-compiler-10146723276903801803.salive b/.kotlin/sessions/kotlin-compiler-10146723276903801803.salive new file mode 100644 index 00000000000..e69de29bb2d diff --git a/plugins/amazonq/chat/jetbrains-community/build.gradle.kts b/plugins/amazonq/chat/jetbrains-community/build.gradle.kts index 1df61f69fe2..d3ac5334b04 100644 --- a/plugins/amazonq/chat/jetbrains-community/build.gradle.kts +++ b/plugins/amazonq/chat/jetbrains-community/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { // everything references codewhisperer, which is not ideal implementation(project(":plugin-amazonq:codewhisperer:jetbrains-community")) implementation(libs.nimbus.jose.jwt) + implementation("org.jetbrains:markdown:0.7.3") compileOnly(project(":plugin-core:jetbrains-community")) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt index 06779df4e88..cea0b15bbee 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt @@ -25,4 +25,3 @@ class QOpenPanelAction : AnAction(message("action.q.openchat.text"), null, AwsIc private fun isQSupportedInThisVersion(): Boolean { return true } -p diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt index 7301637ab81..f4173727cf5 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt @@ -20,7 +20,7 @@ import javax.swing.Icon class ChatCaretListener(private val project: Project, private val context: AmazonQAppInitContext) : CaretListener { private var currentHighlighter: RangeHighlighter? = null - private var currentInlay: ChatInputInlay? = null + private var currentPopup: InlineChatPopup? = null init { val editor = FileEditorManager.getInstance(project).selectedTextEditor editor?.caretModel?.addCaretListener(this) @@ -34,8 +34,8 @@ class ChatCaretListener(private val project: Project, private val context: Amazo val markupModel: MarkupModel = editor.markupModel val gutterIconRenderer = ChatGutterIconRenderer(AllIcons.Actions.Lightning).apply { setClickAction { - currentInlay?.hidePopup() - currentInlay = ChatInputInlay(editor, event.newPosition, context) + currentPopup?.hidePopup() + currentPopup = InlineChatPopup(editor, context) } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatInputInlay.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatInputInlay.kt deleted file mode 100644 index cb1ae34a7ad..00000000000 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatInputInlay.kt +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.cwc.inline - -import com.intellij.openapi.Disposable -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.EditorCustomElementRenderer -import com.intellij.openapi.editor.Inlay -import com.intellij.openapi.editor.InlayModel -import com.intellij.openapi.editor.LogicalPosition -import com.intellij.openapi.editor.VisualPosition -import com.intellij.openapi.editor.event.EditorMouseEvent -import com.intellij.openapi.editor.event.EditorMouseMotionListener -import com.intellij.openapi.editor.ex.EditorGutterComponentEx -import com.intellij.openapi.editor.markup.HighlighterLayer -import com.intellij.openapi.editor.markup.HighlighterTargetArea -import com.intellij.openapi.editor.markup.TextAttributes -import com.intellij.openapi.ui.popup.JBPopup -import com.intellij.openapi.ui.popup.JBPopupFactory -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.wm.ToolWindowManager -import com.intellij.ui.IdeBorderFactory -import com.intellij.ui.awt.RelativePoint -import com.intellij.ui.components.JBTextField -import com.intellij.util.ui.UIUtil -import com.intellij.xdebugger.ui.DebuggerColors -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.launch -import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope -import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext -import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage -import software.aws.toolkits.jetbrains.services.amazonq.messages.MessageConnector -import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AMAZON_Q_WINDOW_ID -import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindow -import software.aws.toolkits.jetbrains.services.amazonq.webview.BrowserConnector -import software.aws.toolkits.jetbrains.services.codewhisperer.inlay.CodeWhispererInlayInlineRenderer -import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContext -import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext -import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupListener -import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererScrollListener -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_DIM_HEX -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_HOVER -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_PANEL_SEPARATOR -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.POPUP_BUTTON_TEXT_SIZE -import software.aws.toolkits.jetbrains.services.cwc.commands.ActionRegistrar -import software.aws.toolkits.jetbrains.services.cwc.commands.ContextMenuActionMessage -import software.aws.toolkits.jetbrains.services.cwc.commands.EditorContextCommand -import software.aws.toolkits.jetbrains.services.cwc.messages.EditorContextCommandMessage -import software.aws.toolkits.jetbrains.services.cwc.messages.IncomingCwcMessage -import software.aws.toolkits.resources.message -import java.awt.BorderLayout -import java.awt.Dimension -import java.awt.Graphics -import java.awt.GridBagLayout -import java.awt.Point -import java.awt.Rectangle -import java.awt.event.KeyAdapter -import java.awt.event.KeyEvent -import java.awt.event.MouseAdapter -import java.awt.event.MouseEvent -import java.awt.event.MouseMotionListener -import java.util.UUID -import javax.swing.BorderFactory -import javax.swing.BoxLayout -import javax.swing.JButton -import javax.swing.JPanel -import javax.swing.JTextArea -import javax.swing.JTextField -import javax.swing.event.DocumentEvent -import javax.swing.event.DocumentListener - -class ChatInputInlay( - private val editor: Editor, - private val position: LogicalPosition, - private val context: AmazonQAppInitContext -) : Disposable { - private var inlay: Inlay? = null - private var currentPopup: JBPopup? = null - - init { - createPopup() - } - - private fun createPopup() { - val popupPanel = ChatInputPopupPanel().apply { - setTextChangeListener { - println("Text changed: $it") - } - - setSubmitClickListener { - val text = textField.text - ActionRegistrar.instance.reportMessageClick(EditorContextCommand.SendToChat, context.project, text) -// println("Submitted text: $text") - hidePopup() - ToolWindowManager.getInstance(context.project).getToolWindow(AMAZON_Q_WINDOW_ID)?.activate(null, true) - } - } - val popup = initPopup(popupPanel) - val popupPoint = editor.visualPositionToXY(editor.caretModel.currentCaret.visualPosition) - popup.setLocation(popupPoint) - popup.showInBestPositionFor(editor) - popupPanel.textField.requestFocusInWindow() - currentPopup = popup - } - -// private fun createInlay() { -// val renderer = object : EditorCustomElementRenderer { -// override fun calcWidthInPixels(inlay: Inlay<*>): Int { -// return 500 -// } -// -// override fun paint(inlay: Inlay<*>, g: Graphics, targetRegion: Rectangle, textAttributes: TextAttributes) { -//// val point = editor.logicalPositionToXY(position) -// val attributes = editor.colorsScheme.getAttributes(DebuggerColors.INLINED_VALUES_EXECUTION_LINE) ?: return -// val fgColor = attributes.foregroundColor ?: return -// -// val popupPanel = ChatInputPopupPanel().apply { -//// add(acceptButton) -// setTextChangeListener { -// println("Text changed: $it") -// } -// -// setSubmitClickListener { -// val text = textField.text -// ActionRegistrar.instance.reportMessageClick(EditorContextCommand.SendToChat, context.project, text) -// println("Submitted text: $text") -// hidePopup() -// } -// } -// val popup = initPopup(popupPanel) -// val popupPoint = editor.visualPositionToXY(editor.caretModel.currentCaret.visualPosition) -// popup.setLocation(popupPoint) -// popup.showInBestPositionFor(editor) -// popupPanel.textField.requestFocusInWindow() -// currentPopup = popup -// } -// } -// val inlayModel = editor.inlayModel -// inlay = inlayModel.addBlockElement( -// position.line, -// true, -// true, -// 0, -// renderer -// ) -// } - - fun hidePopup() { - currentPopup?.dispose() - currentPopup = null - } - - private fun initPopup(panel: ChatInputPopupPanel): JBPopup { - val popup = JBPopupFactory.getInstance() - .createComponentPopupBuilder(panel, panel.textField) -// .setMovable(true) -// .setResizable(true) - .setTitle("Ask Amazon Q") - .setAlpha(0.2F) - .setCancelOnClickOutside(true) - .setCancelOnOtherWindowOpen(true) - .setCancelKeyEnabled(true) - .setFocusable(true) - .setRequestFocus(true) - .setLocateWithinScreenBounds(true) - .setCancelOnWindowDeactivation(true) - .createPopup() - return popup - } - - class ChatInputPopupPanel : JPanel() { - val textField = JTextField(50) - private val submitButton = JButton("Confirm") - private var textChangeListener: ((String) -> Unit)? = null - private var submitClickListener: (() -> Unit)? = null - - - init { - layout = BorderLayout() - val inputPanel = JPanel(BorderLayout()) - inputPanel.add(textField, BorderLayout.WEST) - submitButton.preferredSize = Dimension(80, 30) - inputPanel.add(submitButton, BorderLayout.EAST) - add(inputPanel, BorderLayout.NORTH) - val listener = object : DocumentListener { - override fun insertUpdate(e: DocumentEvent) { - updateText() - } - - override fun removeUpdate(e: DocumentEvent) { - updateText() - } - - override fun changedUpdate(e: DocumentEvent) { - updateText() - } - - private fun updateText() { - val newText = textField.text - textChangeListener?.invoke(newText) - } - } - // Add a document listener to the text field - textField.document.addDocumentListener(listener) - - submitButton.addActionListener { - submitClickListener?.invoke() - } - } - - fun setTextChangeListener(listener: (String) -> Unit) { - textChangeListener = listener - } - - fun setSubmitClickListener(listener: () -> Unit) { - submitClickListener = listener - } - } - - override fun dispose() { - hidePopup() - } -} - diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt index 0f04e4604eb..cd003271c96 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt @@ -4,6 +4,7 @@ package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.FileEditorManagerEvent import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.vfs.VirtualFile import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext @@ -11,7 +12,11 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhisp import software.aws.toolkits.jetbrains.services.cwc.editor.context.project.ProjectContextController class InlineChatFileListener(private val context: AmazonQAppInitContext) : FileEditorManagerListener { - override fun fileOpened(source: FileEditorManager, file: VirtualFile) { - ChatCaretListener(source.project, context) +// override fun fileOpened(source: FileEditorManager, file: VirtualFile) { +// ChatCaretListener(source.project, context) +// } + + override fun selectionChanged(event: FileEditorManagerEvent) { + ChatCaretListener(context.project, context) } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopup.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopup.kt new file mode 100644 index 00000000000..61f0f3280ce --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopup.kt @@ -0,0 +1,374 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cwc.inline + +import com.intellij.openapi.Disposable +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.markup.HighlighterLayer +import com.intellij.openapi.editor.markup.HighlighterTargetArea +import com.intellij.openapi.editor.markup.RangeHighlighter +import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.JBPopupListener +import com.intellij.openapi.ui.popup.LightweightWindowEvent +import com.intellij.openapi.util.text.StringUtil +import com.intellij.ui.JBColor +import com.intellij.ui.components.JBScrollPane +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import migration.software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator +import software.amazon.awssdk.services.codewhispererstreaming.model.UserIntent +import software.aws.toolkits.jetbrains.core.coroutines.EDT +import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope +import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext +import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData +import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerType +import software.aws.toolkits.jetbrains.services.cwc.clients.chat.v1.ChatSessionFactoryV1 +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.messenger.ChatPromptHandler +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.TelemetryHelper +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.userIntent.UserIntentRecognizer +import software.aws.toolkits.jetbrains.services.cwc.editor.context.ActiveFileContextExtractor +import software.aws.toolkits.jetbrains.services.cwc.editor.context.ExtractionTriggerType +import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessage +import software.aws.toolkits.jetbrains.services.cwc.storage.ChatSessionInfo +import software.aws.toolkits.jetbrains.services.cwc.storage.ChatSessionStorage +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.Font +import java.util.UUID +import javax.swing.BorderFactory +import javax.swing.JButton +import javax.swing.JPanel +import javax.swing.JTextArea +import javax.swing.JTextField +import javax.swing.event.DocumentEvent +import javax.swing.event.DocumentListener + +class InlineChatPopup( + private val editor: Editor, + private val context: AmazonQAppInitContext +) : Disposable { + private var currentPopup: JBPopup? = null + private val scope = disposableCoroutineScope(this) + private var rangeHighlighter: RangeHighlighter? = null + private var undoAction: (() -> Unit?)? = null + + init { + createPopup() + } + + private fun createPopup() { + val popupListener = object : JBPopupListener { + override fun onClosed(event: LightweightWindowEvent) { + undoAction?.invoke() + } + } + val popupPanel = ChatInputPopupPanel().apply { + setTextChangeListener { + println("Text changed: $it") + } + + setSubmitClickListener { + submitButton.isEnabled = false + textField.isEnabled = false + val prompt = textField.text + setLabel("Waiting for Amazon Q...") + revalidate() + + scope.launch { + val messages = handleChat(prompt) + val src = messages.last().message ?: return@launch + val codeBlocks = getCodeBlocksRecursively(src) + if (codeBlocks.isNotEmpty()) { + val codeString = StringUtil.unescapeStringCharacters(extractContentAfterFirstNewline(codeBlocks.first())) + withContext(EDT) { + val caret: Caret = editor.caretModel.primaryCaret + val offset = caret.offset + val caretStart = caret.selectionStart + val caretEnd = caret.selectionEnd + + if (caret.hasSelection()) { + if(!isVisible){ + return@withContext + } + caret.removeSelection() + + WriteCommandAction.runWriteCommandAction(context.project) { + editor.document.insertString(offset, codeString) + } + undoAction = { + WriteCommandAction.runWriteCommandAction(context.project) { + editor.document.deleteString(offset, offset + codeString.length) + } + editor.markupModel.removeAllHighlighters() + } + highlightCodeWithBackgroundColor(editor, offset, offset + codeString.length, true) + highlightCodeWithBackgroundColor(editor, caretStart + codeString.length, caretEnd + codeString.length, false) + val acceptAction = { + undoAction = null + WriteCommandAction.runWriteCommandAction(context.project) { + editor.document.deleteString(caretStart + codeString.length, caretEnd + codeString.length) + } + editor.markupModel.removeAllHighlighters() + hidePopup() + } + val rejectAction = { + WriteCommandAction.runWriteCommandAction(context.project) { + editor.document.deleteString(offset, offset + codeString.length) + } + editor.markupModel.removeAllHighlighters() + hidePopup() + } + addCodeActionsPanel(acceptAction, rejectAction) + } + + } + return@launch + } + +// TODO: change textArea to textPane and render markdown +// val flavour = CommonMarkFlavourDescriptor() +// val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(src) +// val html = HtmlGenerator(src, parsedTree, flavour).generateHtml() + setLabel(src) + textField.isEnabled = true + submitButton.isEnabled = true + revalidate() + } + } + } + val popup = initPopup(popupPanel) + popup.addListener(popupListener) + val popupPoint = editor.visualPositionToXY(editor.caretModel.currentCaret.visualPosition) + popup.setLocation(popupPoint) + popup.showInBestPositionFor(editor) + popupPanel.textField.requestFocusInWindow() + currentPopup = popup + } + + private fun highlightCodeWithBackgroundColor(editor: Editor, startOffset: Int, endOffset: Int, isGreen: Boolean) { + + val greenBackgroundAttributes = TextAttributes().apply { + backgroundColor = JBColor(0x66BB6A, 0x006400) + effectColor = JBColor(0x66BB6A, 0x006400) + } + + val redBackgroundAttributes = TextAttributes().apply { + backgroundColor = JBColor(0xEF9A9A, 0x8B0000) + effectColor = JBColor(0xEF9A9A, 0x8B0000) + } + val attributes = if (isGreen) greenBackgroundAttributes else redBackgroundAttributes + rangeHighlighter= editor.markupModel.addRangeHighlighter( + startOffset, endOffset, HighlighterLayer.ADDITIONAL_SYNTAX, + attributes, HighlighterTargetArea.EXACT_RANGE + ) + } + + private fun extractContentAfterFirstNewline(input: String): String { + val newlineIndex = input.indexOf('\n') + return if (newlineIndex != -1) { + input.substring(newlineIndex + 1) + } else { + input + } + } + + + fun hidePopup() { + currentPopup?.dispose() + currentPopup = null + } + + private fun initPopup(panel: ChatInputPopupPanel): JBPopup { + val popup = JBPopupFactory.getInstance() + .createComponentPopupBuilder(panel, panel.textField) + .setMovable(true) + .setResizable(true) + .setTitle("Ask Amazon Q") + .setAlpha(0.1F) + .setCancelOnClickOutside(false) + .setCancelOnOtherWindowOpen(true) +// .setCancelKeyEnabled(true) + .setFocusable(true) + .setRequestFocus(true) + .setLocateWithinScreenBounds(true) + .setCancelOnWindowDeactivation(true) + .createPopup() + return popup + } + + class ChatInputPopupPanel : JPanel() { + val textField = JTextField().apply { + val editorColorsScheme = EditorColorsManager.getInstance().globalScheme + font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) + } + val submitButton = JButton("Send") + private val acceptButton = JButton("Accept") + private val rejectButton = JButton("Reject") + private var textChangeListener: ((String) -> Unit)? = null + private var submitClickListener: (() -> Unit)? = null + private val textArea = JTextArea().apply { + lineWrap = true + wrapStyleWord = true + isEditable = false + val editorColorsScheme = EditorColorsManager.getInstance().globalScheme + font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) + } +// JTextPane().apply { +// contentType = "text/html" +// editorKit = HTMLEditorKit() +// text = initialContent +// isEditable = false +// } + private val scrollPane = JBScrollPane(textArea).apply { + border = BorderFactory.createEmptyBorder(5, 20, 5, 20) + verticalScrollBarPolicy = JBScrollPane.VERTICAL_SCROLLBAR_ALWAYS + horizontalScrollBarPolicy = JBScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS + } + + override fun getPreferredSize(): Dimension { + return Dimension(600, 120) + } + + init { + layout = BorderLayout() + val inputPanel = JPanel(BorderLayout()) + inputPanel.add(submitButton, BorderLayout.EAST) + inputPanel.add(textField, BorderLayout.WEST) + textField.preferredSize = Dimension(500, 30) + submitButton.preferredSize = Dimension(80, 30) + inputPanel.preferredSize = Dimension(600, 30) + add(inputPanel, BorderLayout.NORTH) + val listener = object : DocumentListener { + override fun insertUpdate(e: DocumentEvent) { + updateText() + } + + override fun removeUpdate(e: DocumentEvent) { + updateText() + } + + override fun changedUpdate(e: DocumentEvent) { + updateText() + } + + private fun updateText() { + val newText = textField.text + textChangeListener?.invoke(newText) + } + } + textField.document.addDocumentListener(listener) + + submitButton.addActionListener { + submitClickListener?.invoke() + } +// } + } + + fun setTextChangeListener(listener: (String) -> Unit) { + textChangeListener = listener + } + + fun setSubmitClickListener(listener: () -> Unit) { + submitClickListener = listener + } + + fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit) { + setLabel("Code diff generated. Do you want to accept or reject it?") + val actionsPanel = JPanel(BorderLayout()) + actionsPanel.border = BorderFactory.createEmptyBorder(5, 20, 5, 20) + acceptButton.preferredSize = Dimension(80, 30) + rejectButton.preferredSize = Dimension(80, 30) + acceptButton.addActionListener { acceptAction.invoke() } + rejectButton.addActionListener { rejectAction.invoke() } + actionsPanel.add(acceptButton, BorderLayout.WEST) + actionsPanel.add(rejectButton, BorderLayout.EAST) + add(actionsPanel, BorderLayout.SOUTH) + revalidate() + } + + fun setLabel(text: String) { + textArea.text = text + textArea.revalidate() + scrollPane.revalidate() + if (text.isNotEmpty()){ + add(scrollPane, BorderLayout.CENTER) + } + revalidate() + } + } + + private fun getCodeBlocksRecursively(src: String): List { + val codeBlocks = mutableListOf() + var currentIndex = 0 + + while (currentIndex < src.length) { + val startIndex = src.indexOf("```", currentIndex) + if (startIndex == -1) break + + val endIndex = src.indexOf("```", startIndex + 3) + if (endIndex == -1) break + + val code = src.substring(startIndex + 3, endIndex) + codeBlocks.add(code) + + currentIndex = endIndex + 3 + } + + return codeBlocks + } + + + private suspend fun handleChat (message: String): List { + val messages = mutableListOf() + val triggerId = UUID.randomUUID().toString() + val chatSessionStorage = ChatSessionStorage(ChatSessionFactoryV1()) + val telemetryHelper = TelemetryHelper(context, chatSessionStorage) + val contextExtractor = ActiveFileContextExtractor.create(fqnWebviewAdapter = context.fqnWebviewAdapter, project = context.project) + val intentRecognizer = UserIntentRecognizer() + + val prompt = if (intentRecognizer.getUserIntentFromPromptChatMessage(message, true) != UserIntent.EXPLAIN_CODE_SELECTION) { + "$message. Please only include code blocks in your response." + } else { + "$message. Please do not include code blocks in your response." + } + + val requestData = ChatRequestData( + tabId = "inlineChat-editor", + message = prompt, + activeFileContext = contextExtractor.extractContextForTrigger(ExtractionTriggerType.ChatMessage), + userIntent = intentRecognizer.getUserIntentFromPromptChatMessage(message, true), + triggerType = TriggerType.Click, + customization = CodeWhispererModelConfigurator.getInstance().activeCustomization(context.project), + relevantTextDocuments = emptyList() + ) + val session = ChatSessionFactoryV1().create(context.project) + val sessionInfo = ChatSessionInfo(session = session, scope = scope, history = mutableListOf()) + val chat = sessionInfo.scope.async { ChatPromptHandler(telemetryHelper).handle("inlineChat-editor", triggerId, requestData, sessionInfo, false) + .catch { + // TODO: log error and show in popup window + e -> println("Error: $e") + } + .onEach { messages.add(it) } + .toList() + } + chat.await() + return messages + } + + override fun dispose() { + hidePopup() + } +} + From 6776bb6239628b683ad2ea5de288326825a61a8e Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Fri, 20 Sep 2024 10:37:24 -0700 Subject: [PATCH 03/39] update to prompt and popup --- ...otlin-compiler-10146723276903801803.salive | 0 .../services/cwc/inline/InlineChatPopup.kt | 198 ++++++++---------- 2 files changed, 89 insertions(+), 109 deletions(-) delete mode 100644 .kotlin/sessions/kotlin-compiler-10146723276903801803.salive diff --git a/.kotlin/sessions/kotlin-compiler-10146723276903801803.salive b/.kotlin/sessions/kotlin-compiler-10146723276903801803.salive deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopup.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopup.kt index 61f0f3280ce..5804845ecac 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopup.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopup.kt @@ -18,9 +18,6 @@ import com.intellij.openapi.ui.popup.JBPopupListener import com.intellij.openapi.ui.popup.LightweightWindowEvent import com.intellij.openapi.util.text.StringUtil import com.intellij.ui.JBColor -import com.intellij.ui.components.JBScrollPane -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.onEach @@ -28,7 +25,6 @@ import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import migration.software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator -import software.amazon.awssdk.services.codewhispererstreaming.model.UserIntent import software.aws.toolkits.jetbrains.core.coroutines.EDT import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext @@ -49,8 +45,8 @@ import java.awt.Font import java.util.UUID import javax.swing.BorderFactory import javax.swing.JButton +import javax.swing.JLabel import javax.swing.JPanel -import javax.swing.JTextArea import javax.swing.JTextField import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener @@ -87,7 +83,8 @@ class InlineChatPopup( revalidate() scope.launch { - val messages = handleChat(prompt) + val selectedCode = editor.selectionModel.selectedText ?: "" + val messages = handleChat(prompt, selectedCode) val src = messages.last().message ?: return@launch val codeBlocks = getCodeBlocksRecursively(src) if (codeBlocks.isNotEmpty()) { @@ -98,53 +95,42 @@ class InlineChatPopup( val caretStart = caret.selectionStart val caretEnd = caret.selectionEnd - if (caret.hasSelection()) { - if(!isVisible){ - return@withContext - } - caret.removeSelection() + if(!isVisible){ + return@withContext + } + caret.removeSelection() + WriteCommandAction.runWriteCommandAction(context.project) { + editor.document.insertString(offset, codeString) + } + undoAction = { WriteCommandAction.runWriteCommandAction(context.project) { - editor.document.insertString(offset, codeString) - } - undoAction = { - WriteCommandAction.runWriteCommandAction(context.project) { - editor.document.deleteString(offset, offset + codeString.length) - } - editor.markupModel.removeAllHighlighters() + editor.document.deleteString(offset, offset + codeString.length) } - highlightCodeWithBackgroundColor(editor, offset, offset + codeString.length, true) - highlightCodeWithBackgroundColor(editor, caretStart + codeString.length, caretEnd + codeString.length, false) - val acceptAction = { - undoAction = null - WriteCommandAction.runWriteCommandAction(context.project) { - editor.document.deleteString(caretStart + codeString.length, caretEnd + codeString.length) - } - editor.markupModel.removeAllHighlighters() - hidePopup() + editor.markupModel.removeAllHighlighters() + } + highlightCodeWithBackgroundColor(editor, offset, offset + codeString.length, true) + highlightCodeWithBackgroundColor(editor, caretStart + codeString.length, caretEnd + codeString.length, false) + val acceptAction = { + undoAction = null + WriteCommandAction.runWriteCommandAction(context.project) { + editor.document.deleteString(caretStart + codeString.length, caretEnd + codeString.length) } - val rejectAction = { - WriteCommandAction.runWriteCommandAction(context.project) { - editor.document.deleteString(offset, offset + codeString.length) - } - editor.markupModel.removeAllHighlighters() - hidePopup() + editor.markupModel.removeAllHighlighters() + hidePopup() + } + val rejectAction = { + WriteCommandAction.runWriteCommandAction(context.project) { + editor.document.deleteString(offset, offset + codeString.length) } - addCodeActionsPanel(acceptAction, rejectAction) + editor.markupModel.removeAllHighlighters() + hidePopup() } - + addCodeActionsPanel(acceptAction, rejectAction) } - return@launch + } else { + // TODO: handle throw error and show notification for this case } - -// TODO: change textArea to textPane and render markdown -// val flavour = CommonMarkFlavourDescriptor() -// val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(src) -// val html = HtmlGenerator(src, parsedTree, flavour).generateHtml() - setLabel(src) - textField.isEnabled = true - submitButton.isEnabled = true - revalidate() } } } @@ -196,14 +182,14 @@ class InlineChatPopup( .setMovable(true) .setResizable(true) .setTitle("Ask Amazon Q") - .setAlpha(0.1F) + .setAlpha(0.2F) .setCancelOnClickOutside(false) .setCancelOnOtherWindowOpen(true) // .setCancelKeyEnabled(true) .setFocusable(true) .setRequestFocus(true) .setLocateWithinScreenBounds(true) - .setCancelOnWindowDeactivation(true) + .setCancelOnWindowDeactivation(false) .createPopup() return popup } @@ -214,66 +200,64 @@ class InlineChatPopup( font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } val submitButton = JButton("Send") - private val acceptButton = JButton("Accept") - private val rejectButton = JButton("Reject") + private val acceptButton = JButton("Accept").apply { + preferredSize = Dimension(80, 30) + } + private val rejectButton = JButton("Reject").apply { + preferredSize = Dimension(80, 30) + } private var textChangeListener: ((String) -> Unit)? = null private var submitClickListener: (() -> Unit)? = null - private val textArea = JTextArea().apply { - lineWrap = true - wrapStyleWord = true - isEditable = false + private val textLabel = JLabel("").apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } -// JTextPane().apply { -// contentType = "text/html" -// editorKit = HTMLEditorKit() -// text = initialContent -// isEditable = false -// } - private val scrollPane = JBScrollPane(textArea).apply { + private val actionsPanel = JPanel(BorderLayout()).apply { + border = BorderFactory.createEmptyBorder(5, 20, 5, 20) + add(acceptButton, BorderLayout.WEST) + add(rejectButton, BorderLayout.EAST) + } + private val inputPanel = JPanel(BorderLayout()) + private val labelPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 20, 5, 20) - verticalScrollBarPolicy = JBScrollPane.VERTICAL_SCROLLBAR_ALWAYS - horizontalScrollBarPolicy = JBScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS + add(textLabel, BorderLayout.NORTH) } override fun getPreferredSize(): Dimension { - return Dimension(600, 120) + return Dimension(600, 60) } init { layout = BorderLayout() - val inputPanel = JPanel(BorderLayout()) - inputPanel.add(submitButton, BorderLayout.EAST) - inputPanel.add(textField, BorderLayout.WEST) - textField.preferredSize = Dimension(500, 30) - submitButton.preferredSize = Dimension(80, 30) - inputPanel.preferredSize = Dimension(600, 30) - add(inputPanel, BorderLayout.NORTH) - val listener = object : DocumentListener { - override fun insertUpdate(e: DocumentEvent) { - updateText() - } - - override fun removeUpdate(e: DocumentEvent) { - updateText() - } + inputPanel.add(submitButton, BorderLayout.EAST) + inputPanel.add(textField, BorderLayout.WEST) + textField.preferredSize = Dimension(500, 30) + submitButton.preferredSize = Dimension(80, 30) + inputPanel.preferredSize = Dimension(600, 30) + add(inputPanel, BorderLayout.NORTH) + val listener = object : DocumentListener { + override fun insertUpdate(e: DocumentEvent) { + updateText() + } - override fun changedUpdate(e: DocumentEvent) { - updateText() - } + override fun removeUpdate(e: DocumentEvent) { + updateText() + } - private fun updateText() { - val newText = textField.text - textChangeListener?.invoke(newText) - } + override fun changedUpdate(e: DocumentEvent) { + updateText() } - textField.document.addDocumentListener(listener) - submitButton.addActionListener { - submitClickListener?.invoke() + private fun updateText() { + val newText = textField.text + textChangeListener?.invoke(newText) } -// } + } + textField.document.addDocumentListener(listener) + + submitButton.addActionListener { + submitClickListener?.invoke() + } } fun setTextChangeListener(listener: (String) -> Unit) { @@ -285,26 +269,20 @@ class InlineChatPopup( } fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit) { - setLabel("Code diff generated. Do you want to accept or reject it?") - val actionsPanel = JPanel(BorderLayout()) - actionsPanel.border = BorderFactory.createEmptyBorder(5, 20, 5, 20) - acceptButton.preferredSize = Dimension(80, 30) - rejectButton.preferredSize = Dimension(80, 30) + textLabel.text = "Code diff generated. Do you want to accept it?" + textLabel.revalidate() + inputPanel.revalidate() acceptButton.addActionListener { acceptAction.invoke() } rejectButton.addActionListener { rejectAction.invoke() } - actionsPanel.add(acceptButton, BorderLayout.WEST) - actionsPanel.add(rejectButton, BorderLayout.EAST) add(actionsPanel, BorderLayout.SOUTH) revalidate() } fun setLabel(text: String) { - textArea.text = text - textArea.revalidate() - scrollPane.revalidate() - if (text.isNotEmpty()){ - add(scrollPane, BorderLayout.CENTER) - } + textLabel.text = text + textLabel.revalidate() + remove(inputPanel) + add(labelPanel) revalidate() } } @@ -330,7 +308,7 @@ class InlineChatPopup( } - private suspend fun handleChat (message: String): List { + private suspend fun handleChat (message: String, selectedCode: String = ""): List { val messages = mutableListOf() val triggerId = UUID.randomUUID().toString() val chatSessionStorage = ChatSessionStorage(ChatSessionFactoryV1()) @@ -338,11 +316,13 @@ class InlineChatPopup( val contextExtractor = ActiveFileContextExtractor.create(fqnWebviewAdapter = context.fqnWebviewAdapter, project = context.project) val intentRecognizer = UserIntentRecognizer() - val prompt = if (intentRecognizer.getUserIntentFromPromptChatMessage(message, true) != UserIntent.EXPLAIN_CODE_SELECTION) { - "$message. Please only include code blocks in your response." - } else { - "$message. Please do not include code blocks in your response." - } + val prompt = "You are an expert programmer assisting with code improvement. " + + "I will provide you with selected code from an IDE and a user query about how to improve it. " + + "Your task is to generate improved code based on the user's request. Rules - Output only the improved code, with no explanatory text or comments - " + + "Preserve the original code formatting, tab size and structure as much as possible - Enclose all code in Markdown fenced code blocks - " + + "Do not include any additional text or instructions." + + "Selected code: $selectedCode. User query: $message. Provide the improved code below:" + val requestData = ChatRequestData( tabId = "inlineChat-editor", @@ -357,7 +337,7 @@ class InlineChatPopup( val sessionInfo = ChatSessionInfo(session = session, scope = scope, history = mutableListOf()) val chat = sessionInfo.scope.async { ChatPromptHandler(telemetryHelper).handle("inlineChat-editor", triggerId, requestData, sessionInfo, false) .catch { - // TODO: log error and show in popup window + // TODO: log error and show notification e -> println("Error: $e") } .onEach { messages.add(it) } From a30e825d54024d34e46f2ed4eac937db01ee55b6 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Mon, 30 Sep 2024 11:10:16 -0700 Subject: [PATCH 04/39] bugbash --- .../chat/jetbrains-community/build.gradle.kts | 1 + .../resources/META-INF/plugin-chat.xml | 11 + .../cwc/clients/chat/v1/ChatSessionV1.kt | 3 +- .../services/cwc/controller/ChatController.kt | 2 +- .../chat/messenger/ChatPromptHandler.kt | 5 + .../chat/telemetry/TelemetryHelper.kt | 36 +- .../context/ActiveFileContextExtractor.kt | 2 +- .../context/file/FileContextExtractor.kt | 2 +- .../context/file/util/MatchPolicyExtractor.kt | 4 +- .../focusArea/FocusAreaContextExtractor.kt | 4 +- .../services/cwc/inline/ChatCaretListener.kt | 7 +- .../cwc/inline/InlineChatCodeVisionManager.kt | 81 +++ .../inline/InlineChatCodeVisionProvider.kt | 160 +++++ .../cwc/inline/InlineChatController.kt | 649 ++++++++++++++++++ .../services/cwc/inline/InlineChatPopup.kt | 354 ---------- .../cwc/inline/InlineChatPopupFactory.kt | 286 ++++++++ .../cwc/inline/OpenChatInputAction.kt | 14 + .../inline/actions/InlineChatAcceptAction.kt | 49 ++ .../inline/actions/InlineChatRejectAction.kt | 32 + .../credentials/CodeWhispererClientAdaptor.kt | 54 ++ .../codewhispererruntime/service-2.json | 27 +- 21 files changed, 1411 insertions(+), 372 deletions(-) create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionManager.kt create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionProvider.kt create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt delete mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopup.kt create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatAcceptAction.kt create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatRejectAction.kt diff --git a/plugins/amazonq/chat/jetbrains-community/build.gradle.kts b/plugins/amazonq/chat/jetbrains-community/build.gradle.kts index d3ac5334b04..c50de2daf7c 100644 --- a/plugins/amazonq/chat/jetbrains-community/build.gradle.kts +++ b/plugins/amazonq/chat/jetbrains-community/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(project(":plugin-amazonq:codewhisperer:jetbrains-community")) implementation(libs.nimbus.jose.jwt) implementation("org.jetbrains:markdown:0.7.3") + implementation("io.github.java-diff-utils:java-diff-utils:4.12") compileOnly(project(":plugin-core:jetbrains-community")) diff --git a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml index daec47d1094..cc8f7014ee1 100644 --- a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml +++ b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml @@ -20,6 +20,7 @@ factoryClass="software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory" icon="AwsIcons.Logos.AWS_Q" /> + @@ -29,6 +30,16 @@ + + + + + + + + + + diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt index a5057f797c8..8fbd51e9dd8 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt @@ -178,7 +178,8 @@ class ChatSessionV1( logger.info { "Request from tab: ${data.tabId}, conversationId: $conversationId, request: $request" } client.generateAssistantResponse(request, responseHandler).await() } - } catch (e: TimeoutCancellationException) { + } catch (e: Exception) { + println(e.message) // Re-throw an exception that can be caught downstream throw ChatApiException( message = "API request timed out", diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt index 846bc3476eb..93c186b86a5 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt @@ -96,7 +96,7 @@ class ChatController private constructor( ) : InboundAppMessagesHandler { private val messagePublisher: MessagePublisher = context.messagesFromAppToUi - private val telemetryHelper = TelemetryHelper(context, chatSessionStorage) + private val telemetryHelper = TelemetryHelper(context.project, chatSessionStorage) constructor( context: AmazonQAppInitContext, ) : this( diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt index e1804ef0f00..8a289d18f27 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import software.amazon.awssdk.awscore.exception.AwsServiceException import software.amazon.awssdk.services.codewhispererstreaming.model.CodeWhispererStreamingException @@ -55,6 +56,7 @@ class ChatPromptHandler(private val telemetryHelper: TelemetryHelper) { data: ChatRequestData, sessionInfo: ChatSessionInfo, shouldAddIndexInProgressMessage: Boolean, + isInlineChat: Boolean = false ) = flow { val session = sessionInfo.session session.chat(data) @@ -135,6 +137,9 @@ class ChatPromptHandler(private val telemetryHelper: TelemetryHelper) { ) } } + .onEach { responseEvent -> + if(isInlineChat) processChatEvent(tabId, triggerId, responseEvent, shouldAddIndexInProgressMessage)?.let { emit(it) } + } .collect { responseEvent -> processChatEvent( tabId, diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt index 8a09dea2bb4..695c9811b61 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt @@ -3,9 +3,11 @@ package software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry +import com.intellij.openapi.project.Project import org.jetbrains.annotations.VisibleForTesting import software.amazon.awssdk.services.codewhispererruntime.model.ChatInteractWithMessageEvent import software.amazon.awssdk.services.codewhispererruntime.model.ChatMessageInteractionType +import software.amazon.awssdk.services.codewhispererruntime.model.InlineChatUserDecision import software.amazon.awssdk.services.codewhispererstreaming.model.UserIntent import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment import software.aws.toolkits.core.utils.debug @@ -39,14 +41,14 @@ import java.time.Duration import java.time.Instant import software.amazon.awssdk.services.codewhispererruntime.model.UserIntent as CWClientUserIntent -class TelemetryHelper(private val context: AmazonQAppInitContext, private val sessionStorage: ChatSessionStorage) { +class TelemetryHelper(private val project: Project, private val sessionStorage: ChatSessionStorage) { private val responseStreamStartTime: MutableMap = mutableMapOf() private val responseStreamTotalTime: MutableMap = mutableMapOf() private val responseStreamTimeForChunks: MutableMap> = mutableMapOf() private val responseHasProjectContext: MutableMap = mutableMapOf() private val customization: CodeWhispererCustomization? - get() = CodeWhispererModelConfigurator.getInstance().activeCustomization(context.project) + get() = CodeWhispererModelConfigurator.getInstance().activeCustomization(project) fun getConversationId(tabId: String): String? = sessionStorage.getSession(tabId)?.session?.conversationId @@ -116,7 +118,7 @@ class TelemetryHelper(private val context: AmazonQAppInitContext, private val se cwsprChatRequestLength = data.message.length.toLong(), cwsprChatResponseLength = responseLength.toLong(), cwsprChatConversationType = CwsprChatConversationType.Chat, - credentialStartUrl = getStartUrl(context.project), + credentialStartUrl = getStartUrl(project), codewhispererCustomizationArn = data.customization?.arn, cwsprChatHasProjectContext = getMessageHasProjectContext(response.messageId) ) @@ -124,7 +126,7 @@ class TelemetryHelper(private val context: AmazonQAppInitContext, private val se val programmingLanguage = data.activeFileContext.fileContext?.fileLanguage val validProgrammingLanguage = if (ChatSessionV1.validLanguages.contains(programmingLanguage)) programmingLanguage else null - CodeWhispererClientAdaptor.getInstance(context.project).sendChatAddMessageTelemetry( + CodeWhispererClientAdaptor.getInstance(project).sendChatAddMessageTelemetry( getConversationId(response.tabId).orEmpty(), response.messageId, CWClientUserIntent.fromValue(data.userIntent?.name), @@ -146,6 +148,28 @@ class TelemetryHelper(private val context: AmazonQAppInitContext, private val se } } + fun recordInlineChatTelemetry(requestId: String, + inputLength: Int?, + numSelectedLines: Int?, + codeIntent: Boolean?, + userDecision: InlineChatUserDecision?, + responseStartLatency: Double?, + responseEndLatency: Double?, + numSuggestionAddChars: Int?, + numSuggestionAddLines: Int?, + numSuggestionDelChars: Int?, + numSuggestionDelLines: Int?, + charactersAdded: Int?, + charactersRemoved: Int?) { + CodeWhispererClientAdaptor.getInstance(project).sendInlineChatTelemetry(requestId, inputLength, numSelectedLines, codeIntent, userDecision, + responseStartLatency, responseEndLatency,numSuggestionAddChars, numSuggestionAddLines, numSuggestionDelChars, numSuggestionDelLines, + charactersAdded, charactersRemoved).also { + logger.debug { + "Successfully sendTelemetryEvent for InlineChat with requestId=${it.responseMetadata().requestId()}" + } + } + } + fun recordMessageResponseError(data: ChatRequestData, tabId: String, responseCode: Int) { AmazonqTelemetry.messageResponseError( cwsprChatConversationId = getConversationId(tabId).orEmpty(), @@ -158,7 +182,7 @@ class TelemetryHelper(private val context: AmazonQAppInitContext, private val se cwsprChatResponseCode = responseCode.toLong(), cwsprChatRequestLength = data.message.length.toLong(), cwsprChatConversationType = CwsprChatConversationType.Chat, - credentialStartUrl = getStartUrl(context.project) + credentialStartUrl = getStartUrl(project) ) } @@ -308,7 +332,7 @@ class TelemetryHelper(private val context: AmazonQAppInitContext, private val se } event?.let { - val steResponse = CodeWhispererClientAdaptor.getInstance(context.project).sendChatInteractWithMessageTelemetry(it) + val steResponse = CodeWhispererClientAdaptor.getInstance(project).sendChatInteractWithMessageTelemetry(it) logger.debug { "Successfully sendTelemetryEvent for ChatInteractWithMessage with requestId=${steResponse.responseMetadata().requestId()}" } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/ActiveFileContextExtractor.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/ActiveFileContextExtractor.kt index f2e6a2df082..7d1231b9551 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/ActiveFileContextExtractor.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/ActiveFileContextExtractor.kt @@ -34,7 +34,7 @@ class ActiveFileContextExtractor( } companion object { - fun create(fqnWebviewAdapter: FqnWebviewAdapter, project: Project) = ActiveFileContextExtractor( + fun create(fqnWebviewAdapter: FqnWebviewAdapter?, project: Project) = ActiveFileContextExtractor( fileContextExtractor = FileContextExtractor(fqnWebviewAdapter, project), focusAreaContextExtractor = FocusAreaContextExtractor(fqnWebviewAdapter, project), ) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/FileContextExtractor.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/FileContextExtractor.kt index 1998c3fe321..36249508b44 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/FileContextExtractor.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/FileContextExtractor.kt @@ -14,7 +14,7 @@ import software.aws.toolkits.jetbrains.services.cwc.editor.context.file.util.Lan import software.aws.toolkits.jetbrains.services.cwc.editor.context.file.util.MatchPolicyExtractor import software.aws.toolkits.jetbrains.utils.computeOnEdt -class FileContextExtractor(private val fqnWebviewAdapter: FqnWebviewAdapter, private val project: Project) { +class FileContextExtractor(private val fqnWebviewAdapter: FqnWebviewAdapter?, private val project: Project) { private val languageExtractor: LanguageExtractor = LanguageExtractor() suspend fun extract(): FileContext? { val editor = computeOnEdt { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/MatchPolicyExtractor.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/MatchPolicyExtractor.kt index 0585407bca7..4ec873aa671 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/MatchPolicyExtractor.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/MatchPolicyExtractor.kt @@ -15,7 +15,7 @@ object MatchPolicyExtractor { isCodeSelected: Boolean = false, fileLanguage: String?, fileText: String?, - fqnWebviewAdapter: FqnWebviewAdapter, + fqnWebviewAdapter: FqnWebviewAdapter?, ): MatchPolicy? { val should = extractAdditionalLanguageMatchPolicies(fileLanguage) @@ -29,7 +29,7 @@ object MatchPolicyExtractor { val requestString = ChatController.objectMapper.writeValueAsString(readImportsRequest) return try { - val importsString = fqnWebviewAdapter.readImports(requestString) + val importsString = fqnWebviewAdapter?.readImports(requestString) ?: "[]" val imports = ChatController.objectMapper.readValue>(importsString) imports diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt index ceadc7c41d5..692a2618acd 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt @@ -20,7 +20,7 @@ import software.aws.toolkits.jetbrains.utils.computeOnEdt import java.awt.Point import kotlin.math.min -class FocusAreaContextExtractor(private val fqnWebviewAdapter: FqnWebviewAdapter, private val project: Project) { +class FocusAreaContextExtractor(private val fqnWebviewAdapter: FqnWebviewAdapter?, private val project: Project) { private val languageExtractor: LanguageExtractor = LanguageExtractor() suspend fun extract(): FocusAreaContext? { @@ -140,7 +140,7 @@ class FocusAreaContextExtractor(private val fqnWebviewAdapter: FqnWebviewAdapter val requestString = ChatController.objectMapper.writeValueAsString(extractNamesRequest) codeNames = try { - val namesString = fqnWebviewAdapter.extractNames(requestString) + val namesString = fqnWebviewAdapter?.let { it.extractNames(requestString) } ?: "{\"simpleNames\": [], \"fullyQualifiedNames\": {\"used\": []}}" ChatController.objectMapper.readValue(namesString, CodeNamesImpl::class.java) } catch (e: Exception) { getLogger().warn(e) { "Failed to extract names from file" } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt index f4173727cf5..36287c5d322 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt @@ -20,13 +20,14 @@ import javax.swing.Icon class ChatCaretListener(private val project: Project, private val context: AmazonQAppInitContext) : CaretListener { private var currentHighlighter: RangeHighlighter? = null - private var currentPopup: InlineChatPopup? = null +// private var currentPopup: InlineChatPopup? = null init { val editor = FileEditorManager.getInstance(project).selectedTextEditor editor?.caretModel?.addCaretListener(this) } override fun caretPositionChanged(event: CaretEvent) { +// InlineChatCodeVisionProvider() val editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return val lineNumber = event.newPosition.line val startOffset = editor.document.getLineStartOffset(lineNumber) @@ -34,8 +35,8 @@ class ChatCaretListener(private val project: Project, private val context: Amazo val markupModel: MarkupModel = editor.markupModel val gutterIconRenderer = ChatGutterIconRenderer(AllIcons.Actions.Lightning).apply { setClickAction { - currentPopup?.hidePopup() - currentPopup = InlineChatPopup(editor, context) +// currentPopup?.hidePopup() + InlineChatController(editor, context.project).initPopup() } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionManager.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionManager.kt new file mode 100644 index 00000000000..b2ce86e6552 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionManager.kt @@ -0,0 +1,81 @@ +//import com.intellij.codeInsight.hints.ChangeListener +//import com.intellij.codeInsight.hints.ImmediateConfigurable +//import com.intellij.codeInsight.hints.InlayHintsCollector +//import com.intellij.codeInsight.hints.InlayHintsManager +//import com.intellij.codeInsight.hints.InlayHintsProvider +//import com.intellij.codeInsight.hints.InlayHintsSink +//import com.intellij.codeInsight.hints.NoSettings +//import com.intellij.codeInsight.hints.presentation.InlayPresentation +//import com.intellij.openapi.editor.Editor +//import com.intellij.openapi.editor.event.SelectionEvent +//import com.intellij.openapi.editor.event.SelectionListener +//import com.intellij.openapi.project.Project +//import com.intellij.psi.PsiFile +//import javax.swing.JPanel +// +//class InlineChatCodeVisionManager : InlayHintsProvider { +// private var selectionListener: SelectionListener? = null +// +// override fun createConfigurable(settings: NoSettings) = object : ImmediateConfigurable { +// override fun createComponent(listener: ChangeListener) = JPanel() +// } +// +// override fun getCollectorFor( +// file: PsiFile, +// editor: Editor, +// settings: NoSettings, +// sink: InlayHintsSink +// ): InlayHintsCollector? { +// val project = file.project +// +// // Remove any existing listener +// selectionListener?.let { editor.selectionModel.removeSelectionListener(it) } +// +// // Create a new listener +// selectionListener = object : SelectionListener { +// override fun selectionChanged(e: SelectionEvent) { +// // Trigger a refresh of inlay hints +// InlayHintsManager.getInstance(project).refreshInlayHints(editor) +// } +// } +// +// // Add the new listener +// editor.selectionModel.addSelectionListener(selectionListener!!) +// +// return object : InlayHintsCollector { +// override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean { +// val selectionModel = editor.selectionModel +// val selectionStart = selectionModel.selectionStart +// val selectionEnd = selectionModel.selectionEnd +// +// if (selectionStart == selectionEnd) { +// return true // No selection, no hints +// } +// +// if (element.textRange.intersects(selectionStart, selectionEnd)) { +// val elementType = element.node.elementType.toString().toLowerCase() +// if (elementType.contains("function") || elementType.contains("method") || elementType.contains("class")) { +// val hint = createInlayPresentation(element, editor, project) +// sink.addInlineElement(element.textOffset, true, hint) +// } +// } +// +// return true +// } +// } +// } +// +// private fun createInlayPresentation(element: PsiElement, editor: Editor, project: Project): InlayPresentation { +// // Create and return your inlay presentation here +// // This could be a simple text presentation or a more complex clickable presentation +// } +// +// override fun getKey() = SettingsKey("InlineChatCodeVision") +// override fun getName() = "Inline Chat Code Vision" +// override fun createSettings() = NoSettings() +// +// override fun dispose() { +// // Clean up the listener when the provider is disposed +// selectionListener = null +// } +//} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionProvider.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionProvider.kt new file mode 100644 index 00000000000..ba0020bf29b --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionProvider.kt @@ -0,0 +1,160 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cwc.inline + +import com.intellij.codeInsight.codeVision.CodeVisionAnchorKind +import com.intellij.codeInsight.codeVision.CodeVisionEntry +import com.intellij.codeInsight.codeVision.CodeVisionHost +import com.intellij.codeInsight.codeVision.CodeVisionProvider +import com.intellij.codeInsight.codeVision.CodeVisionProviderFactory +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.ClickableTextCodeVisionEntry +import com.intellij.codeInsight.codeVision.ui.model.TextCodeVisionEntry +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer +import com.intellij.codeInsight.hints.InlayHintsUtils +import com.intellij.history.integration.ui.views.RevisionsList +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.SelectionEvent +import com.intellij.openapi.editor.event.SelectionListener +import com.intellij.openapi.editor.toolbar.floating.EditorFloatingToolbar +import com.intellij.openapi.editor.toolbar.floating.FloatingToolbarProvider +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.SyntaxTraverser +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.elementType +import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext +import java.awt.event.MouseEvent +import java.util.UUID +import kotlin.math.max +import kotlin.math.min + +class InlineChatCodeVisionProvider: CodeVisionProvider { + companion object { + internal const val id: String = "amazonq.chat.code.vision" + } + override val id: String = Companion.id + override val defaultAnchor: CodeVisionAnchorKind = CodeVisionAnchorKind.Top + override val name: String = "AmazonQ Chat Code Vision" + override val relativeOrderings: List = emptyList() +// private var selectionListener: SelectionListener? = null +// private var lenses = ArrayList>() + + +// override fun precomputeOnUiThread(editor: Editor) { +// } + + override fun precomputeOnUiThread(editor: Editor) { + // Remove any existing listener +// selectionListener?.let { editor.selectionModel.removeSelectionListener(it) } +// +// // Create a new listener +// selectionListener = object : SelectionListener { +// override fun selectionChanged(e: SelectionEvent) { +// // Trigger a refresh of code visions +// editor.project?.let { project -> +// DaemonCodeAnalyzer.getInstance(project).restart() +// } +// } +// } +// +// // Add the new listener +// editor.selectionModel.addSelectionListener(selectionListener!!) +// +//// // Generate initial code visions +//// computeCodeVision(editor, Unit) + } + + override fun shouldRecomputeForEditor(editor: Editor, uiData: Unit?): Boolean = true + + override fun isAvailableFor(project: Project): Boolean = true + + + override fun computeCodeVision(editor: Editor, uiData: Unit): CodeVisionState { + return runReadAction { + val lenses = ArrayList>() + val project = editor.project ?: return@runReadAction READY_EMPTY + val document = editor.document + val file = PsiDocumentManager.getInstance(project).getPsiFile(document) ?: return@runReadAction READY_EMPTY + val traverser = SyntaxTraverser.psiTraverser(file) + + val text = "Amazon Q: Edit \u2318 + I" + val elements = traverser.preOrderDfsTraversal().filter { element -> + val elementType = element.elementType.toString().toLowerCase() + elementType.contains("function") || +// elementType.contains("method") || + elementType.contains("class") + } + val clickHandler : + (MouseEvent?, Editor)-> Unit = { e: MouseEvent?, _ -> + InlineChatController(editor, editor.project!!).initPopup() + } + FloatingToolbarProvider + for (element in elements) { + val textRange = InlayHintsUtils.getTextRangeWithoutLeadingCommentsAndWhitespaces(element) + val length = editor.document.textLength + val adjustedRange = TextRange(min(textRange.startOffset, length), min(textRange.endOffset, length)) + val entry = ClickableTextCodeVisionEntry(text, id, clickHandler, icon = null, text, text, emptyList()) + lenses.add(Pair(adjustedRange, entry)) + } + return@runReadAction CodeVisionState.Ready(lenses) + } +// return runReadAction { +// val lenses = ArrayList>() +// val project = editor.project ?: return@runReadAction READY_EMPTY +// val controller = InlineChatController(editor, editor.project!!) +// val document = editor.document +// val file = PsiDocumentManager.getInstance(project).getPsiFile(document) ?: return@runReadAction READY_EMPTY +// +// val selectionModel = editor.selectionModel +// val selectionStart = selectionModel.selectionStart +// val selectionEnd = selectionModel.selectionEnd +// +// // If there's no selection, return empty result +// var ranges: List = emptyList() +// if (selectionStart == selectionEnd) { +// val offset = editor.caretModel.offset +// val nearestFunction = PsiTreeUtil.findFirstParent(file.findElementAt(offset)) { element -> +// element.node.elementType.toString().toLowerCase().contains("function") || +// element.node.elementType.toString().toLowerCase().contains("method") +// } +// +// if (nearestFunction != null) { +// val textRange = nearestFunction.textRange +// ranges = listOf(textRange) +// } +// } else { +// val traverser = SyntaxTraverser.psiTraverser(file) +// ranges = traverser.preOrderDfsTraversal() +// .filter { element -> +// val elementType = element.elementType.toString().toLowerCase() +// element.textRange.intersects(selectionStart, selectionEnd) && +// elementType.contains("function") || +// elementType.contains("method") || +// elementType.contains("class") +// }.map { element -> element.textRange }.toMutableList() +// } +// +// for (range in ranges) { +// val adjustedRange = TextRange( +// max(range.startOffset, selectionStart), +// min(range.endOffset, selectionEnd) +// ) +// val clickHandler: (MouseEvent?, Editor) -> Unit = { e: MouseEvent?, _ -> +// controller.initPopup() +// } +// val text = if(controller.getIsInProgress() || controller.getShouldShowActions()) "Chat is working..." else "AmazonQ Chat" +// val entry = ClickableTextCodeVisionEntry(text, id, clickHandler, icon = null, text, text, emptyList()) +// lenses.add(Pair(adjustedRange, entry)) +// } +// +// return@runReadAction CodeVisionState.Ready(lenses) +// } + } +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt new file mode 100644 index 00000000000..039327ae818 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -0,0 +1,649 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cwc.inline + +import com.github.difflib.text.DiffRow +import com.github.difflib.text.DiffRowGenerator +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.command.CommandProcessor +import com.intellij.openapi.command.UndoConfirmationPolicy +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.RangeMarker +import com.intellij.openapi.editor.markup.HighlighterLayer +import com.intellij.openapi.editor.markup.HighlighterTargetArea +import com.intellij.openapi.editor.markup.RangeHighlighter +import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.ui.popup.JBPopupListener +import com.intellij.openapi.ui.popup.LightweightWindowEvent +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.ui.JBColor +import com.intellij.ui.jcef.JBCefApp +import com.jetbrains.rd.util.AtomicInteger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import migration.software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator +import org.apache.commons.text.StringEscapeUtils +import software.amazon.awssdk.services.codewhispererruntime.model.InlineChatUserDecision +import software.aws.toolkits.core.utils.debug +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.core.utils.info +import software.aws.toolkits.core.utils.warn +import software.aws.toolkits.jetbrains.core.coroutines.EDT +import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope +import software.aws.toolkits.jetbrains.core.gettingstarted.requestCredentialsForQ +import software.aws.toolkits.jetbrains.core.webview.BrowserState +import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel +import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController +import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AMAZON_Q_WINDOW_ID +import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData +import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerType +import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.messenger.ChatPromptHandler +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.TelemetryHelper +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.userIntent.UserIntentRecognizer +import software.aws.toolkits.jetbrains.services.cwc.editor.context.ActiveFileContext +import software.aws.toolkits.jetbrains.services.cwc.editor.context.ActiveFileContextExtractor +import software.aws.toolkits.jetbrains.services.cwc.editor.context.ExtractionTriggerType +import software.aws.toolkits.jetbrains.services.cwc.editor.context.file.FileContextExtractor +import software.aws.toolkits.jetbrains.services.cwc.editor.context.file.util.LanguageExtractor +import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessage +import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType +import software.aws.toolkits.jetbrains.services.cwc.storage.ChatSessionStorage +import software.aws.toolkits.telemetry.FeatureId +import java.util.Stack +import java.util.UUID +import java.util.concurrent.atomic.AtomicBoolean + + +class InlineChatController( + private val editor: Editor, + private val project: Project +) : Disposable { + private var currentPopup: JBPopup? = null + private val scope = disposableCoroutineScope(this) + private var rangeHighlighter: RangeHighlighter? = null + private val partialUndoActions = Stack<() -> Unit>() + private val partialAcceptActions = Stack<() -> Unit>() + private var selectionStartLine = AtomicInteger(0) + private val sessionStorage = ChatSessionStorage() + private val telemetryHelper = TelemetryHelper(project, sessionStorage) + private val shouldShowActions = AtomicBoolean(false) + private val isInProgress = AtomicBoolean(false) + private var metrics: InlineChatMetrics? = null + private var isPopupAborted = AtomicBoolean(true) + + data class InlineChatMetrics( + val requestId: String, + val inputLength: Int? = null, + val numSelectedLines: Int? = null, + val codeIntent: Boolean? = null, + var userDecision: InlineChatUserDecision? = null, + val responseStartLatency: Double? = null, + val responseEndLatency: Double? = null, + var numSuggestionAddChars: Int? = null, + var numSuggestionAddLines: Int? = null, + var numSuggestionDelChars: Int? = null, + var numSuggestionDelLines: Int? = null, + var charactersAdded: Int? = null, + var charactersRemoved: Int? = null, + ) + + private val popupSubmitHandler: suspend (String, String, Int) -> String = { prompt: String, selectedCode: String, selectedLineStart: Int -> +// val selectedCode = getSelectedText(editor) + runBlocking { + isInProgress.set(true) + val message = handleChat(prompt, selectedCode, editor, selectedLineStart) + message + } + } + + private val popupCancelHandler: () -> Unit = { + if (isPopupAborted.get() && currentPopup != null) { + scope.launch(Dispatchers.EDT) { + while (partialUndoActions.isNotEmpty()) { + val action = partialUndoActions.pop() + runChangeAction(project, action) + } + partialAcceptActions.clear() + } + ApplicationManager.getApplication().executeOnPooledThread { + recordInlineChatTelemetry(InlineChatUserDecision.DISMISS) + } + } + } + + private fun recordInlineChatTelemetry(decision: InlineChatUserDecision) { + if(metrics == null) return + metrics?.userDecision = decision + if (decision == InlineChatUserDecision.ACCEPT) { + metrics?.charactersAdded = metrics?.numSuggestionAddChars + metrics?.charactersRemoved = metrics?.numSuggestionDelChars + } + metrics?.requestId?.let { + telemetryHelper.recordInlineChatTelemetry( + it, + metrics?.inputLength, + metrics?.numSelectedLines, + metrics?.codeIntent, + metrics?.userDecision, + metrics?.responseStartLatency, + metrics?.responseEndLatency, + metrics?.numSuggestionAddChars, + metrics?.numSuggestionAddLines, + metrics?.numSuggestionDelChars, + metrics?.numSuggestionDelLines, + metrics?.charactersAdded, + metrics?.charactersRemoved + ) + } + metrics = null + } + + + val diffAcceptHandler: () -> Unit = { + scope.launch(Dispatchers.EDT) { + partialUndoActions.clear() + while (partialAcceptActions.isNotEmpty()) { + val action = partialAcceptActions.pop() + runChangeAction(project, action) + } + invokeLater { hidePopup() } +// hidePopup() + } + ApplicationManager.getApplication().executeOnPooledThread { + recordInlineChatTelemetry(InlineChatUserDecision.ACCEPT) + } + + } + + val diffRejectHandler: () -> Unit = { + scope.launch(Dispatchers.EDT) { + while (partialUndoActions.isNotEmpty()) { + val action = partialUndoActions.pop() + runChangeAction(project, action) + } + partialAcceptActions.clear() + invokeLater { hidePopup() } + } + ApplicationManager.getApplication().executeOnPooledThread { + recordInlineChatTelemetry(InlineChatUserDecision.REJECT) + } + } + + private fun addPopupListeners(popup: JBPopup) { + val popupListener = object : JBPopupListener { + + override fun onClosed(event: LightweightWindowEvent) { + if (isPopupAborted.get() && event.asPopup().isDisposed) { + popupCancelHandler.invoke() + } + } + // telemetryHelper.recordInlineChatTelemetry(prompt.length, numOfLinesSelected, true, +// InlineChatUserDecision.DISMISS, 0.0, requestEndLatency) + } + popup.addListener(popupListener) + } + + + fun initPopup () { +// currentPopup?.dispose() + currentPopup?.let { Disposer.dispose(it) } + currentPopup = InlineChatPopupFactory(acceptHandler = diffAcceptHandler, rejectHandler = diffRejectHandler, editor = editor, + telemetryHelper = telemetryHelper, submitHandler = popupSubmitHandler, cancelHandler = popupCancelHandler, scope = scope).createPopup() + addPopupListeners(currentPopup!!) + Disposer.register(this, currentPopup!!) + isPopupAborted.set(true) + + } + + private fun highlightCodeWithBackgroundColor(editor: Editor, startOffset: Int, endOffset: Int, isGreen: Boolean) { + + val greenBackgroundAttributes = TextAttributes().apply { + backgroundColor = JBColor(0x66BB6A, 0x006400) + effectColor = JBColor(0x66BB6A, 0x006400) + } + + val redBackgroundAttributes = TextAttributes().apply { + backgroundColor = JBColor(0xEF9A9A, 0x8B0000) + effectColor = JBColor(0xEF9A9A, 0x8B0000) + } + val attributes = if (isGreen) greenBackgroundAttributes else redBackgroundAttributes + rangeHighlighter= editor.markupModel.addRangeHighlighter( + startOffset, endOffset, HighlighterLayer.SELECTION + 1, + attributes, HighlighterTargetArea.EXACT_RANGE + ) + } + + private fun extractContentAfterFirstNewline(input: String): String { + val newlineIndex = input.indexOf('\n') + return if (newlineIndex != -1) { + input.substring(newlineIndex + 1) + } else { + input + } + } + + + private fun hidePopup() { + isPopupAborted.set(false) + currentPopup?.closeOk(null) + currentPopup = null + isInProgress.set(false) + shouldShowActions.set(false) + } + + private fun getCodeBlocks(src: String): List { + val codeBlocks = mutableListOf() + var currentIndex = 0 + + while (currentIndex < src.length) { + val startIndex = src.indexOf("```", currentIndex) + if (startIndex == -1) break + + val endIndex = src.indexOf("```", startIndex + 3) + if (endIndex == -1) break + + val code = src.substring(startIndex + 3, endIndex) + codeBlocks.add(code) + + currentIndex = endIndex + 3 + } + + return codeBlocks + } + + private fun compareDiffs(original: List, recommendation: List): List { + val generator = DiffRowGenerator.create().showInlineDiffs(false).build() + val rows: List = generator.generateDiffRows(original, recommendation) + return rows + } + +// private fun incrementRowNumber() { +// selectionStartLine.incrementAndGet() +// } +// +// private fun decrementRowNumber() { +// selectionStartLine.decrementAndGet() +// } +// +// private fun getRowNumber(index: Int): Int { +// return index + selectionStartLine.get() +// } + + fun getShouldShowActions(): Boolean { + return shouldShowActions.get() + } + + fun getIsInProgress(): Boolean { + return isInProgress.get() + } + + private fun unescape(s: String): String { + return StringEscapeUtils.unescapeHtml3(s) + .replace(""", "\"") + .replace("'", "'") + .replace("=>", "=>") + } + + private suspend fun processChatMessage(selectedCode: String, event: ChatMessage, editor: Editor, selectedLineStart: Int) { + if(event.message?.isNotEmpty() == true) { + val codeBlocks = getCodeBlocks(event.message) + if(codeBlocks.isEmpty()) { + if (event.messageType == ChatMessageType.Answer) { + isInProgress.set(false) +// logger.warn { "No code block found in inline chat response with requestId: ${event.messageId}" } + } + logger.info { "No code block found in inline chat response with requestId: ${event.messageId} \nresponse: ${event.message} " } + return + } + val recommendation = unescape(extractContentAfterFirstNewline(codeBlocks.first())) + logger.info { "Recived Inline chat code recommendation:\n ```$recommendation``` \nfrom requestId: ${event.messageId}" } + logger.info { "Original selected code:\n ```$selectedCode```" } + val diff = compareDiffs(selectedCode.split("\n"), recommendation.split("\n")) + while (partialUndoActions.isNotEmpty()) { + val action = partialUndoActions.pop() + action.invoke() + } + partialAcceptActions.clear() + selectionStartLine = AtomicInteger(selectedLineStart) + var currentDocumentLine = selectedLineStart + var insertLine = selectedLineStart + if(event.codeReference?.isNotEmpty() == true) { + editor.project?.let { ReferenceLogController.addReferenceLog(recommendation, event.codeReference, editor, it) } + } + + var deletedCharsCount = 0 + var addedCharsCount = 0 + var addedLinesCount = 0 + var deletedLinesCount = 0 + + if (currentPopup?.isVisible != true) { + logger.debug { "inline chat popup cancelled before diff is shown" } + isInProgress.set(false) + recordInlineChatTelemetry(InlineChatUserDecision.DISMISS) + return + } + diff.forEach { row -> + when (row.tag) { + DiffRow.Tag.EQUAL -> { + currentDocumentLine++ + insertLine++ + } + DiffRow.Tag.DELETE, DiffRow.Tag.CHANGE -> { + try { + if (row.tag == DiffRow.Tag.CHANGE && row.newLine.trimIndent() == row.oldLine?.trimIndent()) return + showCodeChangeInEditor(row, currentDocumentLine, editor) + } catch (e: Exception) { + e.printStackTrace() + } +// currentDocumentLine++ + + if (row.tag == DiffRow.Tag.CHANGE) { + insertLine+=2 + currentDocumentLine+=2 + } else { + insertLine++ + currentDocumentLine++ + } + deletedLinesCount++ + deletedCharsCount += row.oldLine?.length?: 0 + } + DiffRow.Tag.INSERT -> { + try { + showCodeChangeInEditor(row, insertLine, editor) + } catch (e: Exception) { + e.printStackTrace() + } + insertLine++ + addedLinesCount++ + addedCharsCount += row.newLine?.length?: 0 + } + } + } + isInProgress.set(false) + shouldShowActions.set(true) + metrics?.numSuggestionAddChars = addedCharsCount + metrics?.numSuggestionAddLines = addedLinesCount + metrics?.numSuggestionDelChars = deletedCharsCount + metrics?.numSuggestionDelLines = deletedLinesCount + } + } + + + private fun insertNewLineIfNeeded(row: Int, document: Document) : Int { + var newLineInserted = 0 + while (row > document.lineCount - 1) { + document.insertString(document.textLength, "\n") + newLineInserted++ + } + return newLineInserted + } + + private fun getLineStartOffset(document: Document, row: Int): Int { + return ReadAction.compute { + document.getLineStartOffset(row) + } + } + + private fun getLineEndOffset(document: Document, row: Int): Int { + return ReadAction.compute { + if (row == document.lineCount - 1) { + document.getLineEndOffset(row) + } else { + document.getLineEndOffset(row) + 1 + } + } + } + + private fun getSelectionStartLine(editor: Editor): Int { + return ReadAction.compute { + editor.document.getLineNumber(editor.selectionModel.selectionStart) + } + } + + private suspend fun runChangeAction(project: Project, action: () -> Unit, shouldRecordForUndo: Boolean = false) { + withContext(EDT) { + CommandProcessor.getInstance().executeCommand(project, { + ApplicationManager.getApplication().runWriteAction { + WriteCommandAction.runWriteCommandAction(project) { + action() + } + } + + }, "", null, UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, shouldRecordForUndo) + } + } + + private suspend fun insertString(document: Document, offset: Int, text: String) : RangeMarker { + var rangeMarker: RangeMarker? = null + val action = { + document.insertString(offset, text) + rangeMarker = document.createRangeMarker(offset, offset + text.length) + rangeMarker!!.isGreedyToLeft = true + rangeMarker!!.isGreedyToRight = true + highlightCodeWithBackgroundColor(editor, rangeMarker!!.startOffset, rangeMarker!!.endOffset, true) + } + runChangeAction(project, action) + return rangeMarker!! + } + + private suspend fun deleteString(document: Document, start: Int, end: Int) { + val action = { + document.deleteString(start, end) + } + runChangeAction(project, action) + } + + private suspend fun highlightString(document: Document, start: Int, end: Int, isInsert: Boolean) : RangeMarker { + var rangeMarker: RangeMarker? = null + val action = { + rangeMarker = document.createRangeMarker(start, end) +// rangeMarker!!.isGreedyToLeft = true +// rangeMarker!!.isGreedyToRight = true + highlightCodeWithBackgroundColor(editor, rangeMarker!!.startOffset, rangeMarker!!.endOffset, isInsert) + } + runChangeAction(project, action) + return rangeMarker!! + } + + private suspend fun showCodeChangeInEditor(diffRow: DiffRow, row: Int, editor: Editor) { + val document = editor.document + when (diffRow.tag) { + DiffRow.Tag.DELETE -> { +// val rowNum = getRowNumber(index) + val changeStartOffset = getLineStartOffset(document, row) + val changeEndOffset = getLineEndOffset(document, row) + val rangeMarker = highlightString(document, changeStartOffset, changeEndOffset, false) + partialUndoActions.add { + editor.markupModel.removeAllHighlighters() + } + partialAcceptActions.add { + if (rangeMarker.isValid) { + scope.launch(Dispatchers.EDT) { + deleteString(document, rangeMarker.startOffset, rangeMarker.endOffset) + } + } + editor.markupModel.removeAllHighlighters() +// decrementRowNumber() + } + } + + DiffRow.Tag.INSERT -> { +// val insertRow = getRowNumber(index) + val newLineInserted = insertNewLineIfNeeded(row, document) + val insertOffset = getLineStartOffset(document, row) + val textToInsert = unescape(diffRow.newLine) + "\n" + val rangeMarker = insertString(document, insertOffset, textToInsert) + partialUndoActions.add { + if (rangeMarker.isValid) { + scope.launch(Dispatchers.EDT) { + deleteString(document, rangeMarker.startOffset, rangeMarker.endOffset + newLineInserted) + } + } + editor.markupModel.removeAllHighlighters() +// decrementRowNumber() + } + partialAcceptActions.add { + editor.markupModel.removeAllHighlighters() + } +// incrementRowNumber() + } + + else -> { + val changeOffset = getLineStartOffset(document, row) + val changeEndOffset = getLineEndOffset(document, row) + val oldTextRangeMarker = highlightString(document, changeOffset, changeEndOffset, false) + partialAcceptActions.add { + scope.launch(Dispatchers.EDT) { + if (oldTextRangeMarker.isValid) { + deleteString(document, oldTextRangeMarker.startOffset, oldTextRangeMarker.endOffset) + } + } + editor.markupModel.removeAllHighlighters() + } + val insertOffset = getLineEndOffset(document, row) + val newLineInserted = insertNewLineIfNeeded(row, document) + val textToInsert = unescape(diffRow.newLine) + "\n" + val newTextRangeMarker = insertString(document, insertOffset, textToInsert) + partialUndoActions.add { + WriteCommandAction.runWriteCommandAction(project) { + if (newTextRangeMarker.isValid) { + scope.launch(Dispatchers.EDT) { + deleteString(document, newTextRangeMarker.startOffset, newTextRangeMarker.endOffset + newLineInserted) + } + } + } + editor.markupModel.removeAllHighlighters() +// decrementRowNumber() + } +// incrementRowNumber() + } + } + } + + + private suspend fun handleChat (message: String, selectedCode: String = "", editor: Editor, selectedLineStart: Int) : String { + val authController = AuthController() + val credentialState = authController.getAuthNeededStates(project).chat + if (credentialState != null) { + // handle auth + if (!JBCefApp.isSupported()) { + requestCredentialsForQ(project) + } else { + runInEdt { + QWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.Q)) + ToolWindowManager.getInstance(project).getToolWindow(AMAZON_Q_WINDOW_ID)?.activate(null, false) + ToolWindowManager.getInstance(project).getToolWindow(AMAZON_Q_WINDOW_ID)?.show() + } + } + return "Please sign in to Amazon Q" + } + val startTime = System.currentTimeMillis() + var firstResponseLatency = 0.0 + val messages = mutableListOf() + val triggerId = UUID.randomUUID().toString() + + val languageExtractor = LanguageExtractor() + val intentRecognizer = UserIntentRecognizer() + val language = editor.project?.let { languageExtractor.extractLanguageNameFromCurrentFile(editor, it) } ?: "" + + var baseRules = "- Plan out the changes step-by-step before making them, this should be brief and not include any code\n" + + "- Do not explain the code after, the plan and code are sufficient\n" + var prompt = "" + if (selectedCode.isNotBlank()) { + if (language.isNotEmpty()) { + baseRules += "- Ensure the code is written in $language\n" + } + prompt = "Rules for writing code:\n" + baseRules + + "Write a code snipped based on the following:\n" + message + } else { + baseRules += "- If the query is a question only attempt to add comments to the code that answer it\n" + + "- Make sure to preserve the original indentation, code formatting, tab size and structure as much as possible\n" + + "- Do not change the code more than required, try to maintain variables, function names, and other identifiers" + prompt = "```$language\n$selectedCode```\n" + + "Rules for rewriting the code:\n" + baseRules + + "Rewrite the above code to do the following:\n" + message + } + + logger.info { "Inline chat prompt: $prompt" } + + val contextExtractor = ActiveFileContextExtractor.create(fqnWebviewAdapter = null, project = project) + val fileContext = contextExtractor.extractContextForTrigger(ExtractionTriggerType.ChatMessage) +// val fileContextExtractor = FileContextExtractor(null, project) +// val fileContext = fileContextExtractor.extract() + + val requestData = ChatRequestData( + tabId = "inlineChat-editor", + message = prompt, + activeFileContext = fileContext, + userIntent = intentRecognizer.getUserIntentFromPromptChatMessage(message), + triggerType = TriggerType.Click, + customization = CodeWhispererModelConfigurator.getInstance().activeCustomization(project), + relevantTextDocuments = emptyList() + ) + + val sessionInfo = sessionStorage.getSession("inlineChat-editor", project) + + // Save the request in the history + sessionInfo.history.add(requestData) + var errorMessage = "" + var prevMessage = "" + val chat = sessionInfo.scope.async { + ChatPromptHandler(telemetryHelper).handle("inlineChat-editor", triggerId, requestData, sessionInfo, false, true) +// sessionInfo.session.chat(requestData) + .catch { e -> + logger.warn { "Error in inline chat request: ${e.message}" } + errorMessage = e.message ?: "" + } + .onEach { event: ChatMessage -> + if (event.message?.isNotEmpty() == true && prevMessage != event.message) { + processChatMessage(selectedCode, event, editor, selectedLineStart) + prevMessage = event.message + } + if (messages.isEmpty()) { + firstResponseLatency = (System.currentTimeMillis() - startTime).toDouble() + } + messages.add(event) + } + .toList() + } + chat.await() + val lastResponseLatency = (System.currentTimeMillis() - startTime).toDouble() + val requestId = messages.lastOrNull()?.messageId + requestId?.let{ + metrics = InlineChatMetrics(requestId = it, inputLength = message.length, numSelectedLines = selectedCode.split("\n").size, + codeIntent = true, responseStartLatency = firstResponseLatency, responseEndLatency = lastResponseLatency) + } + + return errorMessage + } + + companion object { + private val logger = getLogger() + } + + override fun dispose() { + currentPopup?.let { Disposer.dispose(it) } + hidePopup() + } +} + + diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopup.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopup.kt deleted file mode 100644 index 5804845ecac..00000000000 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopup.kt +++ /dev/null @@ -1,354 +0,0 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.cwc.inline - -import com.intellij.openapi.Disposable -import com.intellij.openapi.command.WriteCommandAction -import com.intellij.openapi.editor.Caret -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.colors.EditorColorsManager -import com.intellij.openapi.editor.markup.HighlighterLayer -import com.intellij.openapi.editor.markup.HighlighterTargetArea -import com.intellij.openapi.editor.markup.RangeHighlighter -import com.intellij.openapi.editor.markup.TextAttributes -import com.intellij.openapi.ui.popup.JBPopup -import com.intellij.openapi.ui.popup.JBPopupFactory -import com.intellij.openapi.ui.popup.JBPopupListener -import com.intellij.openapi.ui.popup.LightweightWindowEvent -import com.intellij.openapi.util.text.StringUtil -import com.intellij.ui.JBColor -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import migration.software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator -import software.aws.toolkits.jetbrains.core.coroutines.EDT -import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope -import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext -import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData -import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerType -import software.aws.toolkits.jetbrains.services.cwc.clients.chat.v1.ChatSessionFactoryV1 -import software.aws.toolkits.jetbrains.services.cwc.controller.chat.messenger.ChatPromptHandler -import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.TelemetryHelper -import software.aws.toolkits.jetbrains.services.cwc.controller.chat.userIntent.UserIntentRecognizer -import software.aws.toolkits.jetbrains.services.cwc.editor.context.ActiveFileContextExtractor -import software.aws.toolkits.jetbrains.services.cwc.editor.context.ExtractionTriggerType -import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessage -import software.aws.toolkits.jetbrains.services.cwc.storage.ChatSessionInfo -import software.aws.toolkits.jetbrains.services.cwc.storage.ChatSessionStorage -import java.awt.BorderLayout -import java.awt.Dimension -import java.awt.Font -import java.util.UUID -import javax.swing.BorderFactory -import javax.swing.JButton -import javax.swing.JLabel -import javax.swing.JPanel -import javax.swing.JTextField -import javax.swing.event.DocumentEvent -import javax.swing.event.DocumentListener - -class InlineChatPopup( - private val editor: Editor, - private val context: AmazonQAppInitContext -) : Disposable { - private var currentPopup: JBPopup? = null - private val scope = disposableCoroutineScope(this) - private var rangeHighlighter: RangeHighlighter? = null - private var undoAction: (() -> Unit?)? = null - - init { - createPopup() - } - - private fun createPopup() { - val popupListener = object : JBPopupListener { - override fun onClosed(event: LightweightWindowEvent) { - undoAction?.invoke() - } - } - val popupPanel = ChatInputPopupPanel().apply { - setTextChangeListener { - println("Text changed: $it") - } - - setSubmitClickListener { - submitButton.isEnabled = false - textField.isEnabled = false - val prompt = textField.text - setLabel("Waiting for Amazon Q...") - revalidate() - - scope.launch { - val selectedCode = editor.selectionModel.selectedText ?: "" - val messages = handleChat(prompt, selectedCode) - val src = messages.last().message ?: return@launch - val codeBlocks = getCodeBlocksRecursively(src) - if (codeBlocks.isNotEmpty()) { - val codeString = StringUtil.unescapeStringCharacters(extractContentAfterFirstNewline(codeBlocks.first())) - withContext(EDT) { - val caret: Caret = editor.caretModel.primaryCaret - val offset = caret.offset - val caretStart = caret.selectionStart - val caretEnd = caret.selectionEnd - - if(!isVisible){ - return@withContext - } - caret.removeSelection() - - WriteCommandAction.runWriteCommandAction(context.project) { - editor.document.insertString(offset, codeString) - } - undoAction = { - WriteCommandAction.runWriteCommandAction(context.project) { - editor.document.deleteString(offset, offset + codeString.length) - } - editor.markupModel.removeAllHighlighters() - } - highlightCodeWithBackgroundColor(editor, offset, offset + codeString.length, true) - highlightCodeWithBackgroundColor(editor, caretStart + codeString.length, caretEnd + codeString.length, false) - val acceptAction = { - undoAction = null - WriteCommandAction.runWriteCommandAction(context.project) { - editor.document.deleteString(caretStart + codeString.length, caretEnd + codeString.length) - } - editor.markupModel.removeAllHighlighters() - hidePopup() - } - val rejectAction = { - WriteCommandAction.runWriteCommandAction(context.project) { - editor.document.deleteString(offset, offset + codeString.length) - } - editor.markupModel.removeAllHighlighters() - hidePopup() - } - addCodeActionsPanel(acceptAction, rejectAction) - } - } else { - // TODO: handle throw error and show notification for this case - } - } - } - } - val popup = initPopup(popupPanel) - popup.addListener(popupListener) - val popupPoint = editor.visualPositionToXY(editor.caretModel.currentCaret.visualPosition) - popup.setLocation(popupPoint) - popup.showInBestPositionFor(editor) - popupPanel.textField.requestFocusInWindow() - currentPopup = popup - } - - private fun highlightCodeWithBackgroundColor(editor: Editor, startOffset: Int, endOffset: Int, isGreen: Boolean) { - - val greenBackgroundAttributes = TextAttributes().apply { - backgroundColor = JBColor(0x66BB6A, 0x006400) - effectColor = JBColor(0x66BB6A, 0x006400) - } - - val redBackgroundAttributes = TextAttributes().apply { - backgroundColor = JBColor(0xEF9A9A, 0x8B0000) - effectColor = JBColor(0xEF9A9A, 0x8B0000) - } - val attributes = if (isGreen) greenBackgroundAttributes else redBackgroundAttributes - rangeHighlighter= editor.markupModel.addRangeHighlighter( - startOffset, endOffset, HighlighterLayer.ADDITIONAL_SYNTAX, - attributes, HighlighterTargetArea.EXACT_RANGE - ) - } - - private fun extractContentAfterFirstNewline(input: String): String { - val newlineIndex = input.indexOf('\n') - return if (newlineIndex != -1) { - input.substring(newlineIndex + 1) - } else { - input - } - } - - - fun hidePopup() { - currentPopup?.dispose() - currentPopup = null - } - - private fun initPopup(panel: ChatInputPopupPanel): JBPopup { - val popup = JBPopupFactory.getInstance() - .createComponentPopupBuilder(panel, panel.textField) - .setMovable(true) - .setResizable(true) - .setTitle("Ask Amazon Q") - .setAlpha(0.2F) - .setCancelOnClickOutside(false) - .setCancelOnOtherWindowOpen(true) -// .setCancelKeyEnabled(true) - .setFocusable(true) - .setRequestFocus(true) - .setLocateWithinScreenBounds(true) - .setCancelOnWindowDeactivation(false) - .createPopup() - return popup - } - - class ChatInputPopupPanel : JPanel() { - val textField = JTextField().apply { - val editorColorsScheme = EditorColorsManager.getInstance().globalScheme - font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) - } - val submitButton = JButton("Send") - private val acceptButton = JButton("Accept").apply { - preferredSize = Dimension(80, 30) - } - private val rejectButton = JButton("Reject").apply { - preferredSize = Dimension(80, 30) - } - private var textChangeListener: ((String) -> Unit)? = null - private var submitClickListener: (() -> Unit)? = null - private val textLabel = JLabel("").apply { - val editorColorsScheme = EditorColorsManager.getInstance().globalScheme - font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) - } - private val actionsPanel = JPanel(BorderLayout()).apply { - border = BorderFactory.createEmptyBorder(5, 20, 5, 20) - add(acceptButton, BorderLayout.WEST) - add(rejectButton, BorderLayout.EAST) - } - private val inputPanel = JPanel(BorderLayout()) - private val labelPanel = JPanel(BorderLayout()).apply { - border = BorderFactory.createEmptyBorder(5, 20, 5, 20) - add(textLabel, BorderLayout.NORTH) - } - - override fun getPreferredSize(): Dimension { - return Dimension(600, 60) - } - - init { - layout = BorderLayout() - inputPanel.add(submitButton, BorderLayout.EAST) - inputPanel.add(textField, BorderLayout.WEST) - textField.preferredSize = Dimension(500, 30) - submitButton.preferredSize = Dimension(80, 30) - inputPanel.preferredSize = Dimension(600, 30) - add(inputPanel, BorderLayout.NORTH) - val listener = object : DocumentListener { - override fun insertUpdate(e: DocumentEvent) { - updateText() - } - - override fun removeUpdate(e: DocumentEvent) { - updateText() - } - - override fun changedUpdate(e: DocumentEvent) { - updateText() - } - - private fun updateText() { - val newText = textField.text - textChangeListener?.invoke(newText) - } - } - textField.document.addDocumentListener(listener) - - submitButton.addActionListener { - submitClickListener?.invoke() - } - } - - fun setTextChangeListener(listener: (String) -> Unit) { - textChangeListener = listener - } - - fun setSubmitClickListener(listener: () -> Unit) { - submitClickListener = listener - } - - fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit) { - textLabel.text = "Code diff generated. Do you want to accept it?" - textLabel.revalidate() - inputPanel.revalidate() - acceptButton.addActionListener { acceptAction.invoke() } - rejectButton.addActionListener { rejectAction.invoke() } - add(actionsPanel, BorderLayout.SOUTH) - revalidate() - } - - fun setLabel(text: String) { - textLabel.text = text - textLabel.revalidate() - remove(inputPanel) - add(labelPanel) - revalidate() - } - } - - private fun getCodeBlocksRecursively(src: String): List { - val codeBlocks = mutableListOf() - var currentIndex = 0 - - while (currentIndex < src.length) { - val startIndex = src.indexOf("```", currentIndex) - if (startIndex == -1) break - - val endIndex = src.indexOf("```", startIndex + 3) - if (endIndex == -1) break - - val code = src.substring(startIndex + 3, endIndex) - codeBlocks.add(code) - - currentIndex = endIndex + 3 - } - - return codeBlocks - } - - - private suspend fun handleChat (message: String, selectedCode: String = ""): List { - val messages = mutableListOf() - val triggerId = UUID.randomUUID().toString() - val chatSessionStorage = ChatSessionStorage(ChatSessionFactoryV1()) - val telemetryHelper = TelemetryHelper(context, chatSessionStorage) - val contextExtractor = ActiveFileContextExtractor.create(fqnWebviewAdapter = context.fqnWebviewAdapter, project = context.project) - val intentRecognizer = UserIntentRecognizer() - - val prompt = "You are an expert programmer assisting with code improvement. " + - "I will provide you with selected code from an IDE and a user query about how to improve it. " + - "Your task is to generate improved code based on the user's request. Rules - Output only the improved code, with no explanatory text or comments - " + - "Preserve the original code formatting, tab size and structure as much as possible - Enclose all code in Markdown fenced code blocks - " + - "Do not include any additional text or instructions." + - "Selected code: $selectedCode. User query: $message. Provide the improved code below:" - - - val requestData = ChatRequestData( - tabId = "inlineChat-editor", - message = prompt, - activeFileContext = contextExtractor.extractContextForTrigger(ExtractionTriggerType.ChatMessage), - userIntent = intentRecognizer.getUserIntentFromPromptChatMessage(message, true), - triggerType = TriggerType.Click, - customization = CodeWhispererModelConfigurator.getInstance().activeCustomization(context.project), - relevantTextDocuments = emptyList() - ) - val session = ChatSessionFactoryV1().create(context.project) - val sessionInfo = ChatSessionInfo(session = session, scope = scope, history = mutableListOf()) - val chat = sessionInfo.scope.async { ChatPromptHandler(telemetryHelper).handle("inlineChat-editor", triggerId, requestData, sessionInfo, false) - .catch { - // TODO: log error and show notification - e -> println("Error: $e") - } - .onEach { messages.add(it) } - .toList() - } - chat.await() - return messages - } - - override fun dispose() { - hidePopup() - } -} - diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt new file mode 100644 index 00000000000..dd2ab9b9090 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -0,0 +1,286 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cwc.inline + +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer +import com.intellij.icons.AllIcons +import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.ui.popup.IconButton +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.JBPopupListener +import com.intellij.openapi.ui.popup.LightweightWindowEvent +import com.intellij.openapi.util.TextRange +import com.intellij.ui.IdeBorderFactory +import com.intellij.ui.JBColor +import com.intellij.ui.components.JBLabel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.POPUP_INFO_TEXT_SIZE +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.TelemetryHelper +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.Font +import javax.swing.BorderFactory +import javax.swing.JButton +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.JTextField +import javax.swing.event.DocumentEvent +import javax.swing.event.DocumentListener + + +class InlineChatPopupFactory( + private val editor: Editor, + private val submitHandler: suspend (String, String, Int) -> String, + private val acceptHandler: () -> Unit, + private val rejectHandler: () -> Unit, + private val cancelHandler: () -> Unit, + private val telemetryHelper: TelemetryHelper, + private val scope: CoroutineScope +) { + + private fun getSelectedText(editor: Editor): String { + return ReadAction.compute { + val selectionStartOffset = editor.selectionModel.selectionStart + val selectionEndOffset = editor.selectionModel.selectionEnd + if (selectionEndOffset > selectionStartOffset) { + val selectionLineStart = editor.document.getLineStartOffset(editor.document.getLineNumber(selectionStartOffset)) + val selectionLineEnd = editor.document.getLineEndOffset(editor.document.getLineNumber(selectionEndOffset)) + editor.document.getText(TextRange(selectionLineStart, selectionLineEnd)) + } else "" + } + } + + private fun getSelectionStartLine(editor: Editor): Int { + return ReadAction.compute { + editor.document.getLineNumber(editor.selectionModel.selectionStart) + } + } + + fun createPopup(): JBPopup { + val popupPanel = ChatInputPopupPanel().apply { + border = IdeBorderFactory.createRoundedBorder(10).apply { + setColor(POPUP_BUTTON_BORDER) + } + + setSubmitClickListener { + submitButton.isEnabled = false + textField.isEnabled = false + val requestStartTime = System.currentTimeMillis() + val prompt = textField.text + setLabel("Waiting for Amazon Q...") + revalidate() + DaemonCodeAnalyzer.getInstance(editor.project).restart() + + scope.launch { + val selectedCode = getSelectedText(editor) + val selectedLineStart = getSelectionStartLine(editor) + val numOfLinesSelected = selectedCode.split("\n").size + var errorMessage = "" + runBlocking { + errorMessage = submitHandler(prompt, selectedCode, selectedLineStart) + } + val requestEndLatency = (System.currentTimeMillis() - requestStartTime).toDouble() +// withContext(EDT) { +// if (!isVisible) { +// cancelHandler.invoke() +// return@launch +// } + if (errorMessage.isNotEmpty()) { + setLabel(errorMessage) + revalidate() + } else { + + val acceptAction = { + acceptHandler.invoke() +// telemetryHelper.recordInlineChatTelemetry(prompt.length, numOfLinesSelected, true, +// InlineChatUserDecision.ACCEPT, 0.0, requestEndLatency) + } + val rejectAction = { + rejectHandler.invoke() +// telemetryHelper.recordInlineChatTelemetry(prompt.length, numOfLinesSelected, true, +// InlineChatUserDecision.REJECT, 0.0, requestEndLatency) + } + addCodeActionsPanel(acceptAction , rejectAction) +// DaemonCodeAnalyzer.getInstance(editor.project).restart() +// } + } + } + } + } + val popup = initPopup(popupPanel) +// popup.addListener(popupListener) +// addPopupListeners(popup) + val popupPoint = editor.visualPositionToXY(editor.caretModel.currentCaret.visualPosition) + popup.setLocation(popupPoint) + popup.showInBestPositionFor(editor) + popupPanel.textField.requestFocusInWindow() + return popup + } + + private fun initPopup(panel: ChatInputPopupPanel): JBPopup { + val titlePanel = JPanel(BorderLayout()).apply { + background = JBColor.background() + val titleLabel = JBLabel("Send to AmazonQ").apply { + font = font.deriveFont(font.style or java.awt.Font.BOLD) + } + add(titleLabel, BorderLayout.CENTER) + val editorColorsScheme = EditorColorsManager.getInstance().globalScheme + border = IdeBorderFactory.createRoundedBorder().apply { + setColor(POPUP_BUTTON_BORDER) + } + font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) + } + val cancelButton = IconButton("Cancel", AllIcons.Actions.Cancel).apply {} + val popup = JBPopupFactory.getInstance() + .createComponentPopupBuilder(panel, panel.textField) + .setMovable(true) + .setResizable(true) + .setTitle("Enter Instructions for Q") + .setCancelButton(cancelButton) + .setShowBorder(true) + .setCancelOnWindowDeactivation(false) + .setAlpha(0.2F) + .setCancelOnClickOutside(false) + .setCancelOnOtherWindowOpen(false) +// .setCancelKeyEnabled(true) + .setFocusable(true) + .setRequestFocus(true) + .setLocateWithinScreenBounds(true) + .createPopup() + return popup + } + + class ChatInputPopupPanel : JPanel() { + val textField = JTextField().apply { + val editorColorsScheme = EditorColorsManager.getInstance().globalScheme + border = IdeBorderFactory.createRoundedBorder().apply { + setColor(POPUP_BUTTON_BORDER) + } + font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) + + } + val submitButton = JButton("Send").apply { + border = IdeBorderFactory.createRoundedBorder().apply { + setColor(POPUP_BUTTON_BORDER) + } + } + private val acceptButton = JButton("Accept").apply { + preferredSize = Dimension(80, 30) +// isFocusable = false + border = IdeBorderFactory.createRoundedBorder().apply { + setColor(POPUP_BUTTON_BORDER) + } + } + private val rejectButton = JButton("Reject").apply { + preferredSize = Dimension(80, 30) +// isFocusable = false + border = IdeBorderFactory.createRoundedBorder().apply { + setColor(POPUP_BUTTON_BORDER) + } + } + private var textChangeListener: ((String) -> Unit)? = null + private var submitClickListener: (() -> Unit)? = null + private val textLabel = JLabel("").apply { + val editorColorsScheme = EditorColorsManager.getInstance().globalScheme +// font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) + font = font.deriveFont(POPUP_INFO_TEXT_SIZE) + } + private val actionsPanel = JPanel(BorderLayout()).apply { + border = BorderFactory.createEmptyBorder(0, 20, 5, 20) + add(acceptButton, BorderLayout.WEST) + add(rejectButton, BorderLayout.EAST) + } + private val inputPanel = JPanel(BorderLayout()).apply { + border = BorderFactory.createEmptyBorder(12, 10, 12, 10) + maximumSize = Dimension(580, 30) + } + private val labelPanel = JPanel(BorderLayout()).apply { + border = BorderFactory.createEmptyBorder(5, 20, 5, 20) + add(textLabel, BorderLayout.CENTER) + } + + override fun getPreferredSize(): Dimension { + return Dimension(600, 60) + } + + init { + layout = BorderLayout() + inputPanel.add(submitButton, BorderLayout.EAST) + inputPanel.add(textField, BorderLayout.WEST) + textField.preferredSize = Dimension(500, 30) + submitButton.preferredSize = Dimension(60, 30) + inputPanel.preferredSize = Dimension(600, 30) + add(inputPanel, BorderLayout.CENTER) + val listener = object : DocumentListener { + override fun insertUpdate(e: DocumentEvent) { + updateText() + } + + override fun removeUpdate(e: DocumentEvent) { + updateText() + } + + override fun changedUpdate(e: DocumentEvent) { + updateText() + } + + private fun updateText() { + val newText = textField.text + textChangeListener?.invoke(newText) + } + } + textField.document.addDocumentListener(listener) + + submitButton.addActionListener { + submitClickListener?.invoke() + } + } + + fun setTextChangeListener(listener: (String) -> Unit) { + textChangeListener = listener + } + + fun setSubmitClickListener(listener: () -> Unit) { + submitClickListener = listener + } + +// private fun addActionListener(id: String, action: EditorWriteActionHandler) : Disposable { +// val actionManager = EditorActionManager.getInstance() +// val originalHandler = actionManager.getActionHandler(IdeActions.ACTION_EDITOR_TAB) +// +// actionManager.setActionHandler(IdeActions.ACTION_EDITOR_TAB, action) +// val restorer = object : Disposable { +// override fun dispose() { +// actionManager.setActionHandler(IdeActions.ACTION_EDITOR_TAB, originalHandler) +// } +// } +// return restorer +// } + + fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit ) { + textLabel.text = "Code diff generated. Do you want to accept it?" + textLabel.revalidate() + inputPanel.revalidate() + acceptButton.addActionListener { acceptAction.invoke() } + rejectButton.addActionListener { rejectAction.invoke() } + add(actionsPanel, BorderLayout.SOUTH) + revalidate() + } + + fun setLabel(text: String) { + textLabel.text = text + textLabel.revalidate() + remove(inputPanel) + add(labelPanel) + revalidate() + } + } +} + diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt new file mode 100644 index 00000000000..1da3a12242c --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt @@ -0,0 +1,14 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cwc.inline + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import org.jetbrains.plugins.terminal.exp.TerminalDataContextUtils.editor + +class OpenChatInputAction : AnAction() { + override fun actionPerformed(e: AnActionEvent) { + e.editor?.project?.let { InlineChatController(e.editor!!, it).initPopup() } + } +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatAcceptAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatAcceptAction.kt new file mode 100644 index 00000000000..97048abf2b6 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatAcceptAction.kt @@ -0,0 +1,49 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cwc.inline.actions + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.actionSystem.IdeActions +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorAction +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.intellij.openapi.editor.actionSystem.EditorActionManager +import com.intellij.openapi.project.Project +import org.jetbrains.plugins.terminal.exp.TerminalDataContextUtils.editor +import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatController + +class InlineChatAcceptAction : EditorAction(InlineChatAcceptHandler()) { + class InlineChatAcceptHandler : EditorActionHandler() { + private val originalTabAction = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_TAB) + + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext) { + val project = editor.project + if (project != null) { + val controller = InlineChatController(editor, project) + controller.diffAcceptHandler.invoke() + } else { + originalTabAction.execute(editor, caret, dataContext) + } + } + + override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext): Boolean { + val project = editor.project + return if (project != null) { + true + } else { + originalTabAction.isEnabled(editor, caret, dataContext) + } + } + + private fun shouldHandleCustomAction(editor: Editor, project: Project): Boolean { + return true +// InlineChatController(editor, project).getShouldShowActions() + } + } +} + diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatRejectAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatRejectAction.kt new file mode 100644 index 00000000000..28d97a251bb --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatRejectAction.kt @@ -0,0 +1,32 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cwc.inline.actions + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import org.jetbrains.plugins.terminal.exp.TerminalDataContextUtils.editor +import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatController + +class InlineChatRejectAction : AnAction() { + override fun update(e: AnActionEvent) { + val editor = e.getData(CommonDataKeys.EDITOR) + val project = e.project + e.presentation.isEnabled = if (editor != null && project != null) { + // Check if inline chat is active + InlineChatController(editor, project).getShouldShowActions() + } else false + } + + override fun actionPerformed(e: AnActionEvent) { + e.editor?.project?.let { + val controller = InlineChatController(e.editor!!, it) + if(controller.getShouldShowActions()) { + controller.diffRejectHandler.invoke() + } + } + } +} + + diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt index 6ba3e038d5d..921a0033780 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt @@ -26,6 +26,7 @@ import software.amazon.awssdk.services.codewhispererruntime.model.Dimension import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse import software.amazon.awssdk.services.codewhispererruntime.model.IdeCategory +import software.amazon.awssdk.services.codewhispererruntime.model.InlineChatUserDecision import software.amazon.awssdk.services.codewhispererruntime.model.ListAvailableCustomizationsRequest import software.amazon.awssdk.services.codewhispererruntime.model.ListFeatureEvaluationsResponse import software.amazon.awssdk.services.codewhispererruntime.model.OperatingSystem @@ -176,6 +177,22 @@ interface CodeWhispererClientAdaptor : Disposable { customization: CodeWhispererCustomization?, ): SendTelemetryEventResponse + fun sendInlineChatTelemetry( + requestId: String, + inputLength: Int?, + numSelectedLines: Int?, + codeIntent: Boolean?, + userDecision: InlineChatUserDecision?, + responseStartLatency: Double?, + responseEndLatency: Double?, + numSuggestionAddChars: Int?, + numSuggestionAddLines: Int?, + numSuggestionDelChars: Int?, + numSuggestionDelLines: Int?, + charactersAdded: Int?, + charactersRemoved: Int?, + ): SendTelemetryEventResponse + companion object { fun getInstance(project: Project): CodeWhispererClientAdaptor = project.service() @@ -548,6 +565,43 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW requestBuilder.userContext(codeWhispererUserContext) } + override fun sendInlineChatTelemetry( + requestId: String, + inputLength: Int?, + numSelectedLines: Int?, + codeIntent: Boolean?, + userDecision: InlineChatUserDecision?, + responseStartLatency: Double?, + responseEndLatency: Double?, + numSuggestionAddChars: Int?, + numSuggestionAddLines: Int?, + numSuggestionDelChars: Int?, + numSuggestionDelLines: Int?, + charactersAdded: Int?, + charactersRemoved: Int? + ): SendTelemetryEventResponse = bearerClient().sendTelemetryEvent { requestBuilder -> + requestBuilder.telemetryEvent { telemetryEventBuilder -> + telemetryEventBuilder.inlineChatEvent { + it.requestId(requestId) + it.inputLength(inputLength) + it.numSelectedLines(numSelectedLines) + it.codeIntent(codeIntent) + it.userDecision(userDecision) + it.responseStartLatency(responseStartLatency) + it.responseEndLatency(responseEndLatency) + it.numSuggestionAddChars(numSuggestionAddChars) + it.numSuggestionAddLines(numSuggestionAddLines) + it.numSuggestionDelChars(numSuggestionDelChars) + it.numSuggestionDelLines(numSuggestionDelLines) + it.charactersRemoved(charactersRemoved) + it.charactersAdded(charactersAdded) + it.timestamp(Instant.now()) + } + } + requestBuilder.optOutPreference(getTelemetryOptOutPreference()) + requestBuilder.userContext(codeWhispererUserContext) + } + override fun dispose() { if (this::mySigv4Client.isLazyInitialized) { mySigv4Client.close() diff --git a/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json b/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json index 9f99ce52d8a..3021d3376e2 100644 --- a/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json +++ b/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json @@ -1089,6 +1089,30 @@ "max":10, "min":0 }, + "InlineChatEvent": { + "type": "structure", + "required": ["requestId", "timestamp"], + "members": { + "requestId": { "shape": "UUID" }, + "timestamp":{"shape":"Timestamp"}, + "inputLength": { "shape": "PrimitiveInteger" }, + "numSelectedLines": { "shape": "PrimitiveInteger" }, + "numSuggestionAddChars": { "shape": "PrimitiveInteger" }, + "numSuggestionAddLines": { "shape": "PrimitiveInteger" }, + "numSuggestionDelChars": { "shape": "PrimitiveInteger" }, + "numSuggestionDelLines": { "shape": "PrimitiveInteger" }, + "codeIntent": { "shape": "Boolean" }, + "userDecision": { "shape": "InlineChatUserDecision" }, + "responseStartLatency": { "shape": "Double" }, + "responseEndLatency": { "shape": "Double" }, + "charactersRemoved": { "shape": "PrimitiveInteger" }, + "charactersAdded": { "shape": "PrimitiveInteger"} + } + }, + "InlineChatUserDecision": { + "type": "string", + "enum": ["ACCEPT", "REJECT", "DISMISS"] + }, "Integer":{ "type":"integer", "box":true @@ -1611,7 +1635,8 @@ "chatAddMessageEvent": { "shape": "ChatAddMessageEvent" }, "chatInteractWithMessageEvent": { "shape": "ChatInteractWithMessageEvent" }, "chatUserModificationEvent": { "shape": "ChatUserModificationEvent" }, - "featureDevEvent": { "shape": "FeatureDevEvent" } + "featureDevEvent": { "shape": "FeatureDevEvent" }, + "inlineChatEvent": { "shape": "InlineChatEvent" } }, "union":true }, From f60db9bd229903a8aa20875b375b960f66881320 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Fri, 11 Oct 2024 09:35:19 -0700 Subject: [PATCH 05/39] bugfix and hint popup ux --- .../context/file/util/LanguageExtractor.kt | 8 ++ .../cwc/inline/InlineChatEditorHint.kt | 74 +++++++++++++++ .../cwc/inline/InlineChatFileListener.kt | 49 +++++++++- .../cwc/inline/InlineChatPopupFactory.kt | 94 +++++++++---------- .../cwc/inline/InlineChatSelectionListener.kt | 41 ++++++++ .../resources/icons/logos/Amazon_Q_grey.svg | 4 + .../icons/misc/Q_inlineChat_shortcut.svg | 4 + .../jetbrains-community/src/icons/AwsIcons.kt | 4 + 8 files changed, 225 insertions(+), 53 deletions(-) create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatSelectionListener.kt create mode 100644 plugins/core/jetbrains-community/resources/icons/logos/Amazon_Q_grey.svg create mode 100644 plugins/core/jetbrains-community/resources/icons/misc/Q_inlineChat_shortcut.svg diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt index a63cc67d429..14227aea8e4 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt @@ -9,6 +9,7 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile +import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage class LanguageExtractor { fun extractLanguageNameFromCurrentFile(editor: Editor, project: Project): String? = @@ -17,4 +18,11 @@ class LanguageExtractor { val psiFile: PsiFile? = PsiDocumentManager.getInstance(project).getPsiFile(doc) psiFile?.fileType?.name?.lowercase() } + + fun extractProgrammingLanguageNameFromCurrentFile(editor: Editor, project: Project): String? = + runReadAction { + val doc: Document = editor.document + val psiFile: PsiFile? = PsiDocumentManager.getInstance(project).getPsiFile(doc) + psiFile?.programmingLanguage()?.toTelemetryType()?.toString() + } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt new file mode 100644 index 00000000000..6bf0bb2c493 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt @@ -0,0 +1,74 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cwc.inline +import com.intellij.codeInsight.hint.HintManager +import com.intellij.codeInsight.hint.HintManagerImpl +import com.intellij.codeInsight.hint.HintUtil +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.ui.LightweightHint +import com.intellij.ui.SimpleColoredText +import com.intellij.ui.SimpleTextAttributes +import icons.AwsIcons +import java.awt.BorderLayout +import java.awt.Point +import javax.swing.JPanel + + +class InlineChatEditorHint(private val project: Project, private val editor: Editor) { + private var hint: LightweightHint? = null + + fun show(location: Point) { + if (hint != null) return + + val icon = AwsIcons.Logos.AWS_Q_GREY + + val component = HintUtil.createInformationComponent() + component.isIconOnTheRight = false; + component.icon = icon + val coloredText = + SimpleColoredText(hintText(), SimpleTextAttributes.REGULAR_ATTRIBUTES) + + val shortCutIcon = AwsIcons.Misc.AWS_Q_INLINECHAT_SHORTCUT + val shortcutComponent = HintUtil.createInformationComponent() + shortcutComponent.isIconOnTheRight = true; + shortcutComponent.icon = shortCutIcon + + coloredText.appendToComponent(shortcutComponent) + + val panel = JPanel(BorderLayout()).apply { + add(component, BorderLayout.WEST) + add(shortcutComponent, BorderLayout.EAST) + isOpaque = true + background = component.background + revalidate() + repaint() + } + + hint = LightweightHint(panel) + + HintManagerImpl.getInstanceImpl().showEditorHint( + hint!!, editor, location, + HintManager.HIDE_BY_TEXT_CHANGE, + 0, false, + HintManagerImpl.createHintHint(editor, location, hint!!, HintManager.RIGHT_UNDER).setContentActive(false) + ) + + + } + + fun hide() { + hint?.hide() + hint = null + } + + + private fun hintText(): String { + return "Edit" + } +} + + + + diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt index cd003271c96..8d1836e738c 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt @@ -3,20 +3,61 @@ package software.aws.toolkits.jetbrains.services.cwc.inline +import com.intellij.openapi.editor.Editor import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManagerEvent import com.intellij.openapi.fileEditor.FileEditorManagerListener +import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.vfs.VirtualFile import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererSettings import software.aws.toolkits.jetbrains.services.cwc.editor.context.project.ProjectContextController class InlineChatFileListener(private val context: AmazonQAppInitContext) : FileEditorManagerListener { -// override fun fileOpened(source: FileEditorManager, file: VirtualFile) { -// ChatCaretListener(source.project, context) -// } + private var currentEditor: Editor? = context.project.let { FileEditorManager.getInstance(it).selectedTextEditor } + private var caretListener: ChatCaretListener? = null + private var selectionListener: InlineChatSelectionListener? = null + + init { + setupListenersForCurrentEditor() + } override fun selectionChanged(event: FileEditorManagerEvent) { - ChatCaretListener(context.project, context) + val newEditor = (event.newEditor as? TextEditor)?.editor + if (newEditor != currentEditor) { + removeListenersFromCurrentEditor() + currentEditor = newEditor + setupListenersForCurrentEditor() + } + } + + private fun setupListenersForCurrentEditor() { + currentEditor?.let { editor -> + caretListener = ChatCaretListener(context.project, context).also { listener -> + editor.caretModel.addCaretListener(listener) + } + + selectionListener = InlineChatSelectionListener().also { listener -> + editor.selectionModel.addSelectionListener(listener) + } + } + } + + private fun removeListenersFromCurrentEditor() { + currentEditor?.let { editor -> + caretListener?.let { listener -> + editor.caretModel.removeCaretListener(listener) + } + selectionListener?.let { listener -> + editor.selectionModel.removeSelectionListener(listener) + } + } + caretListener = null + selectionListener = null + } + + fun dispose() { + removeListenersFromCurrentEditor() + currentEditor = null } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index dd2ab9b9090..901936b7b95 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -3,7 +3,6 @@ package software.aws.toolkits.jetbrains.services.cwc.inline -import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer import com.intellij.icons.AllIcons import com.intellij.openapi.application.ReadAction import com.intellij.openapi.editor.Editor @@ -69,81 +68,79 @@ class InlineChatPopupFactory( setColor(POPUP_BUTTON_BORDER) } - setSubmitClickListener { + val submitListener: () -> Unit = { submitButton.isEnabled = false textField.isEnabled = false - val requestStartTime = System.currentTimeMillis() val prompt = textField.text - setLabel("Waiting for Amazon Q...") - revalidate() - DaemonCodeAnalyzer.getInstance(editor.project).restart() + if (prompt.isNotBlank()) { + setLabel("AmazonQ generating...") + revalidate() - scope.launch { - val selectedCode = getSelectedText(editor) - val selectedLineStart = getSelectionStartLine(editor) - val numOfLinesSelected = selectedCode.split("\n").size - var errorMessage = "" - runBlocking { - errorMessage = submitHandler(prompt, selectedCode, selectedLineStart) - } - val requestEndLatency = (System.currentTimeMillis() - requestStartTime).toDouble() -// withContext(EDT) { -// if (!isVisible) { -// cancelHandler.invoke() -// return@launch -// } + scope.launch { + val selectedCode = getSelectedText(editor) + val selectedLineStart = getSelectionStartLine(editor) + var errorMessage = "" + runBlocking { + errorMessage = submitHandler(prompt, selectedCode, selectedLineStart) + } if (errorMessage.isNotEmpty()) { setLabel(errorMessage) revalidate() } else { - - val acceptAction = { - acceptHandler.invoke() -// telemetryHelper.recordInlineChatTelemetry(prompt.length, numOfLinesSelected, true, -// InlineChatUserDecision.ACCEPT, 0.0, requestEndLatency) + val acceptAction = { + acceptHandler.invoke() + } + val rejectAction = { + rejectHandler.invoke() + } + addCodeActionsPanel(acceptAction , rejectAction) } - val rejectAction = { - rejectHandler.invoke() -// telemetryHelper.recordInlineChatTelemetry(prompt.length, numOfLinesSelected, true, -// InlineChatUserDecision.REJECT, 0.0, requestEndLatency) - } - addCodeActionsPanel(acceptAction , rejectAction) -// DaemonCodeAnalyzer.getInstance(editor.project).restart() -// } } + } else { + // TODO: show some message here } } + setSubmitClickListener(submitListener) } val popup = initPopup(popupPanel) -// popup.addListener(popupListener) -// addPopupListeners(popup) val popupPoint = editor.visualPositionToXY(editor.caretModel.currentCaret.visualPosition) popup.setLocation(popupPoint) popup.showInBestPositionFor(editor) popupPanel.textField.requestFocusInWindow() + popupPanel.textField.addActionListener { e -> + val inputText = popupPanel.textField.text.trim() + if (inputText.isNotEmpty()) { + popupPanel.submitButton.doClick() + } + } return popup } private fun initPopup(panel: ChatInputPopupPanel): JBPopup { - val titlePanel = JPanel(BorderLayout()).apply { - background = JBColor.background() - val titleLabel = JBLabel("Send to AmazonQ").apply { - font = font.deriveFont(font.style or java.awt.Font.BOLD) - } - add(titleLabel, BorderLayout.CENTER) - val editorColorsScheme = EditorColorsManager.getInstance().globalScheme - border = IdeBorderFactory.createRoundedBorder().apply { - setColor(POPUP_BUTTON_BORDER) - } - font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) - } - val cancelButton = IconButton("Cancel", AllIcons.Actions.Cancel).apply {} +// val titlePanel = JPanel(BorderLayout()).apply { +// background = JBColor.background() +// val titleLabel = JBLabel("Send to AmazonQ").apply { +// font = font.deriveFont(font.style or java.awt.Font.BOLD) +// } +// add(titleLabel, BorderLayout.CENTER) +// val editorColorsScheme = EditorColorsManager.getInstance().globalScheme +// border = IdeBorderFactory.createRoundedBorder().apply { +// setColor(POPUP_BUTTON_BORDER) +// } +// font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) +// } + val cancelButton = IconButton("Cancel", AllIcons.Actions.Cancel) +// cancelButton. val popup = JBPopupFactory.getInstance() .createComponentPopupBuilder(panel, panel.textField) .setMovable(true) .setResizable(true) .setTitle("Enter Instructions for Q") .setCancelButton(cancelButton) + .setCancelCallback { + cancelHandler.invoke() + true + } .setShowBorder(true) .setCancelOnWindowDeactivation(false) .setAlpha(0.2F) @@ -164,7 +161,6 @@ class InlineChatPopupFactory( setColor(POPUP_BUTTON_BORDER) } font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) - } val submitButton = JButton("Send").apply { border = IdeBorderFactory.createRoundedBorder().apply { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatSelectionListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatSelectionListener.kt new file mode 100644 index 00000000000..b5a529c3cc2 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatSelectionListener.kt @@ -0,0 +1,41 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cwc.inline + +import com.intellij.openapi.editor.event.SelectionEvent +import com.intellij.openapi.editor.event.SelectionListener +import java.awt.Point +import javax.swing.SwingUtilities + +class InlineChatSelectionListener : SelectionListener { + private var inlineChatEditorHint: InlineChatEditorHint? = null + override fun selectionChanged(e: SelectionEvent) { + val editor = e.editor + val selectionModel = editor.selectionModel + + if (selectionModel.hasSelection()) { + val selectionEnd = selectionModel.selectionEnd + val selectionLineEnd = editor.document.getLineEndOffset(editor.document.getLineNumber(selectionEnd)) + val logicalPosition = editor.offsetToLogicalPosition(selectionLineEnd) + val visualPosition = editor.logicalToVisualPosition(logicalPosition) + val position = editor.visualPositionToXY(visualPosition) + + val visibleArea = editor.scrollingModel.visibleArea + + val adjustedX = (position.x + 200).coerceAtMost(visibleArea.x + visibleArea.width - 50) + val adjustedY = (position.y + 20).coerceAtMost(visibleArea.y + visibleArea.height - 50) + val adjustedPosition = Point(adjustedX, adjustedY) + val pos = SwingUtilities.convertPoint( + editor.component, + adjustedPosition, + editor.component.rootPane.layeredPane + ) + + inlineChatEditorHint = editor.let { editor.project?.let { project -> InlineChatEditorHint(project, it) } } + inlineChatEditorHint?.show(pos) + } else { + inlineChatEditorHint?.hide() + } + } +} diff --git a/plugins/core/jetbrains-community/resources/icons/logos/Amazon_Q_grey.svg b/plugins/core/jetbrains-community/resources/icons/logos/Amazon_Q_grey.svg new file mode 100644 index 00000000000..fdbfcc26d71 --- /dev/null +++ b/plugins/core/jetbrains-community/resources/icons/logos/Amazon_Q_grey.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/core/jetbrains-community/resources/icons/misc/Q_inlineChat_shortcut.svg b/plugins/core/jetbrains-community/resources/icons/misc/Q_inlineChat_shortcut.svg new file mode 100644 index 00000000000..c2cbaab7289 --- /dev/null +++ b/plugins/core/jetbrains-community/resources/icons/misc/Q_inlineChat_shortcut.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/core/jetbrains-community/src/icons/AwsIcons.kt b/plugins/core/jetbrains-community/src/icons/AwsIcons.kt index 4e0c3202e65..b3edb03fe63 100644 --- a/plugins/core/jetbrains-community/src/icons/AwsIcons.kt +++ b/plugins/core/jetbrains-community/src/icons/AwsIcons.kt @@ -32,6 +32,8 @@ object AwsIcons { @JvmField val AWS_Q = load("/icons/logos/AWS_Q.svg") // 13x13 + @JvmField val AWS_Q_GREY = load("/icons/logos/Amazon_Q_grey.svg") + @JvmField val AWS_Q_GRADIENT = load("/icons/logos/Amazon-Q-Icon_Gradient_Large.svg") // 54x54 @JvmField val AWS_Q_GRADIENT_SMALL = load("/icons/logos/Amazon-Q-Icon_Gradient_Medium.svg") // 54x54 @@ -57,6 +59,8 @@ object AwsIcons { @JvmField val CSHARP = load("/icons/misc/csharp.svg") // 16x16 @JvmField val NEW = load("/icons/misc/new.svg") // 16x16 + + @JvmField val AWS_Q_INLINECHAT_SHORTCUT = load("/icons/misc/Q_inlineChat_shortcut.svg") } object Resources { From 3a80178607e6a2463d83ddde8234963505c805a9 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Fri, 11 Oct 2024 14:25:22 -0700 Subject: [PATCH 06/39] more bug fixes --- .../resources/META-INF/plugin-chat.xml | 7 +- .../services/cwc/inline/ChatCaretListener.kt | 20 +++- .../inline/InlineChatCodeVisionProvider.kt | 91 +------------------ .../cwc/inline/InlineChatController.kt | 87 +++++++++--------- .../cwc/inline/InlineChatFileListener.kt | 6 +- .../cwc/inline/OpenChatInputAction.kt | 29 +++++- 6 files changed, 98 insertions(+), 142 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml index cc8f7014ee1..1085e2f5cd7 100644 --- a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml +++ b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml @@ -20,7 +20,12 @@ factoryClass="software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory" icon="AwsIcons.Logos.AWS_Q" /> - + + + + + + diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt index 36287c5d322..d5a1e37891c 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt @@ -3,9 +3,11 @@ package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.icons.AllIcons +import com.intellij.ide.DataManager import com.intellij.openapi.actionSystem.ActionGroup import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.Presentation import com.intellij.openapi.editor.event.CaretEvent import com.intellij.openapi.editor.event.CaretListener import com.intellij.openapi.editor.markup.GutterIconRenderer @@ -15,28 +17,36 @@ import com.intellij.openapi.editor.markup.MarkupModel import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project +import icons.AwsIcons import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext +import java.awt.Component +import java.awt.event.MouseEvent import javax.swing.Icon +import javax.swing.SwingUtilities class ChatCaretListener(private val project: Project, private val context: AmazonQAppInitContext) : CaretListener { private var currentHighlighter: RangeHighlighter? = null -// private var currentPopup: InlineChatPopup? = null init { val editor = FileEditorManager.getInstance(project).selectedTextEditor editor?.caretModel?.addCaretListener(this) } override fun caretPositionChanged(event: CaretEvent) { -// InlineChatCodeVisionProvider() val editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return val lineNumber = event.newPosition.line val startOffset = editor.document.getLineStartOffset(lineNumber) val endOffset = editor.document.getLineEndOffset(lineNumber) val markupModel: MarkupModel = editor.markupModel - val gutterIconRenderer = ChatGutterIconRenderer(AllIcons.Actions.Lightning).apply { + val gutterIconRenderer = ChatGutterIconRenderer(AwsIcons.Logos.AWS_Q_GREY).apply { setClickAction { -// currentPopup?.hidePopup() - InlineChatController(editor, context.project).initPopup() + val action = OpenChatInputAction() + val dataContext = DataManager.getInstance().getDataContext(editor.component) + val e = AnActionEvent.createFromDataContext( + "GutterIconClick", + Presentation(), + dataContext + ) + action.actionPerformed(e) } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionProvider.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionProvider.kt index ba0020bf29b..8d7038212a6 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionProvider.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionProvider.kt @@ -5,36 +5,25 @@ package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.codeInsight.codeVision.CodeVisionAnchorKind import com.intellij.codeInsight.codeVision.CodeVisionEntry -import com.intellij.codeInsight.codeVision.CodeVisionHost import com.intellij.codeInsight.codeVision.CodeVisionProvider -import com.intellij.codeInsight.codeVision.CodeVisionProviderFactory 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.ClickableTextCodeVisionEntry -import com.intellij.codeInsight.codeVision.ui.model.TextCodeVisionEntry -import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer import com.intellij.codeInsight.hints.InlayHintsUtils -import com.intellij.history.integration.ui.views.RevisionsList -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.runReadAction import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.event.SelectionEvent -import com.intellij.openapi.editor.event.SelectionListener -import com.intellij.openapi.editor.toolbar.floating.EditorFloatingToolbar import com.intellij.openapi.editor.toolbar.floating.FloatingToolbarProvider import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiDocumentManager import com.intellij.psi.SyntaxTraverser -import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.elementType -import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext import java.awt.event.MouseEvent -import java.util.UUID -import kotlin.math.max import kotlin.math.min + +// disabled, register if need to bring back the code visions class InlineChatCodeVisionProvider: CodeVisionProvider { companion object { internal const val id: String = "amazonq.chat.code.vision" @@ -43,32 +32,8 @@ class InlineChatCodeVisionProvider: CodeVisionProvider { override val defaultAnchor: CodeVisionAnchorKind = CodeVisionAnchorKind.Top override val name: String = "AmazonQ Chat Code Vision" override val relativeOrderings: List = emptyList() -// private var selectionListener: SelectionListener? = null -// private var lenses = ArrayList>() - - -// override fun precomputeOnUiThread(editor: Editor) { -// } override fun precomputeOnUiThread(editor: Editor) { - // Remove any existing listener -// selectionListener?.let { editor.selectionModel.removeSelectionListener(it) } -// -// // Create a new listener -// selectionListener = object : SelectionListener { -// override fun selectionChanged(e: SelectionEvent) { -// // Trigger a refresh of code visions -// editor.project?.let { project -> -// DaemonCodeAnalyzer.getInstance(project).restart() -// } -// } -// } -// -// // Add the new listener -// editor.selectionModel.addSelectionListener(selectionListener!!) -// -//// // Generate initial code visions -//// computeCodeVision(editor, Unit) } override fun shouldRecomputeForEditor(editor: Editor, uiData: Unit?): Boolean = true @@ -88,7 +53,6 @@ class InlineChatCodeVisionProvider: CodeVisionProvider { val elements = traverser.preOrderDfsTraversal().filter { element -> val elementType = element.elementType.toString().toLowerCase() elementType.contains("function") || -// elementType.contains("method") || elementType.contains("class") } val clickHandler : @@ -105,56 +69,5 @@ class InlineChatCodeVisionProvider: CodeVisionProvider { } return@runReadAction CodeVisionState.Ready(lenses) } -// return runReadAction { -// val lenses = ArrayList>() -// val project = editor.project ?: return@runReadAction READY_EMPTY -// val controller = InlineChatController(editor, editor.project!!) -// val document = editor.document -// val file = PsiDocumentManager.getInstance(project).getPsiFile(document) ?: return@runReadAction READY_EMPTY -// -// val selectionModel = editor.selectionModel -// val selectionStart = selectionModel.selectionStart -// val selectionEnd = selectionModel.selectionEnd -// -// // If there's no selection, return empty result -// var ranges: List = emptyList() -// if (selectionStart == selectionEnd) { -// val offset = editor.caretModel.offset -// val nearestFunction = PsiTreeUtil.findFirstParent(file.findElementAt(offset)) { element -> -// element.node.elementType.toString().toLowerCase().contains("function") || -// element.node.elementType.toString().toLowerCase().contains("method") -// } -// -// if (nearestFunction != null) { -// val textRange = nearestFunction.textRange -// ranges = listOf(textRange) -// } -// } else { -// val traverser = SyntaxTraverser.psiTraverser(file) -// ranges = traverser.preOrderDfsTraversal() -// .filter { element -> -// val elementType = element.elementType.toString().toLowerCase() -// element.textRange.intersects(selectionStart, selectionEnd) && -// elementType.contains("function") || -// elementType.contains("method") || -// elementType.contains("class") -// }.map { element -> element.textRange }.toMutableList() -// } -// -// for (range in ranges) { -// val adjustedRange = TextRange( -// max(range.startOffset, selectionStart), -// min(range.endOffset, selectionEnd) -// ) -// val clickHandler: (MouseEvent?, Editor) -> Unit = { e: MouseEvent?, _ -> -// controller.initPopup() -// } -// val text = if(controller.getIsInProgress() || controller.getShouldShowActions()) "Chat is working..." else "AmazonQ Chat" -// val entry = ClickableTextCodeVisionEntry(text, id, clickHandler, icon = null, text, text, emptyList()) -// lenses.add(Pair(adjustedRange, entry)) -// } -// -// return@runReadAction CodeVisionState.Ready(lenses) -// } } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 039327ae818..df78492b354 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -14,6 +14,8 @@ import com.intellij.openapi.application.runInEdt import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.command.UndoConfirmationPolicy import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.command.undo.BasicUndoableAction +import com.intellij.openapi.command.undo.UndoManager import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.RangeMarker @@ -27,6 +29,7 @@ import com.intellij.openapi.ui.popup.JBPopupListener import com.intellij.openapi.ui.popup.LightweightWindowEvent import com.intellij.openapi.util.Disposer import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.psi.codeStyle.CodeStyleManager import com.intellij.ui.JBColor import com.intellij.ui.jcef.JBCefApp import com.jetbrains.rd.util.AtomicInteger @@ -34,6 +37,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch @@ -53,6 +57,7 @@ import software.aws.toolkits.jetbrains.core.webview.BrowserState import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AMAZON_Q_WINDOW_ID +import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererLanguageManager import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerType import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController @@ -115,7 +120,7 @@ class InlineChatController( } } - private val popupCancelHandler: () -> Unit = { + val popupCancelHandler: () -> Unit = { if (isPopupAborted.get() && currentPopup != null) { scope.launch(Dispatchers.EDT) { while (partialUndoActions.isNotEmpty()) { @@ -157,9 +162,10 @@ class InlineChatController( metrics = null } - val diffAcceptHandler: () -> Unit = { scope.launch(Dispatchers.EDT) { + val undoManager = UndoManager.getGlobalInstance() +// undoManager.undoableActionPerformed() partialUndoActions.clear() while (partialAcceptActions.isNotEmpty()) { val action = partialAcceptActions.pop() @@ -242,7 +248,7 @@ class InlineChatController( } - private fun hidePopup() { + fun hidePopup() { isPopupAborted.set(false) currentPopup?.closeOk(null) currentPopup = null @@ -250,6 +256,11 @@ class InlineChatController( shouldShowActions.set(false) } + fun disposePopup() { + currentPopup?.let { Disposer.dispose(it) } + hidePopup() + } + private fun getCodeBlocks(src: String): List { val codeBlocks = mutableListOf() var currentIndex = 0 @@ -276,18 +287,6 @@ class InlineChatController( return rows } -// private fun incrementRowNumber() { -// selectionStartLine.incrementAndGet() -// } -// -// private fun decrementRowNumber() { -// selectionStartLine.decrementAndGet() -// } -// -// private fun getRowNumber(index: Int): Int { -// return index + selectionStartLine.get() -// } - fun getShouldShowActions(): Boolean { return shouldShowActions.get() } @@ -303,14 +302,10 @@ class InlineChatController( .replace("=>", "=>") } - private suspend fun processChatMessage(selectedCode: String, event: ChatMessage, editor: Editor, selectedLineStart: Int) { + private suspend fun processChatMessage(selectedCode: String, event: ChatMessage, editor: Editor, selectedLineStart: Int, prevMessage: String) { if(event.message?.isNotEmpty() == true) { val codeBlocks = getCodeBlocks(event.message) if(codeBlocks.isEmpty()) { - if (event.messageType == ChatMessageType.Answer) { - isInProgress.set(false) -// logger.warn { "No code block found in inline chat response with requestId: ${event.messageId}" } - } logger.info { "No code block found in inline chat response with requestId: ${event.messageId} \nresponse: ${event.message} " } return } @@ -354,8 +349,6 @@ class InlineChatController( } catch (e: Exception) { e.printStackTrace() } -// currentDocumentLine++ - if (row.tag == DiffRow.Tag.CHANGE) { insertLine+=2 currentDocumentLine+=2 @@ -384,14 +377,23 @@ class InlineChatController( metrics?.numSuggestionAddLines = addedLinesCount metrics?.numSuggestionDelChars = deletedCharsCount metrics?.numSuggestionDelLines = deletedLinesCount + } else { + if (event.messageType == ChatMessageType.Answer) { + val codeBlocks = getCodeBlocks(prevMessage) + if(codeBlocks.isEmpty()) { + logger.warn { "No code block found in inline chat response with requestId: ${event.messageId} \nresponse: ${event.message}" } + isInProgress.set(false) + throw Exception("No recommendation provided. Please try again with a different question.") + } + } } } - private fun insertNewLineIfNeeded(row: Int, document: Document) : Int { + private suspend fun insertNewLineIfNeeded(row: Int, document: Document) : Int { var newLineInserted = 0 while (row > document.lineCount - 1) { - document.insertString(document.textLength, "\n") + insertString(document, document.textLength, "\n") newLineInserted++ } return newLineInserted @@ -421,14 +423,23 @@ class InlineChatController( private suspend fun runChangeAction(project: Project, action: () -> Unit, shouldRecordForUndo: Boolean = false) { withContext(EDT) { +// val undoManager = UndoManager.getInstance(project) +// val undoableGroup = undoManager.undoableGroup CommandProcessor.getInstance().executeCommand(project, { ApplicationManager.getApplication().runWriteAction { WriteCommandAction.runWriteCommandAction(project) { +// UndoManager.getInstance(project).undoableActionPerformed(object : BasicUndoableAction() { +// override fun undo() { +// } +// +// override fun redo() { +// } +// }) action() } } - }, "", null, UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, shouldRecordForUndo) + }, "", null, UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, false) } } @@ -436,9 +447,9 @@ class InlineChatController( var rangeMarker: RangeMarker? = null val action = { document.insertString(offset, text) - rangeMarker = document.createRangeMarker(offset, offset + text.length) - rangeMarker!!.isGreedyToLeft = true - rangeMarker!!.isGreedyToRight = true +// CodeStyleManager.getInstance(project).adjustLineIndent(document, offset) + val row = document.getLineNumber(offset) + rangeMarker = document.createRangeMarker(offset, getLineEndOffset(document, row)) highlightCodeWithBackgroundColor(editor, rangeMarker!!.startOffset, rangeMarker!!.endOffset, true) } runChangeAction(project, action) @@ -456,8 +467,6 @@ class InlineChatController( var rangeMarker: RangeMarker? = null val action = { rangeMarker = document.createRangeMarker(start, end) -// rangeMarker!!.isGreedyToLeft = true -// rangeMarker!!.isGreedyToRight = true highlightCodeWithBackgroundColor(editor, rangeMarker!!.startOffset, rangeMarker!!.endOffset, isInsert) } runChangeAction(project, action) @@ -468,7 +477,6 @@ class InlineChatController( val document = editor.document when (diffRow.tag) { DiffRow.Tag.DELETE -> { -// val rowNum = getRowNumber(index) val changeStartOffset = getLineStartOffset(document, row) val changeEndOffset = getLineEndOffset(document, row) val rangeMarker = highlightString(document, changeStartOffset, changeEndOffset, false) @@ -482,12 +490,10 @@ class InlineChatController( } } editor.markupModel.removeAllHighlighters() -// decrementRowNumber() } } DiffRow.Tag.INSERT -> { -// val insertRow = getRowNumber(index) val newLineInserted = insertNewLineIfNeeded(row, document) val insertOffset = getLineStartOffset(document, row) val textToInsert = unescape(diffRow.newLine) + "\n" @@ -499,12 +505,10 @@ class InlineChatController( } } editor.markupModel.removeAllHighlighters() -// decrementRowNumber() } partialAcceptActions.add { editor.markupModel.removeAllHighlighters() } -// incrementRowNumber() } else -> { @@ -532,9 +536,7 @@ class InlineChatController( } } editor.markupModel.removeAllHighlighters() -// decrementRowNumber() } -// incrementRowNumber() } } } @@ -563,7 +565,7 @@ class InlineChatController( val languageExtractor = LanguageExtractor() val intentRecognizer = UserIntentRecognizer() - val language = editor.project?.let { languageExtractor.extractLanguageNameFromCurrentFile(editor, it) } ?: "" + val language = editor.project?.let { languageExtractor.extractProgrammingLanguageNameFromCurrentFile(editor, it) } ?: "" var baseRules = "- Plan out the changes step-by-step before making them, this should be brief and not include any code\n" + "- Do not explain the code after, the plan and code are sufficient\n" @@ -578,7 +580,7 @@ class InlineChatController( baseRules += "- If the query is a question only attempt to add comments to the code that answer it\n" + "- Make sure to preserve the original indentation, code formatting, tab size and structure as much as possible\n" + "- Do not change the code more than required, try to maintain variables, function names, and other identifiers" - prompt = "```$language\n$selectedCode```\n" + + prompt = "```$language\n$selectedCode\n```\n" + "Rules for rewriting the code:\n" + baseRules + "Rewrite the above code to do the following:\n" + message } @@ -587,8 +589,6 @@ class InlineChatController( val contextExtractor = ActiveFileContextExtractor.create(fqnWebviewAdapter = null, project = project) val fileContext = contextExtractor.extractContextForTrigger(ExtractionTriggerType.ChatMessage) -// val fileContextExtractor = FileContextExtractor(null, project) -// val fileContext = fileContextExtractor.extract() val requestData = ChatRequestData( tabId = "inlineChat-editor", @@ -608,14 +608,13 @@ class InlineChatController( var prevMessage = "" val chat = sessionInfo.scope.async { ChatPromptHandler(telemetryHelper).handle("inlineChat-editor", triggerId, requestData, sessionInfo, false, true) -// sessionInfo.session.chat(requestData) .catch { e -> logger.warn { "Error in inline chat request: ${e.message}" } errorMessage = e.message ?: "" } - .onEach { event: ChatMessage -> + .onEach{ event: ChatMessage -> if (event.message?.isNotEmpty() == true && prevMessage != event.message) { - processChatMessage(selectedCode, event, editor, selectedLineStart) + runBlocking { processChatMessage(selectedCode, event, editor, selectedLineStart, prevMessage) } prevMessage = event.message } if (messages.isEmpty()) { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt index 8d1836e738c..a6327ed2b7f 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt @@ -33,8 +33,10 @@ class InlineChatFileListener(private val context: AmazonQAppInitContext) : FileE private fun setupListenersForCurrentEditor() { currentEditor?.let { editor -> - caretListener = ChatCaretListener(context.project, context).also { listener -> - editor.caretModel.addCaretListener(listener) + caretListener = editor.project?.let { + ChatCaretListener(it, context).also { listener -> + editor.caretModel.addCaretListener(listener) + } } selectionListener = InlineChatSelectionListener().also { listener -> diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt index 1da3a12242c..addc96d8339 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt @@ -5,10 +5,37 @@ package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.CaretEvent +import com.intellij.openapi.editor.event.CaretListener import org.jetbrains.plugins.terminal.exp.TerminalDataContextUtils.editor class OpenChatInputAction : AnAction() { + private var inlineChatController: InlineChatController? = null + private var caretListener: CaretListener? = null override fun actionPerformed(e: AnActionEvent) { - e.editor?.project?.let { InlineChatController(e.editor!!, it).initPopup() } + e.editor?.let { editor -> + e.editor?.project?.let { project -> + inlineChatController = InlineChatController(editor, project) + inlineChatController?.initPopup() + + caretListener = createCaretListener(editor) + editor.caretModel.addCaretListener(caretListener!!) + } + } + } + + private fun createCaretListener(editor: Editor): CaretListener { + return object : CaretListener { + override fun caretPositionChanged(event: CaretEvent) { + // Remove the popup when the caret moves + inlineChatController?.disposePopup() + + // Remove the listener after closing the popup + editor.caretModel.removeCaretListener(this) + caretListener = null + inlineChatController = null + } + } } } From 425a37542e7a1dcb154d74aa97b248f1a281329d Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Mon, 14 Oct 2024 10:10:30 -0700 Subject: [PATCH 07/39] more fixes --- .../resources/META-INF/plugin-chat.xml | 6 - .../services/cwc/inline/ChatCaretListener.kt | 4 - .../cwc/inline/InlineChatCodeVisionManager.kt | 81 --------- .../cwc/inline/InlineChatController.kt | 3 +- .../cwc/inline/InlineChatEditorHint.kt | 2 +- .../cwc/inline/InlineChatFileListener.kt | 3 - .../cwc/inline/InlineChatPopupFactory.kt | 159 +--------------- .../cwc/inline/InlineChatPopupPanel.kt | 169 ++++++++++++++++++ .../cwc/inline/InlineChatSelectionListener.kt | 22 ++- .../inline/actions/InlineChatAcceptAction.kt | 49 ----- .../inline/actions/InlineChatRejectAction.kt | 32 ---- 11 files changed, 184 insertions(+), 346 deletions(-) delete mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionManager.kt create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt delete mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatAcceptAction.kt delete mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatRejectAction.kt diff --git a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml index 1085e2f5cd7..0190882d618 100644 --- a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml +++ b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml @@ -39,12 +39,6 @@ - - - - - - diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt index d5a1e37891c..c4dff8a998d 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.jetbrains.services.cwc.inline -import com.intellij.icons.AllIcons import com.intellij.ide.DataManager import com.intellij.openapi.actionSystem.ActionGroup import com.intellij.openapi.actionSystem.AnAction @@ -19,10 +18,7 @@ import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project import icons.AwsIcons import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext -import java.awt.Component -import java.awt.event.MouseEvent import javax.swing.Icon -import javax.swing.SwingUtilities class ChatCaretListener(private val project: Project, private val context: AmazonQAppInitContext) : CaretListener { private var currentHighlighter: RangeHighlighter? = null diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionManager.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionManager.kt deleted file mode 100644 index b2ce86e6552..00000000000 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionManager.kt +++ /dev/null @@ -1,81 +0,0 @@ -//import com.intellij.codeInsight.hints.ChangeListener -//import com.intellij.codeInsight.hints.ImmediateConfigurable -//import com.intellij.codeInsight.hints.InlayHintsCollector -//import com.intellij.codeInsight.hints.InlayHintsManager -//import com.intellij.codeInsight.hints.InlayHintsProvider -//import com.intellij.codeInsight.hints.InlayHintsSink -//import com.intellij.codeInsight.hints.NoSettings -//import com.intellij.codeInsight.hints.presentation.InlayPresentation -//import com.intellij.openapi.editor.Editor -//import com.intellij.openapi.editor.event.SelectionEvent -//import com.intellij.openapi.editor.event.SelectionListener -//import com.intellij.openapi.project.Project -//import com.intellij.psi.PsiFile -//import javax.swing.JPanel -// -//class InlineChatCodeVisionManager : InlayHintsProvider { -// private var selectionListener: SelectionListener? = null -// -// override fun createConfigurable(settings: NoSettings) = object : ImmediateConfigurable { -// override fun createComponent(listener: ChangeListener) = JPanel() -// } -// -// override fun getCollectorFor( -// file: PsiFile, -// editor: Editor, -// settings: NoSettings, -// sink: InlayHintsSink -// ): InlayHintsCollector? { -// val project = file.project -// -// // Remove any existing listener -// selectionListener?.let { editor.selectionModel.removeSelectionListener(it) } -// -// // Create a new listener -// selectionListener = object : SelectionListener { -// override fun selectionChanged(e: SelectionEvent) { -// // Trigger a refresh of inlay hints -// InlayHintsManager.getInstance(project).refreshInlayHints(editor) -// } -// } -// -// // Add the new listener -// editor.selectionModel.addSelectionListener(selectionListener!!) -// -// return object : InlayHintsCollector { -// override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean { -// val selectionModel = editor.selectionModel -// val selectionStart = selectionModel.selectionStart -// val selectionEnd = selectionModel.selectionEnd -// -// if (selectionStart == selectionEnd) { -// return true // No selection, no hints -// } -// -// if (element.textRange.intersects(selectionStart, selectionEnd)) { -// val elementType = element.node.elementType.toString().toLowerCase() -// if (elementType.contains("function") || elementType.contains("method") || elementType.contains("class")) { -// val hint = createInlayPresentation(element, editor, project) -// sink.addInlineElement(element.textOffset, true, hint) -// } -// } -// -// return true -// } -// } -// } -// -// private fun createInlayPresentation(element: PsiElement, editor: Editor, project: Project): InlayPresentation { -// // Create and return your inlay presentation here -// // This could be a simple text presentation or a more complex clickable presentation -// } -// -// override fun getKey() = SettingsKey("InlineChatCodeVision") -// override fun getName() = "Inline Chat Code Vision" -// override fun createSettings() = NoSettings() -// -// override fun dispose() { -// // Clean up the listener when the provider is disposed -// selectionListener = null -// } -//} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index df78492b354..872cf46387a 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -243,7 +243,7 @@ class InlineChatController( return if (newlineIndex != -1) { input.substring(newlineIndex + 1) } else { - input + "" } } @@ -371,6 +371,7 @@ class InlineChatController( } } } + isInProgress.set(false) shouldShowActions.set(true) metrics?.numSuggestionAddChars = addedCharsCount diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt index 6bf0bb2c493..b1aef48dee2 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt @@ -50,7 +50,7 @@ class InlineChatEditorHint(private val project: Project, private val editor: Edi HintManagerImpl.getInstanceImpl().showEditorHint( hint!!, editor, location, - HintManager.HIDE_BY_TEXT_CHANGE, + HintManager.HIDE_BY_TEXT_CHANGE or HintManager.HIDE_BY_SCROLLING, 0, false, HintManagerImpl.createHintHint(editor, location, hint!!, HintManager.RIGHT_UNDER).setContentActive(false) ) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt index a6327ed2b7f..945c7cf90c8 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt @@ -8,10 +8,7 @@ import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManagerEvent import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.fileEditor.TextEditor -import com.intellij.openapi.vfs.VirtualFile import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext -import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererSettings -import software.aws.toolkits.jetbrains.services.cwc.editor.context.project.ProjectContextController class InlineChatFileListener(private val context: AmazonQAppInitContext) : FileEditorManagerListener { private var currentEditor: Editor? = context.project.let { FileEditorManager.getInstance(it).selectedTextEditor } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index 901936b7b95..701e4e18850 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -6,32 +6,16 @@ package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.icons.AllIcons import com.intellij.openapi.application.ReadAction import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.ui.popup.IconButton import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.ui.popup.JBPopupFactory -import com.intellij.openapi.ui.popup.JBPopupListener -import com.intellij.openapi.ui.popup.LightweightWindowEvent import com.intellij.openapi.util.TextRange import com.intellij.ui.IdeBorderFactory -import com.intellij.ui.JBColor -import com.intellij.ui.components.JBLabel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.POPUP_INFO_TEXT_SIZE import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.TelemetryHelper -import java.awt.BorderLayout -import java.awt.Dimension -import java.awt.Font -import javax.swing.BorderFactory -import javax.swing.JButton -import javax.swing.JLabel -import javax.swing.JPanel -import javax.swing.JTextField -import javax.swing.event.DocumentEvent -import javax.swing.event.DocumentListener class InlineChatPopupFactory( @@ -63,7 +47,7 @@ class InlineChatPopupFactory( } fun createPopup(): JBPopup { - val popupPanel = ChatInputPopupPanel().apply { + val popupPanel = InlineChatPopupPanel().apply { border = IdeBorderFactory.createRoundedBorder(10).apply { setColor(POPUP_BUTTON_BORDER) } @@ -116,21 +100,8 @@ class InlineChatPopupFactory( return popup } - private fun initPopup(panel: ChatInputPopupPanel): JBPopup { -// val titlePanel = JPanel(BorderLayout()).apply { -// background = JBColor.background() -// val titleLabel = JBLabel("Send to AmazonQ").apply { -// font = font.deriveFont(font.style or java.awt.Font.BOLD) -// } -// add(titleLabel, BorderLayout.CENTER) -// val editorColorsScheme = EditorColorsManager.getInstance().globalScheme -// border = IdeBorderFactory.createRoundedBorder().apply { -// setColor(POPUP_BUTTON_BORDER) -// } -// font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) -// } + private fun initPopup(panel: InlineChatPopupPanel): JBPopup { val cancelButton = IconButton("Cancel", AllIcons.Actions.Cancel) -// cancelButton. val popup = JBPopupFactory.getInstance() .createComponentPopupBuilder(panel, panel.textField) .setMovable(true) @@ -146,137 +117,11 @@ class InlineChatPopupFactory( .setAlpha(0.2F) .setCancelOnClickOutside(false) .setCancelOnOtherWindowOpen(false) -// .setCancelKeyEnabled(true) .setFocusable(true) .setRequestFocus(true) .setLocateWithinScreenBounds(true) .createPopup() return popup } - - class ChatInputPopupPanel : JPanel() { - val textField = JTextField().apply { - val editorColorsScheme = EditorColorsManager.getInstance().globalScheme - border = IdeBorderFactory.createRoundedBorder().apply { - setColor(POPUP_BUTTON_BORDER) - } - font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) - } - val submitButton = JButton("Send").apply { - border = IdeBorderFactory.createRoundedBorder().apply { - setColor(POPUP_BUTTON_BORDER) - } - } - private val acceptButton = JButton("Accept").apply { - preferredSize = Dimension(80, 30) -// isFocusable = false - border = IdeBorderFactory.createRoundedBorder().apply { - setColor(POPUP_BUTTON_BORDER) - } - } - private val rejectButton = JButton("Reject").apply { - preferredSize = Dimension(80, 30) -// isFocusable = false - border = IdeBorderFactory.createRoundedBorder().apply { - setColor(POPUP_BUTTON_BORDER) - } - } - private var textChangeListener: ((String) -> Unit)? = null - private var submitClickListener: (() -> Unit)? = null - private val textLabel = JLabel("").apply { - val editorColorsScheme = EditorColorsManager.getInstance().globalScheme -// font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) - font = font.deriveFont(POPUP_INFO_TEXT_SIZE) - } - private val actionsPanel = JPanel(BorderLayout()).apply { - border = BorderFactory.createEmptyBorder(0, 20, 5, 20) - add(acceptButton, BorderLayout.WEST) - add(rejectButton, BorderLayout.EAST) - } - private val inputPanel = JPanel(BorderLayout()).apply { - border = BorderFactory.createEmptyBorder(12, 10, 12, 10) - maximumSize = Dimension(580, 30) - } - private val labelPanel = JPanel(BorderLayout()).apply { - border = BorderFactory.createEmptyBorder(5, 20, 5, 20) - add(textLabel, BorderLayout.CENTER) - } - - override fun getPreferredSize(): Dimension { - return Dimension(600, 60) - } - - init { - layout = BorderLayout() - inputPanel.add(submitButton, BorderLayout.EAST) - inputPanel.add(textField, BorderLayout.WEST) - textField.preferredSize = Dimension(500, 30) - submitButton.preferredSize = Dimension(60, 30) - inputPanel.preferredSize = Dimension(600, 30) - add(inputPanel, BorderLayout.CENTER) - val listener = object : DocumentListener { - override fun insertUpdate(e: DocumentEvent) { - updateText() - } - - override fun removeUpdate(e: DocumentEvent) { - updateText() - } - - override fun changedUpdate(e: DocumentEvent) { - updateText() - } - - private fun updateText() { - val newText = textField.text - textChangeListener?.invoke(newText) - } - } - textField.document.addDocumentListener(listener) - - submitButton.addActionListener { - submitClickListener?.invoke() - } - } - - fun setTextChangeListener(listener: (String) -> Unit) { - textChangeListener = listener - } - - fun setSubmitClickListener(listener: () -> Unit) { - submitClickListener = listener - } - -// private fun addActionListener(id: String, action: EditorWriteActionHandler) : Disposable { -// val actionManager = EditorActionManager.getInstance() -// val originalHandler = actionManager.getActionHandler(IdeActions.ACTION_EDITOR_TAB) -// -// actionManager.setActionHandler(IdeActions.ACTION_EDITOR_TAB, action) -// val restorer = object : Disposable { -// override fun dispose() { -// actionManager.setActionHandler(IdeActions.ACTION_EDITOR_TAB, originalHandler) -// } -// } -// return restorer -// } - - fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit ) { - textLabel.text = "Code diff generated. Do you want to accept it?" - textLabel.revalidate() - inputPanel.revalidate() - acceptButton.addActionListener { acceptAction.invoke() } - rejectButton.addActionListener { rejectAction.invoke() } - add(actionsPanel, BorderLayout.SOUTH) - revalidate() - } - - fun setLabel(text: String) { - textLabel.text = text - textLabel.revalidate() - remove(inputPanel) - add(labelPanel) - revalidate() - } - } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt new file mode 100644 index 00000000000..eca39c9251f --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -0,0 +1,169 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cwc.inline + +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.actionSystem.IdeActions +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.intellij.openapi.editor.actionSystem.EditorActionManager +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.util.Disposer +import com.intellij.ui.IdeBorderFactory +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.POPUP_INFO_TEXT_SIZE +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.Font +import javax.swing.BorderFactory +import javax.swing.JButton +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.JTextField +import javax.swing.event.DocumentEvent +import javax.swing.event.DocumentListener + +class InlineChatPopupPanel : JPanel(), Disposable { + val textField = JTextField().apply { + val editorColorsScheme = EditorColorsManager.getInstance().globalScheme + border = IdeBorderFactory.createRoundedBorder().apply { + setColor(POPUP_BUTTON_BORDER) + } + font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) + } + val submitButton = JButton("Send").apply { + border = IdeBorderFactory.createRoundedBorder().apply { + setColor(POPUP_BUTTON_BORDER) + } + } + private val acceptButton = JButton("Accept").apply { + preferredSize = Dimension(80, 30) + border = IdeBorderFactory.createRoundedBorder().apply { + setColor(POPUP_BUTTON_BORDER) + } + } + private val rejectButton = JButton("Reject").apply { + preferredSize = Dimension(80, 30) + border = IdeBorderFactory.createRoundedBorder().apply { + setColor(POPUP_BUTTON_BORDER) + } + } + private var textChangeListener: ((String) -> Unit)? = null + private var submitClickListener: (() -> Unit)? = null + private val textLabel = JLabel("").apply { + val editorColorsScheme = EditorColorsManager.getInstance().globalScheme + font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) +// font = font.deriveFont(POPUP_INFO_TEXT_SIZE) + } + private val actionsPanel = JPanel(BorderLayout()).apply { + border = BorderFactory.createEmptyBorder(0, 20, 5, 20) + add(acceptButton, BorderLayout.WEST) + add(rejectButton, BorderLayout.EAST) + } + private val inputPanel = JPanel(BorderLayout()).apply { + border = BorderFactory.createEmptyBorder(12, 10, 12, 10) + maximumSize = Dimension(580, 30) + } + private val labelPanel = JPanel(BorderLayout()).apply { + border = BorderFactory.createEmptyBorder(5, 20, 5, 20) + add(textLabel, BorderLayout.CENTER) + } + + override fun getPreferredSize(): Dimension { + return Dimension(600, 60) + } + + init { + layout = BorderLayout() + inputPanel.add(submitButton, BorderLayout.EAST) + inputPanel.add(textField, BorderLayout.WEST) + textField.preferredSize = Dimension(500, 30) + submitButton.preferredSize = Dimension(60, 30) + inputPanel.preferredSize = Dimension(600, 30) + add(inputPanel, BorderLayout.CENTER) + val listener = object : DocumentListener { + override fun insertUpdate(e: DocumentEvent) { + updateText() + } + + override fun removeUpdate(e: DocumentEvent) { + updateText() + } + + override fun changedUpdate(e: DocumentEvent) { + updateText() + } + + private fun updateText() { + val newText = textField.text + textChangeListener?.invoke(newText) + } + } + textField.document.addDocumentListener(listener) + + submitButton.addActionListener { + submitClickListener?.invoke() + } + } + + fun setTextChangeListener(listener: (String) -> Unit) { + textChangeListener = listener + } + + fun setSubmitClickListener(listener: () -> Unit) { + submitClickListener = listener + } + + private fun addActionListener(id: String, action: EditorActionHandler) : Disposable { + val actionManager = EditorActionManager.getInstance() + val originalHandler = actionManager.getActionHandler(id) + + actionManager.setActionHandler(id, action) + val restorer = Disposable { actionManager.setActionHandler(id, originalHandler) } + return restorer + } + + fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit ) { + textLabel.text = "Code diff generated. Do you want to accept it?" + textLabel.revalidate() + inputPanel.revalidate() + acceptButton.addActionListener { acceptAction.invoke() } + rejectButton.addActionListener { rejectAction.invoke() } + add(actionsPanel, BorderLayout.SOUTH) + val enterHandler = object : EditorActionHandler() { + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { + acceptAction.invoke() + Disposer.dispose(this@InlineChatPopupPanel) + } + } + + val enterRestorer = addActionListener(IdeActions.ACTION_EDITOR_ENTER, enterHandler) + Disposer.register(this, enterRestorer) + + val escapeHandler = object : EditorActionHandler() { + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { + rejectAction.invoke() + Disposer.dispose(this@InlineChatPopupPanel) + } + } + + val escapeRestorer = addActionListener(IdeActions.ACTION_EDITOR_ESCAPE, escapeHandler) + Disposer.register(this, escapeRestorer) + revalidate() + } + + fun setLabel(text: String) { + textLabel.text = text + textLabel.revalidate() + remove(inputPanel) + add(labelPanel) + revalidate() + } + + override fun dispose() { + TODO("Not yet implemented") + } +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatSelectionListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatSelectionListener.kt index b5a529c3cc2..6b735553fac 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatSelectionListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatSelectionListener.kt @@ -6,7 +6,6 @@ package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.openapi.editor.event.SelectionEvent import com.intellij.openapi.editor.event.SelectionListener import java.awt.Point -import javax.swing.SwingUtilities class InlineChatSelectionListener : SelectionListener { private var inlineChatEditorHint: InlineChatEditorHint? = null @@ -17,23 +16,22 @@ class InlineChatSelectionListener : SelectionListener { if (selectionModel.hasSelection()) { val selectionEnd = selectionModel.selectionEnd val selectionLineEnd = editor.document.getLineEndOffset(editor.document.getLineNumber(selectionEnd)) - val logicalPosition = editor.offsetToLogicalPosition(selectionLineEnd) - val visualPosition = editor.logicalToVisualPosition(logicalPosition) - val position = editor.visualPositionToXY(visualPosition) + + val xyPosition = editor.offsetToXY(selectionLineEnd) + val editorLocation = editor.component.locationOnScreen + val editorContentLocation = editor.contentComponent.locationOnScreen + val position = Point( + editorContentLocation.x + xyPosition.x, + editorLocation.y + xyPosition.y - editor.scrollingModel.verticalScrollOffset - 50) val visibleArea = editor.scrollingModel.visibleArea - val adjustedX = (position.x + 200).coerceAtMost(visibleArea.x + visibleArea.width - 50) - val adjustedY = (position.y + 20).coerceAtMost(visibleArea.y + visibleArea.height - 50) + val adjustedX = (position.x ).coerceAtMost(visibleArea.x + visibleArea.width - 50) + val adjustedY = (position.y ).coerceAtMost(visibleArea.y + visibleArea.height - 50) val adjustedPosition = Point(adjustedX, adjustedY) - val pos = SwingUtilities.convertPoint( - editor.component, - adjustedPosition, - editor.component.rootPane.layeredPane - ) inlineChatEditorHint = editor.let { editor.project?.let { project -> InlineChatEditorHint(project, it) } } - inlineChatEditorHint?.show(pos) + inlineChatEditorHint?.show(adjustedPosition) } else { inlineChatEditorHint?.hide() } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatAcceptAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatAcceptAction.kt deleted file mode 100644 index 97048abf2b6..00000000000 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatAcceptAction.kt +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.cwc.inline.actions - -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.actionSystem.IdeActions -import com.intellij.openapi.editor.Caret -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.actionSystem.EditorAction -import com.intellij.openapi.editor.actionSystem.EditorActionHandler -import com.intellij.openapi.editor.actionSystem.EditorActionManager -import com.intellij.openapi.project.Project -import org.jetbrains.plugins.terminal.exp.TerminalDataContextUtils.editor -import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatController - -class InlineChatAcceptAction : EditorAction(InlineChatAcceptHandler()) { - class InlineChatAcceptHandler : EditorActionHandler() { - private val originalTabAction = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_TAB) - - override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext) { - val project = editor.project - if (project != null) { - val controller = InlineChatController(editor, project) - controller.diffAcceptHandler.invoke() - } else { - originalTabAction.execute(editor, caret, dataContext) - } - } - - override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext): Boolean { - val project = editor.project - return if (project != null) { - true - } else { - originalTabAction.isEnabled(editor, caret, dataContext) - } - } - - private fun shouldHandleCustomAction(editor: Editor, project: Project): Boolean { - return true -// InlineChatController(editor, project).getShouldShowActions() - } - } -} - diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatRejectAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatRejectAction.kt deleted file mode 100644 index 28d97a251bb..00000000000 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/actions/InlineChatRejectAction.kt +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.cwc.inline.actions - -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import org.jetbrains.plugins.terminal.exp.TerminalDataContextUtils.editor -import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatController - -class InlineChatRejectAction : AnAction() { - override fun update(e: AnActionEvent) { - val editor = e.getData(CommonDataKeys.EDITOR) - val project = e.project - e.presentation.isEnabled = if (editor != null && project != null) { - // Check if inline chat is active - InlineChatController(editor, project).getShouldShowActions() - } else false - } - - override fun actionPerformed(e: AnActionEvent) { - e.editor?.project?.let { - val controller = InlineChatController(e.editor!!, it) - if(controller.getShouldShowActions()) { - controller.diffRejectHandler.invoke() - } - } - } -} - - From 08f2797f66bacfff14079df16766c71b370ac200 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Mon, 14 Oct 2024 10:35:20 -0700 Subject: [PATCH 08/39] move listeners --- .../services/amazonq/toolwindow/AmazonQToolWindow.kt | 2 +- .../jetbrains/services/cwc/controller/ChatController.kt | 2 -- .../editor/context/project/ProjectContextEditorListener.kt | 2 -- .../cwc/inline/{ => listeners}/ChatCaretListener.kt | 6 +++--- .../cwc/inline/{ => listeners}/InlineChatFileListener.kt | 4 ++-- .../inline/{ => listeners}/InlineChatSelectionListener.kt | 3 ++- 6 files changed, 8 insertions(+), 11 deletions(-) rename plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/{ => listeners}/ChatCaretListener.kt (93%) rename plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/{ => listeners}/InlineChatFileListener.kt (94%) rename plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/{ => listeners}/InlineChatSelectionListener.kt (91%) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt index 76092fda4b9..6bbe985bf7b 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt @@ -30,7 +30,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.webview.FqnWebviewAdapte import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.EditorThemeAdapter import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable -import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatFileListener +import software.aws.toolkits.jetbrains.services.cwc.inline.listeners.InlineChatFileListener import javax.swing.JComponent @Service(Service.Level.PROJECT) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt index 93c186b86a5..0c3c64f485b 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt @@ -66,9 +66,7 @@ import software.aws.toolkits.jetbrains.services.cwc.editor.context.ActiveFileCon import software.aws.toolkits.jetbrains.services.cwc.editor.context.ActiveFileContextExtractor import software.aws.toolkits.jetbrains.services.cwc.editor.context.ExtractionTriggerType import software.aws.toolkits.jetbrains.services.cwc.editor.context.project.ProjectContextController -import software.aws.toolkits.jetbrains.services.cwc.editor.context.project.ProjectContextEditorListener import software.aws.toolkits.jetbrains.services.cwc.editor.context.project.RelevantDocument -import software.aws.toolkits.jetbrains.services.cwc.inline.ChatCaretListener import software.aws.toolkits.jetbrains.services.cwc.messages.AuthNeededException import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessage import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/project/ProjectContextEditorListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/project/ProjectContextEditorListener.kt index 8c8cda911a0..235c876d3f2 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/project/ProjectContextEditorListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/project/ProjectContextEditorListener.kt @@ -5,9 +5,7 @@ package software.aws.toolkits.jetbrains.services.cwc.editor.context.project import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.vfs.VirtualFile -import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererSettings -import software.aws.toolkits.jetbrains.services.cwc.inline.ChatCaretListener class ProjectContextEditorListener() : FileEditorManagerListener { override fun fileClosed(source: FileEditorManager, file: VirtualFile) { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt similarity index 93% rename from plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt rename to plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt index c4dff8a998d..138e8f676b6 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/ChatCaretListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt @@ -1,7 +1,7 @@ // Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package software.aws.toolkits.jetbrains.services.cwc.inline +package software.aws.toolkits.jetbrains.services.cwc.inline.listeners import com.intellij.ide.DataManager import com.intellij.openapi.actionSystem.ActionGroup import com.intellij.openapi.actionSystem.AnAction @@ -17,10 +17,10 @@ import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project import icons.AwsIcons -import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext +import software.aws.toolkits.jetbrains.services.cwc.inline.OpenChatInputAction import javax.swing.Icon -class ChatCaretListener(private val project: Project, private val context: AmazonQAppInitContext) : CaretListener { +class ChatCaretListener(private val project: Project) : CaretListener { private var currentHighlighter: RangeHighlighter? = null init { val editor = FileEditorManager.getInstance(project).selectedTextEditor diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt similarity index 94% rename from plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt rename to plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt index 945c7cf90c8..3a6c5ac1657 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatFileListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt @@ -1,7 +1,7 @@ // Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package software.aws.toolkits.jetbrains.services.cwc.inline +package software.aws.toolkits.jetbrains.services.cwc.inline.listeners import com.intellij.openapi.editor.Editor import com.intellij.openapi.fileEditor.FileEditorManager @@ -31,7 +31,7 @@ class InlineChatFileListener(private val context: AmazonQAppInitContext) : FileE private fun setupListenersForCurrentEditor() { currentEditor?.let { editor -> caretListener = editor.project?.let { - ChatCaretListener(it, context).also { listener -> + ChatCaretListener(it).also { listener -> editor.caretModel.addCaretListener(listener) } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatSelectionListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatSelectionListener.kt similarity index 91% rename from plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatSelectionListener.kt rename to plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatSelectionListener.kt index 6b735553fac..6804b745680 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatSelectionListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatSelectionListener.kt @@ -1,10 +1,11 @@ // Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package software.aws.toolkits.jetbrains.services.cwc.inline +package software.aws.toolkits.jetbrains.services.cwc.inline.listeners import com.intellij.openapi.editor.event.SelectionEvent import com.intellij.openapi.editor.event.SelectionListener +import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatEditorHint import java.awt.Point class InlineChatSelectionListener : SelectionListener { From fbe4f6249e10f4f903272c376e1347d1db8493f0 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Tue, 15 Oct 2024 09:36:10 -0700 Subject: [PATCH 09/39] cleanup --- .../amazonq/startup/AmazonQStartupActivity.kt | 2 + .../amazonq/toolwindow/AmazonQToolWindow.kt | 9 --- .../inline/InlineChatCodeVisionProvider.kt | 73 ------------------- .../cwc/inline/InlineChatController.kt | 53 ++++++++------ .../cwc/inline/InlineChatEditorHint.kt | 47 +++++++----- .../inline/InlineChatGutterIconRenderer.kt | 42 +++++++++++ .../cwc/inline/InlineChatPopupFactory.kt | 4 +- .../cwc/inline/OpenChatInputAction.kt | 4 +- .../cwc/inline/listeners/ChatCaretListener.kt | 68 +++++------------ .../listeners/InlineChatFileListener.kt | 54 +++++++------- .../listeners/InlineChatSelectionListener.kt | 30 +++----- 11 files changed, 161 insertions(+), 225 deletions(-) delete mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionProvider.kt create mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt index a3011e5963c..a9b9e15f79e 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt @@ -22,6 +22,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhisp import software.aws.toolkits.jetbrains.services.cwc.editor.context.project.ProjectContextController import java.lang.management.ManagementFactory import java.time.Duration +import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatController import java.util.concurrent.atomic.AtomicBoolean class AmazonQStartupActivity : ProjectActivity { @@ -32,6 +33,7 @@ class AmazonQStartupActivity : ProjectActivity { // initialize html contents in BGT so users don't have to wait when they open the tool window AmazonQToolWindow.getInstance(project) + InlineChatController.getInstance(project) if (CodeWhispererExplorerActionManager.getInstance().getIsFirstRestartAfterQInstall()) { runInEdt { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt index 6bbe985bf7b..e5673cc6dae 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt @@ -10,7 +10,6 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.runInEdt import com.intellij.openapi.components.Service import com.intellij.openapi.components.service -import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.wm.ToolWindowManager @@ -30,7 +29,6 @@ import software.aws.toolkits.jetbrains.services.amazonq.webview.FqnWebviewAdapte import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.EditorThemeAdapter import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable -import software.aws.toolkits.jetbrains.services.cwc.inline.listeners.InlineChatFileListener import javax.swing.JComponent @Service(Service.Level.PROJECT) @@ -48,8 +46,6 @@ class AmazonQToolWindow private constructor( private val appConnections = mutableListOf() - private var listener : InlineChatFileListener? = null - init { initConnections() connectUi() @@ -101,11 +97,6 @@ class AmazonQToolWindow private constructor( messageTypeRegistry = connection.messageTypeRegistry, fqnWebviewAdapter = fqnWebviewAdapter, ) - if (listener == null) { - listener = InlineChatFileListener(initContext).apply { - project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this) - } - } // Connect the app to the UI connection.app.init(initContext) // Dispose of the app when the tool window is disposed. diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionProvider.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionProvider.kt deleted file mode 100644 index 8d7038212a6..00000000000 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatCodeVisionProvider.kt +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.cwc.inline - -import com.intellij.codeInsight.codeVision.CodeVisionAnchorKind -import com.intellij.codeInsight.codeVision.CodeVisionEntry -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.ClickableTextCodeVisionEntry -import com.intellij.codeInsight.hints.InlayHintsUtils -import com.intellij.openapi.application.runReadAction -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.toolbar.floating.FloatingToolbarProvider -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiDocumentManager -import com.intellij.psi.SyntaxTraverser -import com.intellij.psi.util.elementType -import java.awt.event.MouseEvent -import kotlin.math.min - - -// disabled, register if need to bring back the code visions -class InlineChatCodeVisionProvider: CodeVisionProvider { - companion object { - internal const val id: String = "amazonq.chat.code.vision" - } - override val id: String = Companion.id - override val defaultAnchor: CodeVisionAnchorKind = CodeVisionAnchorKind.Top - override val name: String = "AmazonQ Chat Code Vision" - override val relativeOrderings: List = emptyList() - - override fun precomputeOnUiThread(editor: Editor) { - } - - override fun shouldRecomputeForEditor(editor: Editor, uiData: Unit?): Boolean = true - - override fun isAvailableFor(project: Project): Boolean = true - - - override fun computeCodeVision(editor: Editor, uiData: Unit): CodeVisionState { - return runReadAction { - val lenses = ArrayList>() - val project = editor.project ?: return@runReadAction READY_EMPTY - val document = editor.document - val file = PsiDocumentManager.getInstance(project).getPsiFile(document) ?: return@runReadAction READY_EMPTY - val traverser = SyntaxTraverser.psiTraverser(file) - - val text = "Amazon Q: Edit \u2318 + I" - val elements = traverser.preOrderDfsTraversal().filter { element -> - val elementType = element.elementType.toString().toLowerCase() - elementType.contains("function") || - elementType.contains("class") - } - val clickHandler : - (MouseEvent?, Editor)-> Unit = { e: MouseEvent?, _ -> - InlineChatController(editor, editor.project!!).initPopup() - } - FloatingToolbarProvider - for (element in elements) { - val textRange = InlayHintsUtils.getTextRangeWithoutLeadingCommentsAndWhitespaces(element) - val length = editor.document.textLength - val adjustedRange = TextRange(min(textRange.startOffset, length), min(textRange.endOffset, length)) - val entry = ClickableTextCodeVisionEntry(text, id, clickHandler, icon = null, text, text, emptyList()) - lenses.add(Pair(adjustedRange, entry)) - } - return@runReadAction CodeVisionState.Ready(lenses) - } - } -} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 872cf46387a..b5e296d4bcb 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -16,6 +16,8 @@ import com.intellij.openapi.command.UndoConfirmationPolicy import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.command.undo.BasicUndoableAction import com.intellij.openapi.command.undo.UndoManager +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.RangeMarker @@ -23,6 +25,7 @@ import com.intellij.openapi.editor.markup.HighlighterLayer import com.intellij.openapi.editor.markup.HighlighterTargetArea import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.project.Project import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.ui.popup.JBPopupListener @@ -69,6 +72,8 @@ import software.aws.toolkits.jetbrains.services.cwc.editor.context.ActiveFileCon import software.aws.toolkits.jetbrains.services.cwc.editor.context.ExtractionTriggerType import software.aws.toolkits.jetbrains.services.cwc.editor.context.file.FileContextExtractor import software.aws.toolkits.jetbrains.services.cwc.editor.context.file.util.LanguageExtractor +import software.aws.toolkits.jetbrains.services.cwc.editor.context.project.ProjectContextController +import software.aws.toolkits.jetbrains.services.cwc.inline.listeners.InlineChatFileListener import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessage import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType import software.aws.toolkits.jetbrains.services.cwc.storage.ChatSessionStorage @@ -77,9 +82,8 @@ import java.util.Stack import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean - +@Service(Service.Level.PROJECT) class InlineChatController( - private val editor: Editor, private val project: Project ) : Disposable { private var currentPopup: JBPopup? = null @@ -94,6 +98,9 @@ class InlineChatController( private val isInProgress = AtomicBoolean(false) private var metrics: InlineChatMetrics? = null private var isPopupAborted = AtomicBoolean(true) + private var listener: InlineChatFileListener = InlineChatFileListener(project).apply { + project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this) + } data class InlineChatMetrics( val requestId: String, @@ -111,7 +118,7 @@ class InlineChatController( var charactersRemoved: Int? = null, ) - private val popupSubmitHandler: suspend (String, String, Int) -> String = { prompt: String, selectedCode: String, selectedLineStart: Int -> + private val popupSubmitHandler: suspend (String, String, Int, Editor) -> String = { prompt: String, selectedCode: String, selectedLineStart: Int, editor: Editor -> // val selectedCode = getSelectedText(editor) runBlocking { isInProgress.set(true) @@ -162,7 +169,7 @@ class InlineChatController( metrics = null } - val diffAcceptHandler: () -> Unit = { + private val diffAcceptHandler: () -> Unit = { scope.launch(Dispatchers.EDT) { val undoManager = UndoManager.getGlobalInstance() // undoManager.undoableActionPerformed() @@ -180,7 +187,7 @@ class InlineChatController( } - val diffRejectHandler: () -> Unit = { + private val diffRejectHandler: () -> Unit = { scope.launch(Dispatchers.EDT) { while (partialUndoActions.isNotEmpty()) { val action = partialUndoActions.pop() @@ -209,8 +216,7 @@ class InlineChatController( } - fun initPopup () { -// currentPopup?.dispose() + fun initPopup (editor: Editor) { currentPopup?.let { Disposer.dispose(it) } currentPopup = InlineChatPopupFactory(acceptHandler = diffAcceptHandler, rejectHandler = diffRejectHandler, editor = editor, telemetryHelper = telemetryHelper, submitHandler = popupSubmitHandler, cancelHandler = popupCancelHandler, scope = scope).createPopup() @@ -248,7 +254,7 @@ class InlineChatController( } - fun hidePopup() { + private fun hidePopup() { isPopupAborted.set(false) currentPopup?.closeOk(null) currentPopup = null @@ -391,10 +397,10 @@ class InlineChatController( } - private suspend fun insertNewLineIfNeeded(row: Int, document: Document) : Int { + private suspend fun insertNewLineIfNeeded(row: Int, editor: Editor) : Int { var newLineInserted = 0 - while (row > document.lineCount - 1) { - insertString(document, document.textLength, "\n") + while (row > editor.document.lineCount - 1) { + insertString(editor, editor.document.textLength, "\n") newLineInserted++ } return newLineInserted @@ -444,13 +450,13 @@ class InlineChatController( } } - private suspend fun insertString(document: Document, offset: Int, text: String) : RangeMarker { + private suspend fun insertString(editor: Editor, offset: Int, text: String) : RangeMarker { var rangeMarker: RangeMarker? = null val action = { - document.insertString(offset, text) + editor.document.insertString(offset, text) // CodeStyleManager.getInstance(project).adjustLineIndent(document, offset) - val row = document.getLineNumber(offset) - rangeMarker = document.createRangeMarker(offset, getLineEndOffset(document, row)) + val row = editor.document.getLineNumber(offset) + rangeMarker = editor.document.createRangeMarker(offset, getLineEndOffset(editor.document, row)) highlightCodeWithBackgroundColor(editor, rangeMarker!!.startOffset, rangeMarker!!.endOffset, true) } runChangeAction(project, action) @@ -464,10 +470,10 @@ class InlineChatController( runChangeAction(project, action) } - private suspend fun highlightString(document: Document, start: Int, end: Int, isInsert: Boolean) : RangeMarker { + private suspend fun highlightString(editor: Editor, start: Int, end: Int, isInsert: Boolean) : RangeMarker { var rangeMarker: RangeMarker? = null val action = { - rangeMarker = document.createRangeMarker(start, end) + rangeMarker = editor.document.createRangeMarker(start, end) highlightCodeWithBackgroundColor(editor, rangeMarker!!.startOffset, rangeMarker!!.endOffset, isInsert) } runChangeAction(project, action) @@ -480,7 +486,7 @@ class InlineChatController( DiffRow.Tag.DELETE -> { val changeStartOffset = getLineStartOffset(document, row) val changeEndOffset = getLineEndOffset(document, row) - val rangeMarker = highlightString(document, changeStartOffset, changeEndOffset, false) + val rangeMarker = highlightString(editor, changeStartOffset, changeEndOffset, false) partialUndoActions.add { editor.markupModel.removeAllHighlighters() } @@ -495,10 +501,10 @@ class InlineChatController( } DiffRow.Tag.INSERT -> { - val newLineInserted = insertNewLineIfNeeded(row, document) + val newLineInserted = insertNewLineIfNeeded(row, editor) val insertOffset = getLineStartOffset(document, row) val textToInsert = unescape(diffRow.newLine) + "\n" - val rangeMarker = insertString(document, insertOffset, textToInsert) + val rangeMarker = insertString(editor, insertOffset, textToInsert) partialUndoActions.add { if (rangeMarker.isValid) { scope.launch(Dispatchers.EDT) { @@ -515,7 +521,7 @@ class InlineChatController( else -> { val changeOffset = getLineStartOffset(document, row) val changeEndOffset = getLineEndOffset(document, row) - val oldTextRangeMarker = highlightString(document, changeOffset, changeEndOffset, false) + val oldTextRangeMarker = highlightString(editor, changeOffset, changeEndOffset, false) partialAcceptActions.add { scope.launch(Dispatchers.EDT) { if (oldTextRangeMarker.isValid) { @@ -525,9 +531,9 @@ class InlineChatController( editor.markupModel.removeAllHighlighters() } val insertOffset = getLineEndOffset(document, row) - val newLineInserted = insertNewLineIfNeeded(row, document) + val newLineInserted = insertNewLineIfNeeded(row, editor) val textToInsert = unescape(diffRow.newLine) + "\n" - val newTextRangeMarker = insertString(document, insertOffset, textToInsert) + val newTextRangeMarker = insertString(editor, insertOffset, textToInsert) partialUndoActions.add { WriteCommandAction.runWriteCommandAction(project) { if (newTextRangeMarker.isValid) { @@ -637,6 +643,7 @@ class InlineChatController( } companion object { + fun getInstance(project: Project) = project.service() private val logger = getLogger() } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt index b1aef48dee2..06314eeef2b 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt @@ -6,7 +6,6 @@ import com.intellij.codeInsight.hint.HintManager import com.intellij.codeInsight.hint.HintManagerImpl import com.intellij.codeInsight.hint.HintUtil import com.intellij.openapi.editor.Editor -import com.intellij.openapi.project.Project import com.intellij.ui.LightweightHint import com.intellij.ui.SimpleColoredText import com.intellij.ui.SimpleTextAttributes @@ -16,19 +15,38 @@ import java.awt.Point import javax.swing.JPanel -class InlineChatEditorHint(private val project: Project, private val editor: Editor) { - private var hint: LightweightHint? = null +class InlineChatEditorHint { + private val hint = createHint() - fun show(location: Point) { - if (hint != null) return + private fun getHintLocation (editor: Editor): Point { + val selectionModel = editor.selectionModel + val selectionEnd = selectionModel.selectionEnd + val selectionLineEnd = editor.document.getLineEndOffset(editor.document.getLineNumber(selectionEnd)) + val xyPosition = editor.offsetToXY(selectionLineEnd) + val editorLocation = editor.component.locationOnScreen + val editorContentLocation = editor.contentComponent.locationOnScreen + val position = Point( + editorContentLocation.x + xyPosition.x, + editorLocation.y + xyPosition.y - editor.scrollingModel.verticalScrollOffset - 50) + + val visibleArea = editor.scrollingModel.visibleArea + + val adjustedX = (position.x ).coerceAtMost(visibleArea.x + visibleArea.width - 50) + val adjustedY = (position.y ).coerceAtMost(visibleArea.y + visibleArea.height - 50) + val adjustedPosition = Point(adjustedX, adjustedY) + + return adjustedPosition + } + + private fun createHint (): LightweightHint{ val icon = AwsIcons.Logos.AWS_Q_GREY val component = HintUtil.createInformationComponent() component.isIconOnTheRight = false; component.icon = icon val coloredText = - SimpleColoredText(hintText(), SimpleTextAttributes.REGULAR_ATTRIBUTES) + SimpleColoredText("Edit", SimpleTextAttributes.REGULAR_ATTRIBUTES) val shortCutIcon = AwsIcons.Misc.AWS_Q_INLINECHAT_SHORTCUT val shortcutComponent = HintUtil.createInformationComponent() @@ -46,26 +64,21 @@ class InlineChatEditorHint(private val project: Project, private val editor: Edi repaint() } - hint = LightweightHint(panel) + return LightweightHint(panel) + } + fun show(editor: Editor) { + val location = getHintLocation(editor) HintManagerImpl.getInstanceImpl().showEditorHint( - hint!!, editor, location, + hint, editor, location, HintManager.HIDE_BY_TEXT_CHANGE or HintManager.HIDE_BY_SCROLLING, 0, false, HintManagerImpl.createHintHint(editor, location, hint!!, HintManager.RIGHT_UNDER).setContentActive(false) ) - - } fun hide() { - hint?.hide() - hint = null - } - - - private fun hintText(): String { - return "Edit" + hint.hide() } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt new file mode 100644 index 00000000000..76335577b97 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt @@ -0,0 +1,42 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cwc.inline + +import com.intellij.openapi.actionSystem.ActionGroup +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.editor.markup.GutterIconRenderer +import javax.swing.Icon + +class InlineChatGutterIconRenderer (private val icon: Icon) : GutterIconRenderer() { + private var clickAction: (() -> Unit)? = null + override fun equals(other: Any?): Boolean { + if (other is InlineChatGutterIconRenderer) { + return icon == other.icon + } + return false + } + + override fun hashCode(): Int = icon.hashCode() + + override fun getIcon(): Icon = icon + + override fun getTooltipText(): String = "Amazon Q: Edit ⌘ + I" + + override fun isNavigateAction(): Boolean = false + + override fun getClickAction(): AnAction = object : AnAction() { + override fun actionPerformed(e: AnActionEvent) = clickAction?.invoke() ?: Unit + override fun update(e: AnActionEvent) = Unit + } + + fun setClickAction (action: () -> Unit) { + clickAction = action + } + + override fun getPopupMenuActions(): ActionGroup? = null + + override fun getAlignment(): Alignment = Alignment.CENTER +} + diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index 701e4e18850..d6fcec09abe 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -20,7 +20,7 @@ import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.Te class InlineChatPopupFactory( private val editor: Editor, - private val submitHandler: suspend (String, String, Int) -> String, + private val submitHandler: suspend (String, String, Int, Editor) -> String, private val acceptHandler: () -> Unit, private val rejectHandler: () -> Unit, private val cancelHandler: () -> Unit, @@ -65,7 +65,7 @@ class InlineChatPopupFactory( val selectedLineStart = getSelectionStartLine(editor) var errorMessage = "" runBlocking { - errorMessage = submitHandler(prompt, selectedCode, selectedLineStart) + errorMessage = submitHandler(prompt, selectedCode, selectedLineStart, editor) } if (errorMessage.isNotEmpty()) { setLabel(errorMessage) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt index addc96d8339..d461e86adb5 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt @@ -16,8 +16,8 @@ class OpenChatInputAction : AnAction() { override fun actionPerformed(e: AnActionEvent) { e.editor?.let { editor -> e.editor?.project?.let { project -> - inlineChatController = InlineChatController(editor, project) - inlineChatController?.initPopup() + inlineChatController = InlineChatController.getInstance(project) + inlineChatController?.initPopup(editor) caretListener = createCaretListener(editor) editor.caretModel.addCaretListener(caretListener!!) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt index 138e8f676b6..8d799b7f8f3 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt @@ -2,11 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.jetbrains.services.cwc.inline.listeners + import com.intellij.ide.DataManager -import com.intellij.openapi.actionSystem.ActionGroup -import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.Presentation +import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.event.CaretEvent import com.intellij.openapi.editor.event.CaretListener import com.intellij.openapi.editor.markup.GutterIconRenderer @@ -14,26 +15,16 @@ import com.intellij.openapi.editor.markup.HighlighterLayer import com.intellij.openapi.editor.markup.HighlighterTargetArea import com.intellij.openapi.editor.markup.MarkupModel import com.intellij.openapi.editor.markup.RangeHighlighter -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.project.Project import icons.AwsIcons +import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatGutterIconRenderer import software.aws.toolkits.jetbrains.services.cwc.inline.OpenChatInputAction -import javax.swing.Icon -class ChatCaretListener(private val project: Project) : CaretListener { +class ChatCaretListener : CaretListener { private var currentHighlighter: RangeHighlighter? = null - init { - val editor = FileEditorManager.getInstance(project).selectedTextEditor - editor?.caretModel?.addCaretListener(this) - } - override fun caretPositionChanged(event: CaretEvent) { - val editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return - val lineNumber = event.newPosition.line - val startOffset = editor.document.getLineStartOffset(lineNumber) - val endOffset = editor.document.getLineEndOffset(lineNumber) - val markupModel: MarkupModel = editor.markupModel - val gutterIconRenderer = ChatGutterIconRenderer(AwsIcons.Logos.AWS_Q_GREY).apply { + + private fun createGutterIconRenderer(editor: Editor): GutterIconRenderer{ + return InlineChatGutterIconRenderer(AwsIcons.Logos.AWS_Q_GREY).apply { setClickAction { val action = OpenChatInputAction() val dataContext = DataManager.getInstance().getDataContext(editor.component) @@ -45,6 +36,14 @@ class ChatCaretListener(private val project: Project) : CaretListener { action.actionPerformed(e) } } + } + + override fun caretPositionChanged(event: CaretEvent) { + val editor = event.editor + val lineNumber = event.newPosition.line + val startOffset = editor.document.getLineStartOffset(lineNumber) + val endOffset = editor.document.getLineEndOffset(lineNumber) + val markupModel: MarkupModel = editor.markupModel if (event.oldPosition.line != event.newPosition.line) { currentHighlighter?.let { @@ -59,41 +58,8 @@ class ChatCaretListener(private val project: Project) : CaretListener { HighlighterTargetArea.LINES_IN_RANGE ) currentHighlighter = highlighter - highlighter.gutterIconRenderer = gutterIconRenderer + highlighter.gutterIconRenderer = createGutterIconRenderer(editor) } } } } - -private class ChatGutterIconRenderer(private val icon: Icon) : GutterIconRenderer() { - private var clickAction: (() -> Unit)? = null - override fun equals(other: Any?): Boolean { - if (other is ChatGutterIconRenderer) { - return icon == other.icon - } - return false - } - - override fun hashCode(): Int = icon.hashCode() - - override fun getIcon(): Icon = icon - - override fun getTooltipText(): String = "Ask Amazon Q" - - override fun isNavigateAction(): Boolean = false - - override fun getClickAction(): AnAction = object : AnAction() { - // bring up the chat inputbox - override fun actionPerformed(e: AnActionEvent) = clickAction?.invoke() ?: Unit - override fun update(e: AnActionEvent) = Unit - } - - fun setClickAction (action: () -> Unit) { - clickAction = action - } - - override fun getPopupMenuActions(): ActionGroup? = null - - override fun getAlignment(): Alignment = Alignment.LEFT -} - diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt index 3a6c5ac1657..06f067bd2f9 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt @@ -8,55 +8,55 @@ import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManagerEvent import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.fileEditor.TextEditor -import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer -class InlineChatFileListener(private val context: AmazonQAppInitContext) : FileEditorManagerListener { - private var currentEditor: Editor? = context.project.let { FileEditorManager.getInstance(it).selectedTextEditor } +class InlineChatFileListener(project: Project) : FileEditorManagerListener { + private var currentEditor: Editor? = null private var caretListener: ChatCaretListener? = null private var selectionListener: InlineChatSelectionListener? = null init { - setupListenersForCurrentEditor() + val editor = project.let { FileEditorManager.getInstance(it).selectedTextEditor } + if (editor != null) { + setupListenersForEditor(editor) + currentEditor = editor + } } override fun selectionChanged(event: FileEditorManagerEvent) { val newEditor = (event.newEditor as? TextEditor)?.editor - if (newEditor != currentEditor) { - removeListenersFromCurrentEditor() + if (newEditor != null && newEditor != currentEditor) { + currentEditor?.let { removeListenersFromCurrentEditor(it) } + setupListenersForEditor(newEditor) currentEditor = newEditor - setupListenersForCurrentEditor() } } - private fun setupListenersForCurrentEditor() { - currentEditor?.let { editor -> - caretListener = editor.project?.let { - ChatCaretListener(it).also { listener -> - editor.caretModel.addCaretListener(listener) - } - } - - selectionListener = InlineChatSelectionListener().also { listener -> - editor.selectionModel.addSelectionListener(listener) - } + private fun setupListenersForEditor(editor: Editor) { + caretListener = ChatCaretListener().also { listener -> + editor.caretModel.addCaretListener(listener) + } + + selectionListener = InlineChatSelectionListener().also { listener -> + editor.selectionModel.addSelectionListener(listener) } } - private fun removeListenersFromCurrentEditor() { - currentEditor?.let { editor -> - caretListener?.let { listener -> - editor.caretModel.removeCaretListener(listener) - } - selectionListener?.let { listener -> - editor.selectionModel.removeSelectionListener(listener) - } + private fun removeListenersFromCurrentEditor(editor: Editor) { + caretListener?.let { listener -> + editor.caretModel.removeCaretListener(listener) + } + selectionListener?.let { listener -> + editor.selectionModel.removeSelectionListener(listener) + listener.dispose() } caretListener = null selectionListener = null } fun dispose() { - removeListenersFromCurrentEditor() + currentEditor?.let { removeListenersFromCurrentEditor(it) } currentEditor = null } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatSelectionListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatSelectionListener.kt index 6804b745680..2024013dae5 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatSelectionListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatSelectionListener.kt @@ -3,38 +3,26 @@ package software.aws.toolkits.jetbrains.services.cwc.inline.listeners +import com.intellij.openapi.Disposable import com.intellij.openapi.editor.event.SelectionEvent import com.intellij.openapi.editor.event.SelectionListener import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatEditorHint import java.awt.Point -class InlineChatSelectionListener : SelectionListener { - private var inlineChatEditorHint: InlineChatEditorHint? = null +class InlineChatSelectionListener : SelectionListener, Disposable { + private val inlineChatEditorHint = InlineChatEditorHint() override fun selectionChanged(e: SelectionEvent) { val editor = e.editor val selectionModel = editor.selectionModel if (selectionModel.hasSelection()) { - val selectionEnd = selectionModel.selectionEnd - val selectionLineEnd = editor.document.getLineEndOffset(editor.document.getLineNumber(selectionEnd)) - - val xyPosition = editor.offsetToXY(selectionLineEnd) - val editorLocation = editor.component.locationOnScreen - val editorContentLocation = editor.contentComponent.locationOnScreen - val position = Point( - editorContentLocation.x + xyPosition.x, - editorLocation.y + xyPosition.y - editor.scrollingModel.verticalScrollOffset - 50) - - val visibleArea = editor.scrollingModel.visibleArea - - val adjustedX = (position.x ).coerceAtMost(visibleArea.x + visibleArea.width - 50) - val adjustedY = (position.y ).coerceAtMost(visibleArea.y + visibleArea.height - 50) - val adjustedPosition = Point(adjustedX, adjustedY) - - inlineChatEditorHint = editor.let { editor.project?.let { project -> InlineChatEditorHint(project, it) } } - inlineChatEditorHint?.show(adjustedPosition) + inlineChatEditorHint.show(editor) } else { - inlineChatEditorHint?.hide() + inlineChatEditorHint.hide() } } + + override fun dispose() { + inlineChatEditorHint.hide() + } } From 8795b69b24c8925f1adb314a1cf7db019bc0766f Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Tue, 15 Oct 2024 14:37:35 -0700 Subject: [PATCH 10/39] fnf --- .../cwc/inline/InlineChatController.kt | 5 +- .../cwc/inline/InlineChatPopupFactory.kt | 16 +- .../cwc/inline/InlineChatPopupPanel.kt | 141 ++++++++++-------- .../amazonq_inline_chat_cancel_icon.svg | 3 + .../amazonq_inline_chat_confirm_icon.svg | 3 + .../jetbrains-community/src/icons/AwsIcons.kt | 5 + 6 files changed, 100 insertions(+), 73 deletions(-) create mode 100644 plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_cancel_icon.svg create mode 100644 plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_confirm_icon.svg diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index b5e296d4bcb..2b37f2729ff 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -98,7 +98,7 @@ class InlineChatController( private val isInProgress = AtomicBoolean(false) private var metrics: InlineChatMetrics? = null private var isPopupAborted = AtomicBoolean(true) - private var listener: InlineChatFileListener = InlineChatFileListener(project).apply { + private val listener: InlineChatFileListener = InlineChatFileListener(project).apply { project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this) } @@ -139,6 +139,7 @@ class InlineChatController( ApplicationManager.getApplication().executeOnPooledThread { recordInlineChatTelemetry(InlineChatUserDecision.DISMISS) } + currentPopup?.dispose() } } @@ -219,7 +220,7 @@ class InlineChatController( fun initPopup (editor: Editor) { currentPopup?.let { Disposer.dispose(it) } currentPopup = InlineChatPopupFactory(acceptHandler = diffAcceptHandler, rejectHandler = diffRejectHandler, editor = editor, - telemetryHelper = telemetryHelper, submitHandler = popupSubmitHandler, cancelHandler = popupCancelHandler, scope = scope).createPopup() + telemetryHelper = telemetryHelper, submitHandler = popupSubmitHandler, cancelHandler = popupCancelHandler).createPopup(scope) addPopupListeners(currentPopup!!) Disposer.register(this, currentPopup!!) isPopupAborted.set(true) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index d6fcec09abe..1d9b934cabf 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -4,6 +4,7 @@ package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.icons.AllIcons +import com.intellij.openapi.Disposable import com.intellij.openapi.application.ReadAction import com.intellij.openapi.editor.Editor import com.intellij.openapi.ui.popup.IconButton @@ -12,6 +13,7 @@ import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.util.TextRange import com.intellij.ui.IdeBorderFactory import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER @@ -25,8 +27,8 @@ class InlineChatPopupFactory( private val rejectHandler: () -> Unit, private val cancelHandler: () -> Unit, private val telemetryHelper: TelemetryHelper, - private val scope: CoroutineScope -) { +// private val scope: CoroutineScope +) : Disposable { private fun getSelectedText(editor: Editor): String { return ReadAction.compute { @@ -46,8 +48,8 @@ class InlineChatPopupFactory( } } - fun createPopup(): JBPopup { - val popupPanel = InlineChatPopupPanel().apply { + fun createPopup(scope: CoroutineScope): JBPopup { + val popupPanel = InlineChatPopupPanel(this).apply { border = IdeBorderFactory.createRoundedBorder(10).apply { setColor(POPUP_BUTTON_BORDER) } @@ -80,8 +82,6 @@ class InlineChatPopupFactory( addCodeActionsPanel(acceptAction , rejectAction) } } - } else { - // TODO: show some message here } } setSubmitClickListener(submitListener) @@ -123,5 +123,9 @@ class InlineChatPopupFactory( .createPopup() return popup } + + override fun dispose() { + cancelHandler.invoke() + } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index eca39c9251f..0e260d517a1 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -13,32 +13,44 @@ import com.intellij.openapi.editor.actionSystem.EditorActionManager import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.util.Disposer import com.intellij.ui.IdeBorderFactory +import icons.AwsIcons import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.POPUP_INFO_TEXT_SIZE import java.awt.BorderLayout import java.awt.Dimension import java.awt.Font import javax.swing.BorderFactory +import javax.swing.Icon import javax.swing.JButton import javax.swing.JLabel import javax.swing.JPanel import javax.swing.JTextField -import javax.swing.event.DocumentEvent -import javax.swing.event.DocumentListener +import javax.swing.SwingConstants + +class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { + private var submitClickListener: (() -> Unit)? = null -class InlineChatPopupPanel : JPanel(), Disposable { val textField = JTextField().apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme + preferredSize = Dimension(550, 35) border = IdeBorderFactory.createRoundedBorder().apply { setColor(POPUP_BUTTON_BORDER) } font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } - val submitButton = JButton("Send").apply { - border = IdeBorderFactory.createRoundedBorder().apply { - setColor(POPUP_BUTTON_BORDER) - } + + val submitButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, "Confirm") + + private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CANCEL, "Cancel").apply { + addActionListener { Disposer.dispose(parentDisposable) } + } + + private val buttonsPanel = JPanel(BorderLayout()).apply { + border = BorderFactory.createEmptyBorder(5, 5, 5, 5) +// preferredSize = Dimension(180, 40) + add(submitButton, BorderLayout.WEST) + add(cancelButton, BorderLayout.EAST) } + private val acceptButton = JButton("Accept").apply { preferredSize = Dimension(80, 30) border = IdeBorderFactory.createRoundedBorder().apply { @@ -51,8 +63,7 @@ class InlineChatPopupPanel : JPanel(), Disposable { setColor(POPUP_BUTTON_BORDER) } } - private var textChangeListener: ((String) -> Unit)? = null - private var submitClickListener: (() -> Unit)? = null +// private var submitClickListener: (() -> Unit)? = null private val textLabel = JLabel("").apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) @@ -64,66 +75,83 @@ class InlineChatPopupPanel : JPanel(), Disposable { add(rejectButton, BorderLayout.EAST) } private val inputPanel = JPanel(BorderLayout()).apply { - border = BorderFactory.createEmptyBorder(12, 10, 12, 10) - maximumSize = Dimension(580, 30) + border = BorderFactory.createEmptyBorder(10, 10, 5, 10) + preferredSize = Dimension(600, 50) +// maximumSize = Dimension(580, 50) } private val labelPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 20, 5, 20) add(textLabel, BorderLayout.CENTER) } + private val logoPanel = JPanel(BorderLayout()).apply { + border = BorderFactory.createEmptyBorder(5, 5, 5, 5) +// preferredSize = Dimension(180, 40) + val logoLabel = JLabel("Edit Code", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { + font = font.deriveFont(14f) + } + add(logoLabel, BorderLayout.CENTER) + } + + private val bottomPanel = JPanel(BorderLayout()).apply { + add(logoPanel, BorderLayout.WEST) + add(buttonsPanel, BorderLayout.EAST) +// preferredSize = Dimension(650, 40) + } + override fun getPreferredSize(): Dimension { - return Dimension(600, 60) + return Dimension(600, 90) + } + + private fun createButtonWithIcon(icon: Icon, text: String): JButton { + return JButton(text).apply { + horizontalTextPosition = SwingConstants.LEFT + preferredSize = Dimension(80, 30) + setIcon(icon) + isOpaque = false + isContentAreaFilled = false + isBorderPainted = false + font = font.deriveFont(14f) + } } init { layout = BorderLayout() - inputPanel.add(submitButton, BorderLayout.EAST) - inputPanel.add(textField, BorderLayout.WEST) - textField.preferredSize = Dimension(500, 30) - submitButton.preferredSize = Dimension(60, 30) - inputPanel.preferredSize = Dimension(600, 30) + inputPanel.add(textField, BorderLayout.CENTER) + inputPanel.preferredSize = Dimension(600, 50) add(inputPanel, BorderLayout.CENTER) - val listener = object : DocumentListener { - override fun insertUpdate(e: DocumentEvent) { - updateText() - } - - override fun removeUpdate(e: DocumentEvent) { - updateText() - } - - override fun changedUpdate(e: DocumentEvent) { - updateText() - } - - private fun updateText() { - val newText = textField.text - textChangeListener?.invoke(newText) - } - } - textField.document.addDocumentListener(listener) + add(bottomPanel, BorderLayout.SOUTH) submitButton.addActionListener { submitClickListener?.invoke() } } - fun setTextChangeListener(listener: (String) -> Unit) { - textChangeListener = listener - } - fun setSubmitClickListener(listener: () -> Unit) { submitClickListener = listener } - private fun addActionListener(id: String, action: EditorActionHandler) : Disposable { + fun setCancelClickListener(listener: () -> Unit) { + cancelButton.addActionListener { listener.invoke() } + } + + private fun addActionListener(id: String, action: EditorActionHandler) { val actionManager = EditorActionManager.getInstance() val originalHandler = actionManager.getActionHandler(id) actionManager.setActionHandler(id, action) val restorer = Disposable { actionManager.setActionHandler(id, originalHandler) } - return restorer + Disposer.register(parentDisposable, restorer) + } + + private fun getEditorActionHandler(action: () -> Unit) : EditorActionHandler { + val handler = object : EditorActionHandler() { + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { + action.invoke() + Disposer.dispose(parentDisposable) + } + } + return handler } fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit ) { @@ -133,25 +161,11 @@ class InlineChatPopupPanel : JPanel(), Disposable { acceptButton.addActionListener { acceptAction.invoke() } rejectButton.addActionListener { rejectAction.invoke() } add(actionsPanel, BorderLayout.SOUTH) - val enterHandler = object : EditorActionHandler() { - override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { - acceptAction.invoke() - Disposer.dispose(this@InlineChatPopupPanel) - } - } - - val enterRestorer = addActionListener(IdeActions.ACTION_EDITOR_ENTER, enterHandler) - Disposer.register(this, enterRestorer) + val enterHandler = getEditorActionHandler(acceptAction) + addActionListener(IdeActions.ACTION_EDITOR_ENTER, enterHandler) - val escapeHandler = object : EditorActionHandler() { - override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { - rejectAction.invoke() - Disposer.dispose(this@InlineChatPopupPanel) - } - } - - val escapeRestorer = addActionListener(IdeActions.ACTION_EDITOR_ESCAPE, escapeHandler) - Disposer.register(this, escapeRestorer) + val escapeHandler = getEditorActionHandler(rejectAction) + addActionListener(IdeActions.ACTION_EDITOR_ESCAPE, escapeHandler) revalidate() } @@ -159,11 +173,8 @@ class InlineChatPopupPanel : JPanel(), Disposable { textLabel.text = text textLabel.revalidate() remove(inputPanel) + remove(bottomPanel) add(labelPanel) revalidate() } - - override fun dispose() { - TODO("Not yet implemented") - } } diff --git a/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_cancel_icon.svg b/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_cancel_icon.svg new file mode 100644 index 00000000000..3e3e1560291 --- /dev/null +++ b/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_cancel_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_confirm_icon.svg b/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_confirm_icon.svg new file mode 100644 index 00000000000..40b16f05129 --- /dev/null +++ b/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_confirm_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/core/jetbrains-community/src/icons/AwsIcons.kt b/plugins/core/jetbrains-community/src/icons/AwsIcons.kt index b3edb03fe63..7cdae73ca2f 100644 --- a/plugins/core/jetbrains-community/src/icons/AwsIcons.kt +++ b/plugins/core/jetbrains-community/src/icons/AwsIcons.kt @@ -127,6 +127,11 @@ object AwsIcons { @JvmField val SEVERITY_CRITICAL = load("/icons/resources/codewhisperer/severity-critical.svg") } + + object InlineChat { + @JvmField val CONFIRM = load("/icons/resources/inlinechat/amazonq_inline_chat_confirm_icon.svg") + @JvmField val CANCEL = load("/icons/resources/inlinechat/amazonq_inline_chat_cancel_icon.svg") + } } object Actions { From 06a6f0d99003e1fe2d11c8a60a02a2c04f3e3fb2 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Wed, 16 Oct 2024 12:44:26 -0700 Subject: [PATCH 11/39] more fnf --- .../resources/META-INF/plugin-chat.xml | 6 - .../cwc/inline/InlineChatEditorHint.kt | 16 +- .../cwc/inline/InlineChatPopupFactory.kt | 4 +- .../cwc/inline/InlineChatPopupPanel.kt | 181 +----------------- .../inlinechat/amazonq_inline_chat_reject.svg | 4 + .../jetbrains-community/src/icons/AwsIcons.kt | 1 + 6 files changed, 19 insertions(+), 193 deletions(-) create mode 100644 plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_reject.svg diff --git a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml index 0190882d618..64659195e4c 100644 --- a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml +++ b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml @@ -20,12 +20,6 @@ factoryClass="software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory" icon="AwsIcons.Logos.AWS_Q" /> - - - - - - diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt index 06314eeef2b..5a3ee9ab410 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt @@ -6,6 +6,7 @@ import com.intellij.codeInsight.hint.HintManager import com.intellij.codeInsight.hint.HintManagerImpl import com.intellij.codeInsight.hint.HintUtil import com.intellij.openapi.editor.Editor +import com.intellij.openapi.util.SystemInfo import com.intellij.ui.LightweightHint import com.intellij.ui.SimpleColoredText import com.intellij.ui.SimpleTextAttributes @@ -48,12 +49,17 @@ class InlineChatEditorHint { val coloredText = SimpleColoredText("Edit", SimpleTextAttributes.REGULAR_ATTRIBUTES) - val shortCutIcon = AwsIcons.Misc.AWS_Q_INLINECHAT_SHORTCUT + coloredText.appendToComponent(component) val shortcutComponent = HintUtil.createInformationComponent() - shortcutComponent.isIconOnTheRight = true; - shortcutComponent.icon = shortCutIcon - - coloredText.appendToComponent(shortcutComponent) + if (!SystemInfo.isWindows) { + val shortCutIcon = AwsIcons.Misc.AWS_Q_INLINECHAT_SHORTCUT + shortcutComponent.isIconOnTheRight = true; + shortcutComponent.icon = shortCutIcon + } else { + val shortcutText = + SimpleColoredText("(Ctrl + I)", SimpleTextAttributes.REGULAR_ATTRIBUTES) + shortcutText.appendToComponent(shortcutComponent) + } val panel = JPanel(BorderLayout()).apply { add(component, BorderLayout.WEST) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index 1d9b934cabf..955483ac52f 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -59,7 +59,7 @@ class InlineChatPopupFactory( textField.isEnabled = false val prompt = textField.text if (prompt.isNotBlank()) { - setLabel("AmazonQ generating...") + setLabel("Generating...") revalidate() scope.launch { @@ -70,7 +70,7 @@ class InlineChatPopupFactory( errorMessage = submitHandler(prompt, selectedCode, selectedLineStart, editor) } if (errorMessage.isNotEmpty()) { - setLabel(errorMessage) + setErrorMessage(errorMessage) revalidate() } else { val acceptAction = { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index 0e260d517a1..95bd538d615 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -1,180 +1 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.cwc.inline - -import com.intellij.openapi.Disposable -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.actionSystem.IdeActions -import com.intellij.openapi.editor.Caret -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.actionSystem.EditorActionHandler -import com.intellij.openapi.editor.actionSystem.EditorActionManager -import com.intellij.openapi.editor.colors.EditorColorsManager -import com.intellij.openapi.util.Disposer -import com.intellij.ui.IdeBorderFactory -import icons.AwsIcons -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER -import java.awt.BorderLayout -import java.awt.Dimension -import java.awt.Font -import javax.swing.BorderFactory -import javax.swing.Icon -import javax.swing.JButton -import javax.swing.JLabel -import javax.swing.JPanel -import javax.swing.JTextField -import javax.swing.SwingConstants - -class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { - private var submitClickListener: (() -> Unit)? = null - - val textField = JTextField().apply { - val editorColorsScheme = EditorColorsManager.getInstance().globalScheme - preferredSize = Dimension(550, 35) - border = IdeBorderFactory.createRoundedBorder().apply { - setColor(POPUP_BUTTON_BORDER) - } - font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) - } - - val submitButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, "Confirm") - - private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CANCEL, "Cancel").apply { - addActionListener { Disposer.dispose(parentDisposable) } - } - - private val buttonsPanel = JPanel(BorderLayout()).apply { - border = BorderFactory.createEmptyBorder(5, 5, 5, 5) -// preferredSize = Dimension(180, 40) - add(submitButton, BorderLayout.WEST) - add(cancelButton, BorderLayout.EAST) - } - - private val acceptButton = JButton("Accept").apply { - preferredSize = Dimension(80, 30) - border = IdeBorderFactory.createRoundedBorder().apply { - setColor(POPUP_BUTTON_BORDER) - } - } - private val rejectButton = JButton("Reject").apply { - preferredSize = Dimension(80, 30) - border = IdeBorderFactory.createRoundedBorder().apply { - setColor(POPUP_BUTTON_BORDER) - } - } -// private var submitClickListener: (() -> Unit)? = null - private val textLabel = JLabel("").apply { - val editorColorsScheme = EditorColorsManager.getInstance().globalScheme - font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) -// font = font.deriveFont(POPUP_INFO_TEXT_SIZE) - } - private val actionsPanel = JPanel(BorderLayout()).apply { - border = BorderFactory.createEmptyBorder(0, 20, 5, 20) - add(acceptButton, BorderLayout.WEST) - add(rejectButton, BorderLayout.EAST) - } - private val inputPanel = JPanel(BorderLayout()).apply { - border = BorderFactory.createEmptyBorder(10, 10, 5, 10) - preferredSize = Dimension(600, 50) -// maximumSize = Dimension(580, 50) - } - private val labelPanel = JPanel(BorderLayout()).apply { - border = BorderFactory.createEmptyBorder(5, 20, 5, 20) - add(textLabel, BorderLayout.CENTER) - } - - private val logoPanel = JPanel(BorderLayout()).apply { - border = BorderFactory.createEmptyBorder(5, 5, 5, 5) -// preferredSize = Dimension(180, 40) - val logoLabel = JLabel("Edit Code", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { - font = font.deriveFont(14f) - } - add(logoLabel, BorderLayout.CENTER) - } - - private val bottomPanel = JPanel(BorderLayout()).apply { - add(logoPanel, BorderLayout.WEST) - add(buttonsPanel, BorderLayout.EAST) -// preferredSize = Dimension(650, 40) - } - - override fun getPreferredSize(): Dimension { - return Dimension(600, 90) - } - - private fun createButtonWithIcon(icon: Icon, text: String): JButton { - return JButton(text).apply { - horizontalTextPosition = SwingConstants.LEFT - preferredSize = Dimension(80, 30) - setIcon(icon) - isOpaque = false - isContentAreaFilled = false - isBorderPainted = false - font = font.deriveFont(14f) - } - } - - init { - layout = BorderLayout() - inputPanel.add(textField, BorderLayout.CENTER) - inputPanel.preferredSize = Dimension(600, 50) - add(inputPanel, BorderLayout.CENTER) - add(bottomPanel, BorderLayout.SOUTH) - - submitButton.addActionListener { - submitClickListener?.invoke() - } - } - - fun setSubmitClickListener(listener: () -> Unit) { - submitClickListener = listener - } - - fun setCancelClickListener(listener: () -> Unit) { - cancelButton.addActionListener { listener.invoke() } - } - - private fun addActionListener(id: String, action: EditorActionHandler) { - val actionManager = EditorActionManager.getInstance() - val originalHandler = actionManager.getActionHandler(id) - - actionManager.setActionHandler(id, action) - val restorer = Disposable { actionManager.setActionHandler(id, originalHandler) } - Disposer.register(parentDisposable, restorer) - } - - private fun getEditorActionHandler(action: () -> Unit) : EditorActionHandler { - val handler = object : EditorActionHandler() { - override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { - action.invoke() - Disposer.dispose(parentDisposable) - } - } - return handler - } - - fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit ) { - textLabel.text = "Code diff generated. Do you want to accept it?" - textLabel.revalidate() - inputPanel.revalidate() - acceptButton.addActionListener { acceptAction.invoke() } - rejectButton.addActionListener { rejectAction.invoke() } - add(actionsPanel, BorderLayout.SOUTH) - val enterHandler = getEditorActionHandler(acceptAction) - addActionListener(IdeActions.ACTION_EDITOR_ENTER, enterHandler) - - val escapeHandler = getEditorActionHandler(rejectAction) - addActionListener(IdeActions.ACTION_EDITOR_ESCAPE, escapeHandler) - revalidate() - } - - fun setLabel(text: String) { - textLabel.text = text - textLabel.revalidate() - remove(inputPanel) - remove(bottomPanel) - add(labelPanel) - revalidate() - } -} +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.editor.actionSystem.EditorActionManager import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.util.Disposer import com.intellij.ui.IdeBorderFactory import icons.AwsIcons import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER import java.awt.BorderLayout import java.awt.Dimension import java.awt.Font import java.awt.event.ActionEvent import java.awt.event.KeyEvent import javax.swing.AbstractAction import javax.swing.BorderFactory import javax.swing.Icon import javax.swing.JButton import javax.swing.JComponent import javax.swing.JLabel import javax.swing.JPanel import javax.swing.JTextField import javax.swing.KeyStroke import javax.swing.SwingConstants class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { private var submitClickListener: (() -> Unit)? = null val textField = JTextField().apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme preferredSize = Dimension(550, 35) border = IdeBorderFactory.createRoundedBorder().apply { setColor(POPUP_BUTTON_BORDER) } font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } val emptyTextField = JTextField().apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme preferredSize = Dimension(550, 35) border = IdeBorderFactory.createRoundedBorder().apply { setColor(POPUP_BUTTON_BORDER) } font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } val submitButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, "Confirm") private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CANCEL, "Cancel").apply { addActionListener { Disposer.dispose(parentDisposable) } } private val buttonsPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) // preferredSize = Dimension(180, 40) add(submitButton, BorderLayout.WEST) add(cancelButton, BorderLayout.EAST) } private val acceptButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, "Accept") private val rejectButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.REJECT, "Reject") // private var submitClickListener: (() -> Unit)? = null private val textLabel = JLabel("Edit Code", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(14f) } private val errorLabel = JLabel("", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(14f) } private val actionsPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(0, 20, 5, 20) add(acceptButton, BorderLayout.WEST) add(rejectButton, BorderLayout.EAST) } private val inputPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(10, 10, 5, 10) preferredSize = Dimension(600, 50) // maximumSize = Dimension(580, 50) } private val labelPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 20, 5, 20) add(textLabel, BorderLayout.CENTER) } private val logoPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) // preferredSize = Dimension(180, 40) // val logoLabel = JLabel("Edit Code", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { // font = font.deriveFont(14f) // } add(textLabel, BorderLayout.CENTER) } private val bottomPanel = JPanel(BorderLayout()).apply { add(logoPanel, BorderLayout.WEST) add(buttonsPanel, BorderLayout.EAST) // preferredSize = Dimension(650, 40) } override fun getPreferredSize(): Dimension { return Dimension(600, 90) } private fun createButtonWithIcon(icon: Icon, text: String): JButton { return JButton(text).apply { horizontalTextPosition = SwingConstants.LEFT preferredSize = Dimension(80, 30) setIcon(icon) isOpaque = false isContentAreaFilled = false isBorderPainted = false font = font.deriveFont(14f) } } init { layout = BorderLayout() inputPanel.add(textField, BorderLayout.CENTER) inputPanel.preferredSize = Dimension(600, 50) add(inputPanel, BorderLayout.CENTER) add(bottomPanel, BorderLayout.SOUTH) submitButton.addActionListener { submitClickListener?.invoke() } } fun setSubmitClickListener(listener: () -> Unit) { submitClickListener = listener } fun setCancelClickListener(listener: () -> Unit) { cancelButton.addActionListener { listener.invoke() } } private fun addActionListener(id: String, action: EditorActionHandler) { val actionManager = EditorActionManager.getInstance() val originalHandler = actionManager.getActionHandler(id) actionManager.setActionHandler(id, action) val restorer = Disposable { actionManager.setActionHandler(id, originalHandler) } Disposer.register(parentDisposable, restorer) } private fun getEditorActionHandler(action: () -> Unit) : EditorActionHandler { val handler = object : EditorActionHandler() { override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { action.invoke() Disposer.dispose(parentDisposable) } } return handler } fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit ) { textLabel.text = "Edit Code" textLabel.revalidate() logoPanel.revalidate() emptyTextField.text = textField.text emptyTextField.isEnabled = false inputPanel.remove(textField) inputPanel.add(emptyTextField, BorderLayout.CENTER) inputPanel.revalidate() buttonsPanel.remove(submitButton) buttonsPanel.remove(cancelButton) buttonsPanel.add(acceptButton, BorderLayout.WEST) buttonsPanel.add(rejectButton, BorderLayout.EAST) acceptButton.addActionListener { acceptAction.invoke() } rejectButton.addActionListener { rejectAction.invoke() } buttonsPanel.revalidate() val enterHandler = getEditorActionHandler(acceptAction) addActionListener(IdeActions.ACTION_EDITOR_ENTER, enterHandler) val escapeHandler = getEditorActionHandler(rejectAction) addActionListener(IdeActions.ACTION_EDITOR_ESCAPE, escapeHandler) revalidate() } fun setErrorMessage(text: String) { errorLabel.text = text errorLabel.revalidate() add(errorLabel, BorderLayout.CENTER) remove(inputPanel) remove(bottomPanel) revalidate() } fun setLabel(text: String) { textLabel.text = text textLabel.revalidate() logoPanel.revalidate() textField.isEnabled = false inputPanel.revalidate() revalidate() } } \ No newline at end of file diff --git a/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_reject.svg b/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_reject.svg new file mode 100644 index 00000000000..1f583d66e03 --- /dev/null +++ b/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_reject.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/core/jetbrains-community/src/icons/AwsIcons.kt b/plugins/core/jetbrains-community/src/icons/AwsIcons.kt index 7cdae73ca2f..8f55089f67c 100644 --- a/plugins/core/jetbrains-community/src/icons/AwsIcons.kt +++ b/plugins/core/jetbrains-community/src/icons/AwsIcons.kt @@ -131,6 +131,7 @@ object AwsIcons { object InlineChat { @JvmField val CONFIRM = load("/icons/resources/inlinechat/amazonq_inline_chat_confirm_icon.svg") @JvmField val CANCEL = load("/icons/resources/inlinechat/amazonq_inline_chat_cancel_icon.svg") + @JvmField val REJECT = load("/icons/resources/inlinechat/amazonq_inline_chat_reject.svg") } } From 69ce914d63dde772c686af52be261b69057787c3 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Wed, 16 Oct 2024 13:53:44 -0700 Subject: [PATCH 12/39] update to latest prompt --- .../cwc/inline/InlineChatController.kt | 58 +++++++++++++------ .../cwc/inline/InlineChatPopupPanel.kt | 2 +- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 2b37f2729ff..eb017451e0c 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -313,7 +313,7 @@ class InlineChatController( if(event.message?.isNotEmpty() == true) { val codeBlocks = getCodeBlocks(event.message) if(codeBlocks.isEmpty()) { - logger.info { "No code block found in inline chat response with requestId: ${event.messageId} \nresponse: ${event.message} " } + logger.info { "No code block found in inline chat response with requestId: ${event.messageId}" } return } val recommendation = unescape(extractContentAfterFirstNewline(codeBlocks.first())) @@ -343,6 +343,7 @@ class InlineChatController( recordInlineChatTelemetry(InlineChatUserDecision.DISMISS) return } + var isAllEqual = true diff.forEach { row -> when (row.tag) { DiffRow.Tag.EQUAL -> { @@ -350,6 +351,7 @@ class InlineChatController( insertLine++ } DiffRow.Tag.DELETE, DiffRow.Tag.CHANGE -> { + isAllEqual = false try { if (row.tag == DiffRow.Tag.CHANGE && row.newLine.trimIndent() == row.oldLine?.trimIndent()) return showCodeChangeInEditor(row, currentDocumentLine, editor) @@ -367,6 +369,7 @@ class InlineChatController( deletedCharsCount += row.oldLine?.length?: 0 } DiffRow.Tag.INSERT -> { + isAllEqual = false try { showCodeChangeInEditor(row, insertLine, editor) } catch (e: Exception) { @@ -378,6 +381,9 @@ class InlineChatController( } } } + if (isAllEqual) { + throw Exception("No recommendation provided. Please try again with a different question.") + } isInProgress.set(false) shouldShowActions.set(true) @@ -570,29 +576,47 @@ class InlineChatController( var firstResponseLatency = 0.0 val messages = mutableListOf() val triggerId = UUID.randomUUID().toString() - - val languageExtractor = LanguageExtractor() val intentRecognizer = UserIntentRecognizer() - val language = editor.project?.let { languageExtractor.extractProgrammingLanguageNameFromCurrentFile(editor, it) } ?: "" - var baseRules = "- Plan out the changes step-by-step before making them, this should be brief and not include any code\n" + - "- Do not explain the code after, the plan and code are sufficient\n" +// val languageExtractor = LanguageExtractor() +// val language = editor.project?.let { languageExtractor.extractProgrammingLanguageNameFromCurrentFile(editor, it) } ?: "" +// This is temporary. TODO: remove this after prompt added on service side var prompt = "" if (selectedCode.isNotBlank()) { - if (language.isNotEmpty()) { - baseRules += "- Ensure the code is written in $language\n" - } - prompt = "Rules for writing code:\n" + baseRules + - "Write a code snipped based on the following:\n" + message + prompt = "You are a code transformation assistant. Your task is to modify a selection of lines from a given code file according to a specific instruction.\n"+ + "Follow these steps carefully:\n" + + "- You will be given some selected code from a file to be transformed, enclosed in XML tags\n" + + "- You will receive an instruction for how to transform the selected code, enclosed in XML tags\n" + + "- You will be given the contents of that same file as context, enclosed in XML tags\n" + "- Your task is to:\n" + + "- Apply the transformation instruction to the selected code\n" + + "- Ensure that the transformation is applied correctly and consistently\n" + + "- Reuse existing functions and other code from the context wherever possible\n" + + "- Important rules to follow:\n" + + "- Maintain the original indentation of the selected code\n" + + "- If the instruction asks to provide explanations or answer questions about the code, add these as new comment lines above the relevant lines in the code; do not change the code lines themselves\n" + + "- If the instruction is unclear or cannot be applied, do not make any changes to the code\n" + + "- After performing the transformation, return the transformed code. If the transformation generates new code but does not modify the selected code, be sure to include the selected code in your response.\n" } else { - baseRules += "- If the query is a question only attempt to add comments to the code that answer it\n" + - "- Make sure to preserve the original indentation, code formatting, tab size and structure as much as possible\n" + - "- Do not change the code more than required, try to maintain variables, function names, and other identifiers" - prompt = "```$language\n$selectedCode\n```\n" + - "Rules for rewriting the code:\n" + baseRules + - "Rewrite the above code to do the following:\n" + message + prompt = "You are a coding assistant. Your task is to generate code according to an specific instruction.\n" + + "Follow these steps carefully:\n" + + "- You will receive an instruction for how to generate code, enclosed in XML tags\n" + + "- You will be given the contents of that same file as context, enclosed in XML tags\n" + + "- Your task is to:\n" + + "- Generate code according to the instruction\n" + + "- Reuse existing functions and other code from the context wherever possible\n" + + "- Important rules to follow:\n" + + "- If the instruction asks to provide explanations or answer questions about the code, add these as new comment lines above the relevant lines in the code; do not change the code lines themselves\n" + + "- If the instruction is unclear or cannot be applied, do not make any changes to the code\n" + + "- After generating the code, return ONLY the new code you generated; do not include any existing lines of code from the context.\n" } + prompt += "- Respond with the code in markdown format. Do not include any explanations or other text outside of the code itself\n" + + "Remember, your output should contain nothing but the transformed code or code comments.\n" + if (selectedCode.isNotBlank()) { prompt += "$selectedCode\n" } + prompt += "$message\n" + prompt += "${editor.document.text.take(8000)}" + logger.info { "Inline chat prompt: $prompt" } val contextExtractor = ActiveFileContextExtractor.create(fqnWebviewAdapter = null, project = project) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index 95bd538d615..316858bd1af 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -1 +1 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.editor.actionSystem.EditorActionManager import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.util.Disposer import com.intellij.ui.IdeBorderFactory import icons.AwsIcons import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER import java.awt.BorderLayout import java.awt.Dimension import java.awt.Font import java.awt.event.ActionEvent import java.awt.event.KeyEvent import javax.swing.AbstractAction import javax.swing.BorderFactory import javax.swing.Icon import javax.swing.JButton import javax.swing.JComponent import javax.swing.JLabel import javax.swing.JPanel import javax.swing.JTextField import javax.swing.KeyStroke import javax.swing.SwingConstants class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { private var submitClickListener: (() -> Unit)? = null val textField = JTextField().apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme preferredSize = Dimension(550, 35) border = IdeBorderFactory.createRoundedBorder().apply { setColor(POPUP_BUTTON_BORDER) } font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } val emptyTextField = JTextField().apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme preferredSize = Dimension(550, 35) border = IdeBorderFactory.createRoundedBorder().apply { setColor(POPUP_BUTTON_BORDER) } font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } val submitButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, "Confirm") private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CANCEL, "Cancel").apply { addActionListener { Disposer.dispose(parentDisposable) } } private val buttonsPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) // preferredSize = Dimension(180, 40) add(submitButton, BorderLayout.WEST) add(cancelButton, BorderLayout.EAST) } private val acceptButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, "Accept") private val rejectButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.REJECT, "Reject") // private var submitClickListener: (() -> Unit)? = null private val textLabel = JLabel("Edit Code", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(14f) } private val errorLabel = JLabel("", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(14f) } private val actionsPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(0, 20, 5, 20) add(acceptButton, BorderLayout.WEST) add(rejectButton, BorderLayout.EAST) } private val inputPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(10, 10, 5, 10) preferredSize = Dimension(600, 50) // maximumSize = Dimension(580, 50) } private val labelPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 20, 5, 20) add(textLabel, BorderLayout.CENTER) } private val logoPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) // preferredSize = Dimension(180, 40) // val logoLabel = JLabel("Edit Code", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { // font = font.deriveFont(14f) // } add(textLabel, BorderLayout.CENTER) } private val bottomPanel = JPanel(BorderLayout()).apply { add(logoPanel, BorderLayout.WEST) add(buttonsPanel, BorderLayout.EAST) // preferredSize = Dimension(650, 40) } override fun getPreferredSize(): Dimension { return Dimension(600, 90) } private fun createButtonWithIcon(icon: Icon, text: String): JButton { return JButton(text).apply { horizontalTextPosition = SwingConstants.LEFT preferredSize = Dimension(80, 30) setIcon(icon) isOpaque = false isContentAreaFilled = false isBorderPainted = false font = font.deriveFont(14f) } } init { layout = BorderLayout() inputPanel.add(textField, BorderLayout.CENTER) inputPanel.preferredSize = Dimension(600, 50) add(inputPanel, BorderLayout.CENTER) add(bottomPanel, BorderLayout.SOUTH) submitButton.addActionListener { submitClickListener?.invoke() } } fun setSubmitClickListener(listener: () -> Unit) { submitClickListener = listener } fun setCancelClickListener(listener: () -> Unit) { cancelButton.addActionListener { listener.invoke() } } private fun addActionListener(id: String, action: EditorActionHandler) { val actionManager = EditorActionManager.getInstance() val originalHandler = actionManager.getActionHandler(id) actionManager.setActionHandler(id, action) val restorer = Disposable { actionManager.setActionHandler(id, originalHandler) } Disposer.register(parentDisposable, restorer) } private fun getEditorActionHandler(action: () -> Unit) : EditorActionHandler { val handler = object : EditorActionHandler() { override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { action.invoke() Disposer.dispose(parentDisposable) } } return handler } fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit ) { textLabel.text = "Edit Code" textLabel.revalidate() logoPanel.revalidate() emptyTextField.text = textField.text emptyTextField.isEnabled = false inputPanel.remove(textField) inputPanel.add(emptyTextField, BorderLayout.CENTER) inputPanel.revalidate() buttonsPanel.remove(submitButton) buttonsPanel.remove(cancelButton) buttonsPanel.add(acceptButton, BorderLayout.WEST) buttonsPanel.add(rejectButton, BorderLayout.EAST) acceptButton.addActionListener { acceptAction.invoke() } rejectButton.addActionListener { rejectAction.invoke() } buttonsPanel.revalidate() val enterHandler = getEditorActionHandler(acceptAction) addActionListener(IdeActions.ACTION_EDITOR_ENTER, enterHandler) val escapeHandler = getEditorActionHandler(rejectAction) addActionListener(IdeActions.ACTION_EDITOR_ESCAPE, escapeHandler) revalidate() } fun setErrorMessage(text: String) { errorLabel.text = text errorLabel.revalidate() add(errorLabel, BorderLayout.CENTER) remove(inputPanel) remove(bottomPanel) revalidate() } fun setLabel(text: String) { textLabel.text = text textLabel.revalidate() logoPanel.revalidate() textField.isEnabled = false inputPanel.revalidate() revalidate() } } \ No newline at end of file +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.editor.actionSystem.EditorActionManager import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.util.Disposer import com.intellij.ui.IdeBorderFactory import icons.AwsIcons import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER import java.awt.BorderLayout import java.awt.Dimension import java.awt.Font import java.awt.event.ActionEvent import java.awt.event.KeyEvent import javax.swing.AbstractAction import javax.swing.BorderFactory import javax.swing.Icon import javax.swing.JButton import javax.swing.JComponent import javax.swing.JLabel import javax.swing.JPanel import javax.swing.JTextField import javax.swing.KeyStroke import javax.swing.SwingConstants class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { private var submitClickListener: (() -> Unit)? = null val textField = JTextField().apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme preferredSize = Dimension(550, 35) border = IdeBorderFactory.createRoundedBorder().apply { setColor(POPUP_BUTTON_BORDER) } font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } private val emptyTextField = JTextField().apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme preferredSize = Dimension(550, 35) border = IdeBorderFactory.createRoundedBorder().apply { setColor(POPUP_BUTTON_BORDER) } font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } val submitButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, "Confirm") private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CANCEL, "Cancel").apply { addActionListener { Disposer.dispose(parentDisposable) } } private val buttonsPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) // preferredSize = Dimension(180, 40) add(submitButton, BorderLayout.WEST) add(cancelButton, BorderLayout.EAST) } private val acceptButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, "Accept") private val rejectButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.REJECT, "Reject") // private var submitClickListener: (() -> Unit)? = null private val textLabel = JLabel("Edit Code", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(14f) } private val errorLabel = JLabel("", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(14f) } private val actionsPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(0, 20, 5, 20) add(acceptButton, BorderLayout.WEST) add(rejectButton, BorderLayout.EAST) } private val inputPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(10, 10, 5, 10) preferredSize = Dimension(600, 50) // maximumSize = Dimension(580, 50) } private val labelPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 20, 5, 20) add(textLabel, BorderLayout.CENTER) } private val logoPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) // preferredSize = Dimension(180, 40) // val logoLabel = JLabel("Edit Code", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { // font = font.deriveFont(14f) // } add(textLabel, BorderLayout.CENTER) } private val bottomPanel = JPanel(BorderLayout()).apply { add(logoPanel, BorderLayout.WEST) add(buttonsPanel, BorderLayout.EAST) // preferredSize = Dimension(650, 40) } override fun getPreferredSize(): Dimension { return Dimension(600, 90) } private fun createButtonWithIcon(icon: Icon, text: String): JButton { return JButton(text).apply { horizontalTextPosition = SwingConstants.LEFT preferredSize = Dimension(80, 30) setIcon(icon) isOpaque = false isContentAreaFilled = false isBorderPainted = false font = font.deriveFont(14f) } } init { layout = BorderLayout() inputPanel.add(textField, BorderLayout.CENTER) inputPanel.preferredSize = Dimension(600, 50) add(inputPanel, BorderLayout.CENTER) add(bottomPanel, BorderLayout.SOUTH) submitButton.addActionListener { submitClickListener?.invoke() } } fun setSubmitClickListener(listener: () -> Unit) { submitClickListener = listener } fun setCancelClickListener(listener: () -> Unit) { cancelButton.addActionListener { listener.invoke() } } private fun addActionListener(id: String, action: EditorActionHandler) { val actionManager = EditorActionManager.getInstance() val originalHandler = actionManager.getActionHandler(id) actionManager.setActionHandler(id, action) val restorer = Disposable { actionManager.setActionHandler(id, originalHandler) } Disposer.register(parentDisposable, restorer) } private fun getEditorActionHandler(action: () -> Unit) : EditorActionHandler { val handler = object : EditorActionHandler() { override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { action.invoke() Disposer.dispose(parentDisposable) } } return handler } fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit ) { textLabel.text = "Edit Code" textLabel.revalidate() logoPanel.revalidate() emptyTextField.text = textField.text emptyTextField.isEnabled = false inputPanel.remove(textField) inputPanel.add(emptyTextField, BorderLayout.CENTER) inputPanel.revalidate() buttonsPanel.remove(submitButton) buttonsPanel.remove(cancelButton) buttonsPanel.add(acceptButton, BorderLayout.WEST) buttonsPanel.add(rejectButton, BorderLayout.EAST) acceptButton.addActionListener { acceptAction.invoke() } rejectButton.addActionListener { rejectAction.invoke() } buttonsPanel.revalidate() val enterHandler = getEditorActionHandler(acceptAction) addActionListener(IdeActions.ACTION_EDITOR_ENTER, enterHandler) val escapeHandler = getEditorActionHandler(rejectAction) addActionListener(IdeActions.ACTION_EDITOR_ESCAPE, escapeHandler) revalidate() } fun setErrorMessage(text: String) { errorLabel.text = text errorLabel.revalidate() add(errorLabel, BorderLayout.CENTER) remove(inputPanel) remove(bottomPanel) revalidate() } fun setLabel(text: String) { textLabel.text = text textLabel.revalidate() logoPanel.revalidate() textField.isEnabled = false inputPanel.revalidate() revalidate() } } \ No newline at end of file From 15081a2f2c69aab6b34bea0434566db131f69763 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Wed, 16 Oct 2024 14:06:37 -0700 Subject: [PATCH 13/39] cleanup --- .../cwc/inline/InlineChatController.kt | 18 +++++------------- .../cwc/inline/InlineChatGutterIconRenderer.kt | 2 +- .../cwc/inline/InlineChatPopupFactory.kt | 4 ---- .../cwc/inline/InlineChatPopupPanel.kt | 2 +- 4 files changed, 7 insertions(+), 19 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index eb017451e0c..f3b1e82018d 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -14,7 +14,6 @@ import com.intellij.openapi.application.runInEdt import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.command.UndoConfirmationPolicy import com.intellij.openapi.command.WriteCommandAction -import com.intellij.openapi.command.undo.BasicUndoableAction import com.intellij.openapi.command.undo.UndoManager import com.intellij.openapi.components.Service import com.intellij.openapi.components.service @@ -32,15 +31,13 @@ import com.intellij.openapi.ui.popup.JBPopupListener import com.intellij.openapi.ui.popup.LightweightWindowEvent import com.intellij.openapi.util.Disposer import com.intellij.openapi.wm.ToolWindowManager -import com.intellij.psi.codeStyle.CodeStyleManager import com.intellij.ui.JBColor import com.intellij.ui.jcef.JBCefApp import com.jetbrains.rd.util.AtomicInteger +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch @@ -54,25 +51,19 @@ import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.info import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.core.coroutines.EDT -import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope import software.aws.toolkits.jetbrains.core.gettingstarted.requestCredentialsForQ import software.aws.toolkits.jetbrains.core.webview.BrowserState import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AMAZON_Q_WINDOW_ID -import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererLanguageManager import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerType import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController import software.aws.toolkits.jetbrains.services.cwc.controller.chat.messenger.ChatPromptHandler import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.TelemetryHelper import software.aws.toolkits.jetbrains.services.cwc.controller.chat.userIntent.UserIntentRecognizer -import software.aws.toolkits.jetbrains.services.cwc.editor.context.ActiveFileContext import software.aws.toolkits.jetbrains.services.cwc.editor.context.ActiveFileContextExtractor import software.aws.toolkits.jetbrains.services.cwc.editor.context.ExtractionTriggerType -import software.aws.toolkits.jetbrains.services.cwc.editor.context.file.FileContextExtractor -import software.aws.toolkits.jetbrains.services.cwc.editor.context.file.util.LanguageExtractor -import software.aws.toolkits.jetbrains.services.cwc.editor.context.project.ProjectContextController import software.aws.toolkits.jetbrains.services.cwc.inline.listeners.InlineChatFileListener import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessage import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType @@ -84,10 +75,11 @@ import java.util.concurrent.atomic.AtomicBoolean @Service(Service.Level.PROJECT) class InlineChatController( - private val project: Project + private val project: Project, + private val scope: CoroutineScope ) : Disposable { private var currentPopup: JBPopup? = null - private val scope = disposableCoroutineScope(this) +// private val scope = disposableCoroutineScope(this) private var rangeHighlighter: RangeHighlighter? = null private val partialUndoActions = Stack<() -> Unit>() private val partialAcceptActions = Stack<() -> Unit>() @@ -220,7 +212,7 @@ class InlineChatController( fun initPopup (editor: Editor) { currentPopup?.let { Disposer.dispose(it) } currentPopup = InlineChatPopupFactory(acceptHandler = diffAcceptHandler, rejectHandler = diffRejectHandler, editor = editor, - telemetryHelper = telemetryHelper, submitHandler = popupSubmitHandler, cancelHandler = popupCancelHandler).createPopup(scope) + submitHandler = popupSubmitHandler, cancelHandler = popupCancelHandler).createPopup(scope) addPopupListeners(currentPopup!!) Disposer.register(this, currentPopup!!) isPopupAborted.set(true) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt index 76335577b97..ec629a7dd2c 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt @@ -22,7 +22,7 @@ class InlineChatGutterIconRenderer (private val icon: Icon) : GutterIconRenderer override fun getIcon(): Icon = icon - override fun getTooltipText(): String = "Amazon Q: Edit ⌘ + I" + override fun getTooltipText(): String = "Amazon Q" override fun isNavigateAction(): Boolean = false diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index 955483ac52f..957b2c5b9cf 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -13,11 +13,9 @@ import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.util.TextRange import com.intellij.ui.IdeBorderFactory import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER -import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.TelemetryHelper class InlineChatPopupFactory( @@ -26,8 +24,6 @@ class InlineChatPopupFactory( private val acceptHandler: () -> Unit, private val rejectHandler: () -> Unit, private val cancelHandler: () -> Unit, - private val telemetryHelper: TelemetryHelper, -// private val scope: CoroutineScope ) : Disposable { private fun getSelectedText(editor: Editor): String { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index 316858bd1af..499c9049f56 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -1 +1 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.editor.actionSystem.EditorActionManager import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.util.Disposer import com.intellij.ui.IdeBorderFactory import icons.AwsIcons import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER import java.awt.BorderLayout import java.awt.Dimension import java.awt.Font import java.awt.event.ActionEvent import java.awt.event.KeyEvent import javax.swing.AbstractAction import javax.swing.BorderFactory import javax.swing.Icon import javax.swing.JButton import javax.swing.JComponent import javax.swing.JLabel import javax.swing.JPanel import javax.swing.JTextField import javax.swing.KeyStroke import javax.swing.SwingConstants class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { private var submitClickListener: (() -> Unit)? = null val textField = JTextField().apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme preferredSize = Dimension(550, 35) border = IdeBorderFactory.createRoundedBorder().apply { setColor(POPUP_BUTTON_BORDER) } font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } private val emptyTextField = JTextField().apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme preferredSize = Dimension(550, 35) border = IdeBorderFactory.createRoundedBorder().apply { setColor(POPUP_BUTTON_BORDER) } font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } val submitButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, "Confirm") private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CANCEL, "Cancel").apply { addActionListener { Disposer.dispose(parentDisposable) } } private val buttonsPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) // preferredSize = Dimension(180, 40) add(submitButton, BorderLayout.WEST) add(cancelButton, BorderLayout.EAST) } private val acceptButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, "Accept") private val rejectButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.REJECT, "Reject") // private var submitClickListener: (() -> Unit)? = null private val textLabel = JLabel("Edit Code", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(14f) } private val errorLabel = JLabel("", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(14f) } private val actionsPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(0, 20, 5, 20) add(acceptButton, BorderLayout.WEST) add(rejectButton, BorderLayout.EAST) } private val inputPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(10, 10, 5, 10) preferredSize = Dimension(600, 50) // maximumSize = Dimension(580, 50) } private val labelPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 20, 5, 20) add(textLabel, BorderLayout.CENTER) } private val logoPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) // preferredSize = Dimension(180, 40) // val logoLabel = JLabel("Edit Code", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { // font = font.deriveFont(14f) // } add(textLabel, BorderLayout.CENTER) } private val bottomPanel = JPanel(BorderLayout()).apply { add(logoPanel, BorderLayout.WEST) add(buttonsPanel, BorderLayout.EAST) // preferredSize = Dimension(650, 40) } override fun getPreferredSize(): Dimension { return Dimension(600, 90) } private fun createButtonWithIcon(icon: Icon, text: String): JButton { return JButton(text).apply { horizontalTextPosition = SwingConstants.LEFT preferredSize = Dimension(80, 30) setIcon(icon) isOpaque = false isContentAreaFilled = false isBorderPainted = false font = font.deriveFont(14f) } } init { layout = BorderLayout() inputPanel.add(textField, BorderLayout.CENTER) inputPanel.preferredSize = Dimension(600, 50) add(inputPanel, BorderLayout.CENTER) add(bottomPanel, BorderLayout.SOUTH) submitButton.addActionListener { submitClickListener?.invoke() } } fun setSubmitClickListener(listener: () -> Unit) { submitClickListener = listener } fun setCancelClickListener(listener: () -> Unit) { cancelButton.addActionListener { listener.invoke() } } private fun addActionListener(id: String, action: EditorActionHandler) { val actionManager = EditorActionManager.getInstance() val originalHandler = actionManager.getActionHandler(id) actionManager.setActionHandler(id, action) val restorer = Disposable { actionManager.setActionHandler(id, originalHandler) } Disposer.register(parentDisposable, restorer) } private fun getEditorActionHandler(action: () -> Unit) : EditorActionHandler { val handler = object : EditorActionHandler() { override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { action.invoke() Disposer.dispose(parentDisposable) } } return handler } fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit ) { textLabel.text = "Edit Code" textLabel.revalidate() logoPanel.revalidate() emptyTextField.text = textField.text emptyTextField.isEnabled = false inputPanel.remove(textField) inputPanel.add(emptyTextField, BorderLayout.CENTER) inputPanel.revalidate() buttonsPanel.remove(submitButton) buttonsPanel.remove(cancelButton) buttonsPanel.add(acceptButton, BorderLayout.WEST) buttonsPanel.add(rejectButton, BorderLayout.EAST) acceptButton.addActionListener { acceptAction.invoke() } rejectButton.addActionListener { rejectAction.invoke() } buttonsPanel.revalidate() val enterHandler = getEditorActionHandler(acceptAction) addActionListener(IdeActions.ACTION_EDITOR_ENTER, enterHandler) val escapeHandler = getEditorActionHandler(rejectAction) addActionListener(IdeActions.ACTION_EDITOR_ESCAPE, escapeHandler) revalidate() } fun setErrorMessage(text: String) { errorLabel.text = text errorLabel.revalidate() add(errorLabel, BorderLayout.CENTER) remove(inputPanel) remove(bottomPanel) revalidate() } fun setLabel(text: String) { textLabel.text = text textLabel.revalidate() logoPanel.revalidate() textField.isEnabled = false inputPanel.revalidate() revalidate() } } \ No newline at end of file +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.editor.actionSystem.EditorActionManager import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.util.Disposer import com.intellij.ui.IdeBorderFactory import icons.AwsIcons import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER import java.awt.BorderLayout import java.awt.Dimension import java.awt.Font import javax.swing.BorderFactory import javax.swing.Icon import javax.swing.JButton import javax.swing.JLabel import javax.swing.JPanel import javax.swing.JTextField import javax.swing.SwingConstants class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { private var submitClickListener: (() -> Unit)? = null val textField = createTextField() val submitButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, "Confirm") private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CANCEL, "Cancel").apply { addActionListener { Disposer.dispose(parentDisposable) } } private val buttonsPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) add(submitButton, BorderLayout.WEST) add(cancelButton, BorderLayout.EAST) } private val acceptButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, "Accept") private val rejectButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.REJECT, "Reject") private val textLabel = JLabel("Edit Code", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(14f) } private val errorLabel = JLabel("", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(14f) } private val inputPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(10, 10, 5, 10) preferredSize = Dimension(600, 50) } private val logoPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) add(textLabel, BorderLayout.CENTER) } private val bottomPanel = JPanel(BorderLayout()).apply { add(logoPanel, BorderLayout.WEST) add(buttonsPanel, BorderLayout.EAST) } override fun getPreferredSize(): Dimension { return Dimension(600, 90) } private fun createTextField() : JTextField = JTextField().apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme preferredSize = Dimension(550, 35) border = IdeBorderFactory.createRoundedBorder().apply { setColor(POPUP_BUTTON_BORDER) } font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } private fun createButtonWithIcon(icon: Icon, text: String): JButton { return JButton(text).apply { horizontalTextPosition = SwingConstants.LEFT preferredSize = Dimension(80, 30) setIcon(icon) isOpaque = false isContentAreaFilled = false isBorderPainted = false font = font.deriveFont(14f) } } init { layout = BorderLayout() inputPanel.add(textField, BorderLayout.CENTER) inputPanel.preferredSize = Dimension(600, 50) add(inputPanel, BorderLayout.CENTER) add(bottomPanel, BorderLayout.SOUTH) submitButton.addActionListener { submitClickListener?.invoke() } } fun setSubmitClickListener(listener: () -> Unit) { submitClickListener = listener } private fun addActionListener(id: String, action: EditorActionHandler) { val actionManager = EditorActionManager.getInstance() val originalHandler = actionManager.getActionHandler(id) actionManager.setActionHandler(id, action) val restorer = Disposable { actionManager.setActionHandler(id, originalHandler) } Disposer.register(parentDisposable, restorer) } private fun getEditorActionHandler(action: () -> Unit) : EditorActionHandler { val handler = object : EditorActionHandler() { override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { action.invoke() Disposer.dispose(parentDisposable) } } return handler } fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit ) { textLabel.text = "Edit Code" // this is a workaround somehow the textField will interfere with the enter handler val emptyTextField = createTextField() emptyTextField.text = textField.text emptyTextField.isEnabled = false inputPanel.remove(textField) inputPanel.add(emptyTextField, BorderLayout.CENTER) buttonsPanel.remove(submitButton) buttonsPanel.remove(cancelButton) buttonsPanel.add(acceptButton, BorderLayout.WEST) buttonsPanel.add(rejectButton, BorderLayout.EAST) acceptButton.addActionListener { acceptAction.invoke() } rejectButton.addActionListener { rejectAction.invoke() } val enterHandler = getEditorActionHandler(acceptAction) addActionListener(IdeActions.ACTION_EDITOR_ENTER, enterHandler) val escapeHandler = getEditorActionHandler(rejectAction) addActionListener(IdeActions.ACTION_EDITOR_ESCAPE, escapeHandler) revalidate() } fun setErrorMessage(text: String) { errorLabel.text = text add(errorLabel, BorderLayout.CENTER) remove(inputPanel) remove(bottomPanel) revalidate() } fun setLabel(text: String) { textLabel.text = text textField.isEnabled = false revalidate() } } \ No newline at end of file From add98995457a4f852a5785ba4235c623adc24e11 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Wed, 16 Oct 2024 16:45:03 -0700 Subject: [PATCH 14/39] better popup position --- .../cwc/inline/InlineChatEditorHint.kt | 14 ++++---- .../inline/InlineChatGutterIconRenderer.kt | 3 +- .../cwc/inline/InlineChatPopupFactory.kt | 36 +++++++++++++++---- .../cwc/inline/InlineChatPopupPanel.kt | 2 +- .../inlineChat/InlineChatControllerTest.kt | 4 +++ .../resources/AmazonQBundle.properties | 10 ++++++ 6 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/inlineChat/InlineChatControllerTest.kt diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt index 5a3ee9ab410..4f51e0ca3a8 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt @@ -11,6 +11,7 @@ import com.intellij.ui.LightweightHint import com.intellij.ui.SimpleColoredText import com.intellij.ui.SimpleTextAttributes import icons.AwsIcons +import software.aws.toolkits.resources.AmazonQBundle.message import java.awt.BorderLayout import java.awt.Point import javax.swing.JPanel @@ -18,6 +19,7 @@ import javax.swing.JPanel class InlineChatEditorHint { private val hint = createHint() + private val HINT_BUFFER = 50 private fun getHintLocation (editor: Editor): Point { val selectionModel = editor.selectionModel @@ -29,12 +31,12 @@ class InlineChatEditorHint { val editorContentLocation = editor.contentComponent.locationOnScreen val position = Point( editorContentLocation.x + xyPosition.x, - editorLocation.y + xyPosition.y - editor.scrollingModel.verticalScrollOffset - 50) + editorLocation.y + xyPosition.y - editor.scrollingModel.verticalScrollOffset - HINT_BUFFER) val visibleArea = editor.scrollingModel.visibleArea - val adjustedX = (position.x ).coerceAtMost(visibleArea.x + visibleArea.width - 50) - val adjustedY = (position.y ).coerceAtMost(visibleArea.y + visibleArea.height - 50) + val adjustedX = (position.x ).coerceAtMost(visibleArea.x + visibleArea.width - HINT_BUFFER) + val adjustedY = (position.y ).coerceAtMost(visibleArea.y + visibleArea.height - HINT_BUFFER) val adjustedPosition = Point(adjustedX, adjustedY) return adjustedPosition @@ -47,7 +49,7 @@ class InlineChatEditorHint { component.isIconOnTheRight = false; component.icon = icon val coloredText = - SimpleColoredText("Edit", SimpleTextAttributes.REGULAR_ATTRIBUTES) + SimpleColoredText(message("amazonqInlineChat.hint.edit"), SimpleTextAttributes.REGULAR_ATTRIBUTES) coloredText.appendToComponent(component) val shortcutComponent = HintUtil.createInformationComponent() @@ -57,7 +59,7 @@ class InlineChatEditorHint { shortcutComponent.icon = shortCutIcon } else { val shortcutText = - SimpleColoredText("(Ctrl + I)", SimpleTextAttributes.REGULAR_ATTRIBUTES) + SimpleColoredText(message("amazonqInlineChat.hint.windows.shortCut"), SimpleTextAttributes.REGULAR_ATTRIBUTES) shortcutText.appendToComponent(shortcutComponent) } @@ -79,7 +81,7 @@ class InlineChatEditorHint { hint, editor, location, HintManager.HIDE_BY_TEXT_CHANGE or HintManager.HIDE_BY_SCROLLING, 0, false, - HintManagerImpl.createHintHint(editor, location, hint!!, HintManager.RIGHT_UNDER).setContentActive(false) + HintManagerImpl.createHintHint(editor, location, hint, HintManager.RIGHT_UNDER).setContentActive(false) ) } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt index ec629a7dd2c..b7c519752ae 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt @@ -7,6 +7,7 @@ import com.intellij.openapi.actionSystem.ActionGroup import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.editor.markup.GutterIconRenderer +import software.aws.toolkits.resources.AmazonQBundle.message import javax.swing.Icon class InlineChatGutterIconRenderer (private val icon: Icon) : GutterIconRenderer() { @@ -22,7 +23,7 @@ class InlineChatGutterIconRenderer (private val icon: Icon) : GutterIconRenderer override fun getIcon(): Icon = icon - override fun getTooltipText(): String = "Amazon Q" + override fun getTooltipText(): String = message("amazonqInlineChat.gutter.tooltip") override fun isNavigateAction(): Boolean = false diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index 957b2c5b9cf..bea9a0db84c 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -12,10 +12,13 @@ import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.util.TextRange import com.intellij.ui.IdeBorderFactory +import com.intellij.ui.awt.RelativePoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER +import software.aws.toolkits.resources.AmazonQBundle.message +import java.awt.Point class InlineChatPopupFactory( @@ -55,7 +58,7 @@ class InlineChatPopupFactory( textField.isEnabled = false val prompt = textField.text if (prompt.isNotBlank()) { - setLabel("Generating...") + setLabel(message("amazonqInlineChat.popup.generating")) revalidate() scope.launch { @@ -83,9 +86,29 @@ class InlineChatPopupFactory( setSubmitClickListener(submitListener) } val popup = initPopup(popupPanel) - val popupPoint = editor.visualPositionToXY(editor.caretModel.currentCaret.visualPosition) - popup.setLocation(popupPoint) - popup.showInBestPositionFor(editor) + showPopupInEditor(popup, popupPanel, editor) + + return popup + } + + private fun showPopupInEditor(popup: JBPopup, popupPanel: InlineChatPopupPanel, editor: Editor) { + val popupHeight = popupPanel.POPUP_HEIGHT + val editorComponent = editor.component + val locationOnScreen = editorComponent.locationOnScreen + val popupPoint = JBPopupFactory.getInstance().guessBestPopupLocation(editor).point + + val spaceAbove = popupPoint.y - locationOnScreen.y + val spaceNeeded = popupHeight + 15 // Add a small buffer + + val adjustedPoint = if (spaceAbove >= spaceNeeded) { + // Position above the caret + Point(popupPoint.x, popupPoint.y - spaceNeeded) + } else { + // Position below the caret + popupPoint + } + popup.show(RelativePoint(adjustedPoint)) + popupPanel.textField.requestFocusInWindow() popupPanel.textField.addActionListener { e -> val inputText = popupPanel.textField.text.trim() @@ -93,16 +116,15 @@ class InlineChatPopupFactory( popupPanel.submitButton.doClick() } } - return popup } private fun initPopup(panel: InlineChatPopupPanel): JBPopup { - val cancelButton = IconButton("Cancel", AllIcons.Actions.Cancel) + val cancelButton = IconButton(message("amazonqInlineChat.popup.cancel"), AllIcons.Actions.Cancel) val popup = JBPopupFactory.getInstance() .createComponentPopupBuilder(panel, panel.textField) .setMovable(true) .setResizable(true) - .setTitle("Enter Instructions for Q") + .setTitle(message("amazonqInlineChat.popup.title")) .setCancelButton(cancelButton) .setCancelCallback { cancelHandler.invoke() diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index 499c9049f56..ff5cdd5f42a 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -1 +1 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.editor.actionSystem.EditorActionManager import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.util.Disposer import com.intellij.ui.IdeBorderFactory import icons.AwsIcons import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER import java.awt.BorderLayout import java.awt.Dimension import java.awt.Font import javax.swing.BorderFactory import javax.swing.Icon import javax.swing.JButton import javax.swing.JLabel import javax.swing.JPanel import javax.swing.JTextField import javax.swing.SwingConstants class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { private var submitClickListener: (() -> Unit)? = null val textField = createTextField() val submitButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, "Confirm") private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CANCEL, "Cancel").apply { addActionListener { Disposer.dispose(parentDisposable) } } private val buttonsPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) add(submitButton, BorderLayout.WEST) add(cancelButton, BorderLayout.EAST) } private val acceptButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, "Accept") private val rejectButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.REJECT, "Reject") private val textLabel = JLabel("Edit Code", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(14f) } private val errorLabel = JLabel("", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(14f) } private val inputPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(10, 10, 5, 10) preferredSize = Dimension(600, 50) } private val logoPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) add(textLabel, BorderLayout.CENTER) } private val bottomPanel = JPanel(BorderLayout()).apply { add(logoPanel, BorderLayout.WEST) add(buttonsPanel, BorderLayout.EAST) } override fun getPreferredSize(): Dimension { return Dimension(600, 90) } private fun createTextField() : JTextField = JTextField().apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme preferredSize = Dimension(550, 35) border = IdeBorderFactory.createRoundedBorder().apply { setColor(POPUP_BUTTON_BORDER) } font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } private fun createButtonWithIcon(icon: Icon, text: String): JButton { return JButton(text).apply { horizontalTextPosition = SwingConstants.LEFT preferredSize = Dimension(80, 30) setIcon(icon) isOpaque = false isContentAreaFilled = false isBorderPainted = false font = font.deriveFont(14f) } } init { layout = BorderLayout() inputPanel.add(textField, BorderLayout.CENTER) inputPanel.preferredSize = Dimension(600, 50) add(inputPanel, BorderLayout.CENTER) add(bottomPanel, BorderLayout.SOUTH) submitButton.addActionListener { submitClickListener?.invoke() } } fun setSubmitClickListener(listener: () -> Unit) { submitClickListener = listener } private fun addActionListener(id: String, action: EditorActionHandler) { val actionManager = EditorActionManager.getInstance() val originalHandler = actionManager.getActionHandler(id) actionManager.setActionHandler(id, action) val restorer = Disposable { actionManager.setActionHandler(id, originalHandler) } Disposer.register(parentDisposable, restorer) } private fun getEditorActionHandler(action: () -> Unit) : EditorActionHandler { val handler = object : EditorActionHandler() { override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { action.invoke() Disposer.dispose(parentDisposable) } } return handler } fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit ) { textLabel.text = "Edit Code" // this is a workaround somehow the textField will interfere with the enter handler val emptyTextField = createTextField() emptyTextField.text = textField.text emptyTextField.isEnabled = false inputPanel.remove(textField) inputPanel.add(emptyTextField, BorderLayout.CENTER) buttonsPanel.remove(submitButton) buttonsPanel.remove(cancelButton) buttonsPanel.add(acceptButton, BorderLayout.WEST) buttonsPanel.add(rejectButton, BorderLayout.EAST) acceptButton.addActionListener { acceptAction.invoke() } rejectButton.addActionListener { rejectAction.invoke() } val enterHandler = getEditorActionHandler(acceptAction) addActionListener(IdeActions.ACTION_EDITOR_ENTER, enterHandler) val escapeHandler = getEditorActionHandler(rejectAction) addActionListener(IdeActions.ACTION_EDITOR_ESCAPE, escapeHandler) revalidate() } fun setErrorMessage(text: String) { errorLabel.text = text add(errorLabel, BorderLayout.CENTER) remove(inputPanel) remove(bottomPanel) revalidate() } fun setLabel(text: String) { textLabel.text = text textField.isEnabled = false revalidate() } } \ No newline at end of file +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.editor.actionSystem.EditorActionManager import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.util.Disposer import com.intellij.ui.IdeBorderFactory import icons.AwsIcons import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER import software.aws.toolkits.resources.AmazonQBundle.message import java.awt.BorderLayout import java.awt.Dimension import java.awt.Font import javax.swing.BorderFactory import javax.swing.Icon import javax.swing.JButton import javax.swing.JLabel import javax.swing.JPanel import javax.swing.JTextField import javax.swing.SwingConstants class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { private var submitClickListener: (() -> Unit)? = null private val POPUP_BUTTON_FONT_SIZE = 14f private val POPUP_WIDTH = 600 val POPUP_HEIGHT = 90 private val POPUP_BUTTON_HEIGHT = 30 private val POPUP_BUTTON_WIDTH = 80 private val POPUP_INPUT_HEIGHT = 40 private val POPUP_INPUT_WIDTH = 500 val textField = createTextField() val submitButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, message("amazonqInlineChat.popup.confirm")) private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CANCEL, message("amazonqInlineChat.popup.cancel")).apply { addActionListener { Disposer.dispose(parentDisposable) } } private val buttonsPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) add(submitButton, BorderLayout.WEST) add(cancelButton, BorderLayout.EAST) } private val acceptButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, message("amazonqInlineChat.popup.accept")) private val rejectButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.REJECT, message("amazonqInlineChat.popup.reject")) private val textLabel = JLabel(message("amazonqInlineChat.popup.editCode"), AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(POPUP_BUTTON_FONT_SIZE) } private val errorLabel = JLabel("", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(POPUP_BUTTON_FONT_SIZE) } private val inputPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(10, 10, 5, 10) } private val logoPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) add(textLabel, BorderLayout.CENTER) } private val bottomPanel = JPanel(BorderLayout()).apply { add(logoPanel, BorderLayout.WEST) add(buttonsPanel, BorderLayout.EAST) } override fun getPreferredSize(): Dimension { return Dimension(POPUP_WIDTH, POPUP_HEIGHT) } private fun createTextField() : JTextField = JTextField().apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme preferredSize = Dimension(POPUP_INPUT_WIDTH, POPUP_INPUT_HEIGHT) border = IdeBorderFactory.createRoundedBorder().apply { setColor(POPUP_BUTTON_BORDER) } font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } private fun createButtonWithIcon(icon: Icon, text: String): JButton { return JButton(text).apply { horizontalTextPosition = SwingConstants.LEFT preferredSize = Dimension(POPUP_BUTTON_WIDTH, POPUP_BUTTON_HEIGHT) setIcon(icon) isOpaque = false isContentAreaFilled = false isBorderPainted = false font = font.deriveFont(POPUP_BUTTON_FONT_SIZE) } } init { layout = BorderLayout() inputPanel.add(textField, BorderLayout.CENTER) add(inputPanel, BorderLayout.CENTER) add(bottomPanel, BorderLayout.SOUTH) submitButton.addActionListener { submitClickListener?.invoke() } } fun setSubmitClickListener(listener: () -> Unit) { submitClickListener = listener } private fun addActionListener(id: String, action: EditorActionHandler) { val actionManager = EditorActionManager.getInstance() val originalHandler = actionManager.getActionHandler(id) actionManager.setActionHandler(id, action) val restorer = Disposable { actionManager.setActionHandler(id, originalHandler) } Disposer.register(parentDisposable, restorer) } private fun getEditorActionHandler(action: () -> Unit) : EditorActionHandler { val handler = object : EditorActionHandler() { override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { action.invoke() Disposer.dispose(parentDisposable) } } return handler } fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit ) { textLabel.text = message("amazonqInlineChat.popup.editCode") // this is a workaround somehow the textField will interfere with the enter handler val emptyTextField = createTextField() emptyTextField.text = textField.text emptyTextField.isEnabled = false inputPanel.remove(textField) inputPanel.add(emptyTextField, BorderLayout.CENTER) buttonsPanel.remove(submitButton) buttonsPanel.remove(cancelButton) buttonsPanel.add(acceptButton, BorderLayout.WEST) buttonsPanel.add(rejectButton, BorderLayout.EAST) acceptButton.addActionListener { acceptAction.invoke() } rejectButton.addActionListener { rejectAction.invoke() } val enterHandler = getEditorActionHandler(acceptAction) addActionListener(IdeActions.ACTION_EDITOR_ENTER, enterHandler) val escapeHandler = getEditorActionHandler(rejectAction) addActionListener(IdeActions.ACTION_EDITOR_ESCAPE, escapeHandler) revalidate() } fun setErrorMessage(text: String) { errorLabel.text = text add(errorLabel, BorderLayout.CENTER) remove(inputPanel) remove(bottomPanel) revalidate() } fun setLabel(text: String) { textLabel.text = text textField.isEnabled = false revalidate() } } \ No newline at end of file diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/inlineChat/InlineChatControllerTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/inlineChat/InlineChatControllerTest.kt new file mode 100644 index 00000000000..63b5670da1f --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/inlineChat/InlineChatControllerTest.kt @@ -0,0 +1,4 @@ +package software.aws.toolkits.jetbrains.services.amazonq.inlineChat + +class InlineChatControllerTest { +} diff --git a/plugins/amazonq/shared/jetbrains-community/resources/software/aws/toolkits/resources/AmazonQBundle.properties b/plugins/amazonq/shared/jetbrains-community/resources/software/aws/toolkits/resources/AmazonQBundle.properties index 8355e3c76f6..c0bd78f26bb 100644 --- a/plugins/amazonq/shared/jetbrains-community/resources/software/aws/toolkits/resources/AmazonQBundle.properties +++ b/plugins/amazonq/shared/jetbrains-community/resources/software/aws/toolkits/resources/AmazonQBundle.properties @@ -1,3 +1,13 @@ action.q.hello.description=Hello description +amazonqInlineChat.gutter.tooltip = Amazon Q +amazonqInlineChat.hint.edit = Edit +amazonqInlineChat.hint.windows.shortCut = (Ctrl + I) +amazonqInlineChat.popup.accept=Accept +amazonqInlineChat.popup.cancel=Cancel +amazonqInlineChat.popup.confirm=Confirm +amazonqInlineChat.popup.editCode = Edit Code +amazonqInlineChat.popup.generating = Generating... +amazonqInlineChat.popup.reject=Reject +amazonqInlineChat.popup.title=Enter Instructions for Q amazonq.refresh.panel=Refresh Chat Session q.hello=Hello From fa5317311f613be46730def962a439e3fa6540ec Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Wed, 16 Oct 2024 17:07:52 -0700 Subject: [PATCH 15/39] improve hint location --- .../cwc/inline/InlineChatEditorHint.kt | 20 +- .../cwc/inline/InlineChatPopupPanel.kt | 176 +++++++++++++++++- 2 files changed, 182 insertions(+), 14 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt index 4f51e0ca3a8..987aca60af2 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt @@ -6,6 +6,7 @@ import com.intellij.codeInsight.hint.HintManager import com.intellij.codeInsight.hint.HintManagerImpl import com.intellij.codeInsight.hint.HintUtil import com.intellij.openapi.editor.Editor +import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.util.SystemInfo import com.intellij.ui.LightweightHint import com.intellij.ui.SimpleColoredText @@ -19,24 +20,17 @@ import javax.swing.JPanel class InlineChatEditorHint { private val hint = createHint() - private val HINT_BUFFER = 50 + private val HINT_LOCATION_BUFFER = 50 private fun getHintLocation (editor: Editor): Point { val selectionModel = editor.selectionModel - val selectionEnd = selectionModel.selectionEnd - val selectionLineEnd = editor.document.getLineEndOffset(editor.document.getLineNumber(selectionEnd)) - - val xyPosition = editor.offsetToXY(selectionLineEnd) - val editorLocation = editor.component.locationOnScreen - val editorContentLocation = editor.contentComponent.locationOnScreen - val position = Point( - editorContentLocation.x + xyPosition.x, - editorLocation.y + xyPosition.y - editor.scrollingModel.verticalScrollOffset - HINT_BUFFER) - + val lineSelected = selectionModel.selectedText?.split("\n")?.size + val offset: Int = if (lineSelected != null && lineSelected > 1) {(lineSelected - 1).times(editor.lineHeight)} else {0} + val bestPosition = JBPopupFactory.getInstance().guessBestPopupLocation(editor).point val visibleArea = editor.scrollingModel.visibleArea - val adjustedX = (position.x ).coerceAtMost(visibleArea.x + visibleArea.width - HINT_BUFFER) - val adjustedY = (position.y ).coerceAtMost(visibleArea.y + visibleArea.height - HINT_BUFFER) + val adjustedX = (bestPosition.x + 200).coerceAtMost(visibleArea.x + visibleArea.width - HINT_LOCATION_BUFFER) + val adjustedY = (bestPosition.y + offset).coerceAtMost(visibleArea.y + visibleArea.height - HINT_LOCATION_BUFFER) val adjustedPosition = Point(adjustedX, adjustedY) return adjustedPosition diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index ff5cdd5f42a..a06e2b210fb 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -1 +1,175 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.editor.actionSystem.EditorActionManager import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.util.Disposer import com.intellij.ui.IdeBorderFactory import icons.AwsIcons import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER import software.aws.toolkits.resources.AmazonQBundle.message import java.awt.BorderLayout import java.awt.Dimension import java.awt.Font import javax.swing.BorderFactory import javax.swing.Icon import javax.swing.JButton import javax.swing.JLabel import javax.swing.JPanel import javax.swing.JTextField import javax.swing.SwingConstants class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { private var submitClickListener: (() -> Unit)? = null private val POPUP_BUTTON_FONT_SIZE = 14f private val POPUP_WIDTH = 600 val POPUP_HEIGHT = 90 private val POPUP_BUTTON_HEIGHT = 30 private val POPUP_BUTTON_WIDTH = 80 private val POPUP_INPUT_HEIGHT = 40 private val POPUP_INPUT_WIDTH = 500 val textField = createTextField() val submitButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, message("amazonqInlineChat.popup.confirm")) private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CANCEL, message("amazonqInlineChat.popup.cancel")).apply { addActionListener { Disposer.dispose(parentDisposable) } } private val buttonsPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) add(submitButton, BorderLayout.WEST) add(cancelButton, BorderLayout.EAST) } private val acceptButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, message("amazonqInlineChat.popup.accept")) private val rejectButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.REJECT, message("amazonqInlineChat.popup.reject")) private val textLabel = JLabel(message("amazonqInlineChat.popup.editCode"), AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(POPUP_BUTTON_FONT_SIZE) } private val errorLabel = JLabel("", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(POPUP_BUTTON_FONT_SIZE) } private val inputPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(10, 10, 5, 10) } private val logoPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(5, 5, 5, 5) add(textLabel, BorderLayout.CENTER) } private val bottomPanel = JPanel(BorderLayout()).apply { add(logoPanel, BorderLayout.WEST) add(buttonsPanel, BorderLayout.EAST) } override fun getPreferredSize(): Dimension { return Dimension(POPUP_WIDTH, POPUP_HEIGHT) } private fun createTextField() : JTextField = JTextField().apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme preferredSize = Dimension(POPUP_INPUT_WIDTH, POPUP_INPUT_HEIGHT) border = IdeBorderFactory.createRoundedBorder().apply { setColor(POPUP_BUTTON_BORDER) } font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } private fun createButtonWithIcon(icon: Icon, text: String): JButton { return JButton(text).apply { horizontalTextPosition = SwingConstants.LEFT preferredSize = Dimension(POPUP_BUTTON_WIDTH, POPUP_BUTTON_HEIGHT) setIcon(icon) isOpaque = false isContentAreaFilled = false isBorderPainted = false font = font.deriveFont(POPUP_BUTTON_FONT_SIZE) } } init { layout = BorderLayout() inputPanel.add(textField, BorderLayout.CENTER) add(inputPanel, BorderLayout.CENTER) add(bottomPanel, BorderLayout.SOUTH) submitButton.addActionListener { submitClickListener?.invoke() } } fun setSubmitClickListener(listener: () -> Unit) { submitClickListener = listener } private fun addActionListener(id: String, action: EditorActionHandler) { val actionManager = EditorActionManager.getInstance() val originalHandler = actionManager.getActionHandler(id) actionManager.setActionHandler(id, action) val restorer = Disposable { actionManager.setActionHandler(id, originalHandler) } Disposer.register(parentDisposable, restorer) } private fun getEditorActionHandler(action: () -> Unit) : EditorActionHandler { val handler = object : EditorActionHandler() { override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { action.invoke() Disposer.dispose(parentDisposable) } } return handler } fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit ) { textLabel.text = message("amazonqInlineChat.popup.editCode") // this is a workaround somehow the textField will interfere with the enter handler val emptyTextField = createTextField() emptyTextField.text = textField.text emptyTextField.isEnabled = false inputPanel.remove(textField) inputPanel.add(emptyTextField, BorderLayout.CENTER) buttonsPanel.remove(submitButton) buttonsPanel.remove(cancelButton) buttonsPanel.add(acceptButton, BorderLayout.WEST) buttonsPanel.add(rejectButton, BorderLayout.EAST) acceptButton.addActionListener { acceptAction.invoke() } rejectButton.addActionListener { rejectAction.invoke() } val enterHandler = getEditorActionHandler(acceptAction) addActionListener(IdeActions.ACTION_EDITOR_ENTER, enterHandler) val escapeHandler = getEditorActionHandler(rejectAction) addActionListener(IdeActions.ACTION_EDITOR_ESCAPE, escapeHandler) revalidate() } fun setErrorMessage(text: String) { errorLabel.text = text add(errorLabel, BorderLayout.CENTER) remove(inputPanel) remove(bottomPanel) revalidate() } fun setLabel(text: String) { textLabel.text = text textField.isEnabled = false revalidate() } } \ No newline at end of file +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.cwc.inline + +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.actionSystem.IdeActions +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.intellij.openapi.editor.actionSystem.EditorActionManager +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.util.Disposer +import com.intellij.ui.IdeBorderFactory +import icons.AwsIcons +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER +import software.aws.toolkits.resources.AmazonQBundle.message +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.Font +import javax.swing.BorderFactory +import javax.swing.Icon +import javax.swing.JButton +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.JTextField +import javax.swing.SwingConstants + +class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { + private var submitClickListener: (() -> Unit)? = null + private val POPUP_BUTTON_FONT_SIZE = 14f + private val POPUP_WIDTH = 600 + val POPUP_HEIGHT = 90 + private val POPUP_BUTTON_HEIGHT = 30 + private val POPUP_BUTTON_WIDTH = 80 + private val POPUP_INPUT_HEIGHT = 40 + private val POPUP_INPUT_WIDTH = 500 + + + val textField = createTextField() + + val submitButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, message("amazonqInlineChat.popup.confirm")) + + private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CANCEL, message("amazonqInlineChat.popup.cancel")).apply { + addActionListener { Disposer.dispose(parentDisposable) } + } + + private val buttonsPanel = JPanel(BorderLayout()).apply { + border = BorderFactory.createEmptyBorder(5, 5, 5, 5) + add(submitButton, BorderLayout.WEST) + add(cancelButton, BorderLayout.EAST) + } + + private val acceptButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, message("amazonqInlineChat.popup.accept")) + private val rejectButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.REJECT, message("amazonqInlineChat.popup.reject")) + private val textLabel = JLabel(message("amazonqInlineChat.popup.editCode"), AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { + font = font.deriveFont(POPUP_BUTTON_FONT_SIZE) + } + + private val errorLabel = JLabel("", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { + font = font.deriveFont(POPUP_BUTTON_FONT_SIZE) + } + + private val inputPanel = JPanel(BorderLayout()).apply { + border = BorderFactory.createEmptyBorder(10, 10, 5, 10) + } + + private val logoPanel = JPanel(BorderLayout()).apply { + border = BorderFactory.createEmptyBorder(5, 5, 5, 5) + add(textLabel, BorderLayout.CENTER) + } + + private val bottomPanel = JPanel(BorderLayout()).apply { + add(logoPanel, BorderLayout.WEST) + add(buttonsPanel, BorderLayout.EAST) + } + + override fun getPreferredSize(): Dimension { + return Dimension(POPUP_WIDTH, POPUP_HEIGHT) + } + + private fun createTextField() : JTextField = JTextField().apply { + val editorColorsScheme = EditorColorsManager.getInstance().globalScheme + preferredSize = Dimension(POPUP_INPUT_WIDTH, POPUP_INPUT_HEIGHT) + border = IdeBorderFactory.createRoundedBorder().apply { + setColor(POPUP_BUTTON_BORDER) + } + font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) + } + + private fun createButtonWithIcon(icon: Icon, text: String): JButton { + return JButton(text).apply { + horizontalTextPosition = SwingConstants.LEFT + preferredSize = Dimension(POPUP_BUTTON_WIDTH, POPUP_BUTTON_HEIGHT) + setIcon(icon) + isOpaque = false + isContentAreaFilled = false + isBorderPainted = false + font = font.deriveFont(POPUP_BUTTON_FONT_SIZE) + } + } + + init { + layout = BorderLayout() + inputPanel.add(textField, BorderLayout.CENTER) + add(inputPanel, BorderLayout.CENTER) + add(bottomPanel, BorderLayout.SOUTH) + + submitButton.addActionListener { + submitClickListener?.invoke() + } + } + + fun setSubmitClickListener(listener: () -> Unit) { + submitClickListener = listener + } + + private fun addActionListener(id: String, action: EditorActionHandler) { + val actionManager = EditorActionManager.getInstance() + val originalHandler = actionManager.getActionHandler(id) + + actionManager.setActionHandler(id, action) + val restorer = Disposable { actionManager.setActionHandler(id, originalHandler) } + Disposer.register(parentDisposable, restorer) + } + + private fun getEditorActionHandler(action: () -> Unit) : EditorActionHandler { + val handler = object : EditorActionHandler() { + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { + action.invoke() + Disposer.dispose(parentDisposable) + } + } + return handler + } + + fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit ) { + textLabel.text = message("amazonqInlineChat.popup.editCode") + // this is a workaround somehow the textField will interfere with the enter handler + val emptyTextField = createTextField() + emptyTextField.text = textField.text + emptyTextField.isEnabled = false + inputPanel.remove(textField) + inputPanel.add(emptyTextField, BorderLayout.CENTER) + + buttonsPanel.remove(submitButton) + buttonsPanel.remove(cancelButton) + buttonsPanel.add(acceptButton, BorderLayout.WEST) + buttonsPanel.add(rejectButton, BorderLayout.EAST) + acceptButton.addActionListener { acceptAction.invoke() } + rejectButton.addActionListener { rejectAction.invoke() } + + val enterHandler = getEditorActionHandler(acceptAction) + addActionListener(IdeActions.ACTION_EDITOR_ENTER, enterHandler) + + val escapeHandler = getEditorActionHandler(rejectAction) + addActionListener(IdeActions.ACTION_EDITOR_ESCAPE, escapeHandler) + revalidate() + } + + fun setErrorMessage(text: String) { + errorLabel.text = text + add(errorLabel, BorderLayout.CENTER) + remove(inputPanel) + remove(bottomPanel) + revalidate() + } + + fun setLabel(text: String) { + textLabel.text = text + textField.isEnabled = false + revalidate() + } +} From 751f2bf9297117b62e59d780b16b66d026cc7024 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Thu, 17 Oct 2024 07:46:28 -0700 Subject: [PATCH 16/39] cleanup --- gradle/libs.versions.toml | 2 + .../chat/jetbrains-community/build.gradle.kts | 3 +- .../services/amazonq/QOpenPanelAction.kt | 4 - .../cwc/clients/chat/v1/ChatSessionV1.kt | 3 +- .../services/cwc/commands/ActionRegistrar.kt | 4 +- .../cwc/commands/ContextMenuActionMessage.kt | 2 +- .../services/cwc/controller/ChatController.kt | 4 - .../chat/userIntent/UserIntentRecognizer.kt | 1 - .../cwc/inline/InlineChatController.kt | 152 +++++++----------- .../cwc/inline/InlineChatEditorHint.kt | 2 +- .../cwc/inline/OpenChatInputAction.kt | 2 - .../cwc/inline/listeners/ChatCaretListener.kt | 1 - .../listeners/InlineChatFileListener.kt | 1 - .../listeners/InlineChatSelectionListener.kt | 1 - .../inlineChat/InlineChatControllerTest.kt | 4 - .../resources/icons/logos/Amazon_Q_grey.svg | 2 +- .../amazonq_inline_chat_shortcut.svg} | 0 .../jetbrains-community/src/icons/AwsIcons.kt | 8 +- 18 files changed, 76 insertions(+), 120 deletions(-) delete mode 100644 plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/inlineChat/InlineChatControllerTest.kt rename plugins/core/jetbrains-community/resources/icons/{misc/Q_inlineChat_shortcut.svg => resources/inlinechat/amazonq_inline_chat_shortcut.svg} (100%) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a5c9bfcddea..eacfb8ffffa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ assertJ = "3.26.3" awsSdk = "2.26.25" commonmark = "0.22.0" detekt = "1.23.7" +diff-util = "4.12" intellijExt = "1.1.8" # match with /settings.gradle.kts intellijGradle = "2.1.0" @@ -71,6 +72,7 @@ commons-collections = { module = "org.apache.commons:commons-collections4", vers commons-io = { module = "commons-io:commons-io", version.ref = "apache-commons-io" } detekt-api = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" } detekt-formattingRules = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } +diff-util = { module = "io.github.java-diff-utils:java-diff-utils", version.ref = "diff-util" } detekt-test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" } gradlePlugin-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } gradlePlugin-ideaExt = { module = "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle-idea-ext", version.ref = "intellijExt" } diff --git a/plugins/amazonq/chat/jetbrains-community/build.gradle.kts b/plugins/amazonq/chat/jetbrains-community/build.gradle.kts index c50de2daf7c..37fce7a3f23 100644 --- a/plugins/amazonq/chat/jetbrains-community/build.gradle.kts +++ b/plugins/amazonq/chat/jetbrains-community/build.gradle.kts @@ -20,8 +20,7 @@ dependencies { // everything references codewhisperer, which is not ideal implementation(project(":plugin-amazonq:codewhisperer:jetbrains-community")) implementation(libs.nimbus.jose.jwt) - implementation("org.jetbrains:markdown:0.7.3") - implementation("io.github.java-diff-utils:java-diff-utils:4.12") + implementation(libs.diff.util) compileOnly(project(":plugin-core:jetbrains-community")) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt index cea0b15bbee..5d249301c82 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QOpenPanelAction.kt @@ -21,7 +21,3 @@ class QOpenPanelAction : AnAction(message("action.q.openchat.text"), null, AwsIc ToolWindowManager.getInstance(project).getToolWindow(AMAZON_Q_WINDOW_ID)?.activate(null, true) } } - -private fun isQSupportedInThisVersion(): Boolean { - return true -} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt index 8fbd51e9dd8..a5057f797c8 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt @@ -178,8 +178,7 @@ class ChatSessionV1( logger.info { "Request from tab: ${data.tabId}, conversationId: $conversationId, request: $request" } client.generateAssistantResponse(request, responseHandler).await() } - } catch (e: Exception) { - println(e.message) + } catch (e: TimeoutCancellationException) { // Re-throw an exception that can be caught downstream throw ChatApiException( message = "API request timed out", diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt index dddffaad5ab..71110cba420 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt @@ -14,8 +14,8 @@ class ActionRegistrar { private val _messages by lazy { MutableSharedFlow(extraBufferCapacity = 10) } val flow = _messages.asSharedFlow() - fun reportMessageClick(command: EditorContextCommand, project: Project, message: String? = null) { - _messages.tryEmit(ContextMenuActionMessage(command, project, message)) + fun reportMessageClick(command: EditorContextCommand, project: Project) { + _messages.tryEmit(ContextMenuActionMessage(command, project)) } fun reportMessageClick(command: EditorContextCommand, issue: MutableMap, project: Project) { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ContextMenuActionMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ContextMenuActionMessage.kt index dbe37326efb..d7ae565d277 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ContextMenuActionMessage.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ContextMenuActionMessage.kt @@ -9,4 +9,4 @@ import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage /** * Event emitted for context menu editor actions */ -data class ContextMenuActionMessage(val command: EditorContextCommand, val project: Project, val message: String? = null) : AmazonQMessage +data class ContextMenuActionMessage(val command: EditorContextCommand, val project: Project) : AmazonQMessage diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt index 0c3c64f485b..15e4d9a765d 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt @@ -89,8 +89,6 @@ class ChatController private constructor( private val contextExtractor: ActiveFileContextExtractor, private val intentRecognizer: UserIntentRecognizer, private val authController: AuthController, -// private val editorListener: ProjectContextEditorListener, -// private val caretListener: ChatCaretListener, ) : InboundAppMessagesHandler { private val messagePublisher: MessagePublisher = context.messagesFromAppToUi @@ -103,8 +101,6 @@ class ChatController private constructor( contextExtractor = ActiveFileContextExtractor.create(fqnWebviewAdapter = context.fqnWebviewAdapter, project = context.project), intentRecognizer = UserIntentRecognizer(), authController = AuthController(), -// editorListener = ProjectContextEditorListener(context), -// caretListener = ChatCaretListener(context.project, context), ) override suspend fun processClearQuickAction(message: IncomingCwcMessage.ClearChat) { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/userIntent/UserIntentRecognizer.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/userIntent/UserIntentRecognizer.kt index 414fa12f438..f15f0125e9f 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/userIntent/UserIntentRecognizer.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/userIntent/UserIntentRecognizer.kt @@ -18,7 +18,6 @@ class UserIntentRecognizer { EditorContextCommand.ExplainCodeScanIssue -> UserIntent.EXPLAIN_CODE_SELECTION EditorContextCommand.GenerateUnitTests -> UserIntent.GENERATE_UNIT_TESTS EditorContextCommand.SendToPrompt -> null - EditorContextCommand.SendToChat -> null } fun getUserIntentFromPromptChatMessage(prompt: String, startUrl: String?) = when { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index f3b1e82018d..462878acefc 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -14,7 +14,6 @@ import com.intellij.openapi.application.runInEdt import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.command.UndoConfirmationPolicy import com.intellij.openapi.command.WriteCommandAction -import com.intellij.openapi.command.undo.UndoManager import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.editor.Document @@ -79,7 +78,6 @@ class InlineChatController( private val scope: CoroutineScope ) : Disposable { private var currentPopup: JBPopup? = null -// private val scope = disposableCoroutineScope(this) private var rangeHighlighter: RangeHighlighter? = null private val partialUndoActions = Stack<() -> Unit>() private val partialAcceptActions = Stack<() -> Unit>() @@ -90,8 +88,11 @@ class InlineChatController( private val isInProgress = AtomicBoolean(false) private var metrics: InlineChatMetrics? = null private var isPopupAborted = AtomicBoolean(true) - private val listener: InlineChatFileListener = InlineChatFileListener(project).apply { - project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this) + + init { + InlineChatFileListener(project).apply { + project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this) + } } data class InlineChatMetrics( @@ -111,7 +112,6 @@ class InlineChatController( ) private val popupSubmitHandler: suspend (String, String, Int, Editor) -> String = { prompt: String, selectedCode: String, selectedLineStart: Int, editor: Editor -> -// val selectedCode = getSelectedText(editor) runBlocking { isInProgress.set(true) val message = handleChat(prompt, selectedCode, editor, selectedLineStart) @@ -164,15 +164,12 @@ class InlineChatController( private val diffAcceptHandler: () -> Unit = { scope.launch(Dispatchers.EDT) { - val undoManager = UndoManager.getGlobalInstance() -// undoManager.undoableActionPerformed() partialUndoActions.clear() while (partialAcceptActions.isNotEmpty()) { val action = partialAcceptActions.pop() runChangeAction(project, action) } invokeLater { hidePopup() } -// hidePopup() } ApplicationManager.getApplication().executeOnPooledThread { recordInlineChatTelemetry(InlineChatUserDecision.ACCEPT) @@ -202,8 +199,6 @@ class InlineChatController( popupCancelHandler.invoke() } } - // telemetryHelper.recordInlineChatTelemetry(prompt.length, numOfLinesSelected, true, -// InlineChatUserDecision.DISMISS, 0.0, requestEndLatency) } popup.addListener(popupListener) } @@ -286,13 +281,6 @@ class InlineChatController( return rows } - fun getShouldShowActions(): Boolean { - return shouldShowActions.get() - } - - fun getIsInProgress(): Boolean { - return isInProgress.get() - } private fun unescape(s: String): String { return StringEscapeUtils.unescapeHtml3(s) @@ -343,13 +331,10 @@ class InlineChatController( insertLine++ } DiffRow.Tag.DELETE, DiffRow.Tag.CHANGE -> { + if (row.tag == DiffRow.Tag.CHANGE && row.newLine.trimIndent() == row.oldLine?.trimIndent()) return isAllEqual = false - try { - if (row.tag == DiffRow.Tag.CHANGE && row.newLine.trimIndent() == row.oldLine?.trimIndent()) return - showCodeChangeInEditor(row, currentDocumentLine, editor) - } catch (e: Exception) { - e.printStackTrace() - } + showCodeChangeInEditor(row, currentDocumentLine, editor) + if (row.tag == DiffRow.Tag.CHANGE) { insertLine+=2 currentDocumentLine+=2 @@ -362,11 +347,8 @@ class InlineChatController( } DiffRow.Tag.INSERT -> { isAllEqual = false - try { - showCodeChangeInEditor(row, insertLine, editor) - } catch (e: Exception) { - e.printStackTrace() - } + showCodeChangeInEditor(row, insertLine, editor) + insertLine++ addedLinesCount++ addedCharsCount += row.newLine?.length?: 0 @@ -421,26 +403,11 @@ class InlineChatController( } } - private fun getSelectionStartLine(editor: Editor): Int { - return ReadAction.compute { - editor.document.getLineNumber(editor.selectionModel.selectionStart) - } - } - private suspend fun runChangeAction(project: Project, action: () -> Unit, shouldRecordForUndo: Boolean = false) { withContext(EDT) { -// val undoManager = UndoManager.getInstance(project) -// val undoableGroup = undoManager.undoableGroup CommandProcessor.getInstance().executeCommand(project, { ApplicationManager.getApplication().runWriteAction { WriteCommandAction.runWriteCommandAction(project) { -// UndoManager.getInstance(project).undoableActionPerformed(object : BasicUndoableAction() { -// override fun undo() { -// } -// -// override fun redo() { -// } -// }) action() } } @@ -480,70 +447,75 @@ class InlineChatController( } private suspend fun showCodeChangeInEditor(diffRow: DiffRow, row: Int, editor: Editor) { - val document = editor.document - when (diffRow.tag) { - DiffRow.Tag.DELETE -> { - val changeStartOffset = getLineStartOffset(document, row) - val changeEndOffset = getLineEndOffset(document, row) - val rangeMarker = highlightString(editor, changeStartOffset, changeEndOffset, false) - partialUndoActions.add { - editor.markupModel.removeAllHighlighters() - } - partialAcceptActions.add { - if (rangeMarker.isValid) { - scope.launch(Dispatchers.EDT) { - deleteString(document, rangeMarker.startOffset, rangeMarker.endOffset) + try { + val document = editor.document + when (diffRow.tag) { + DiffRow.Tag.DELETE -> { + val changeStartOffset = getLineStartOffset(document, row) + val changeEndOffset = getLineEndOffset(document, row) + val rangeMarker = highlightString(editor, changeStartOffset, changeEndOffset, false) + partialUndoActions.add { + editor.markupModel.removeAllHighlighters() + } + partialAcceptActions.add { + if (rangeMarker.isValid) { + scope.launch(Dispatchers.EDT) { + deleteString(document, rangeMarker.startOffset, rangeMarker.endOffset) + } } + editor.markupModel.removeAllHighlighters() } - editor.markupModel.removeAllHighlighters() } - } - DiffRow.Tag.INSERT -> { - val newLineInserted = insertNewLineIfNeeded(row, editor) - val insertOffset = getLineStartOffset(document, row) - val textToInsert = unescape(diffRow.newLine) + "\n" - val rangeMarker = insertString(editor, insertOffset, textToInsert) - partialUndoActions.add { - if (rangeMarker.isValid) { - scope.launch(Dispatchers.EDT) { - deleteString(document, rangeMarker.startOffset, rangeMarker.endOffset + newLineInserted) + DiffRow.Tag.INSERT -> { + val newLineInserted = insertNewLineIfNeeded(row, editor) + val insertOffset = getLineStartOffset(document, row) + val textToInsert = unescape(diffRow.newLine) + "\n" + val rangeMarker = insertString(editor, insertOffset, textToInsert) + partialUndoActions.add { + if (rangeMarker.isValid) { + scope.launch(Dispatchers.EDT) { + deleteString(document, rangeMarker.startOffset, rangeMarker.endOffset + newLineInserted) + } } + editor.markupModel.removeAllHighlighters() + } + partialAcceptActions.add { + editor.markupModel.removeAllHighlighters() } - editor.markupModel.removeAllHighlighters() - } - partialAcceptActions.add { - editor.markupModel.removeAllHighlighters() } - } - else -> { - val changeOffset = getLineStartOffset(document, row) - val changeEndOffset = getLineEndOffset(document, row) - val oldTextRangeMarker = highlightString(editor, changeOffset, changeEndOffset, false) - partialAcceptActions.add { - scope.launch(Dispatchers.EDT) { + else -> { + val changeOffset = getLineStartOffset(document, row) + val changeEndOffset = getLineEndOffset(document, row) + val oldTextRangeMarker = highlightString(editor, changeOffset, changeEndOffset, false) + partialAcceptActions.add { + scope.launch(Dispatchers.EDT) { if (oldTextRangeMarker.isValid) { deleteString(document, oldTextRangeMarker.startOffset, oldTextRangeMarker.endOffset) } + } + editor.markupModel.removeAllHighlighters() } - editor.markupModel.removeAllHighlighters() - } - val insertOffset = getLineEndOffset(document, row) - val newLineInserted = insertNewLineIfNeeded(row, editor) - val textToInsert = unescape(diffRow.newLine) + "\n" - val newTextRangeMarker = insertString(editor, insertOffset, textToInsert) - partialUndoActions.add { - WriteCommandAction.runWriteCommandAction(project) { - if (newTextRangeMarker.isValid) { - scope.launch(Dispatchers.EDT) { - deleteString(document, newTextRangeMarker.startOffset, newTextRangeMarker.endOffset + newLineInserted) + val insertOffset = getLineEndOffset(document, row) + val newLineInserted = insertNewLineIfNeeded(row, editor) + val textToInsert = unescape(diffRow.newLine) + "\n" + val newTextRangeMarker = insertString(editor, insertOffset, textToInsert) + partialUndoActions.add { + WriteCommandAction.runWriteCommandAction(project) { + if (newTextRangeMarker.isValid) { + scope.launch(Dispatchers.EDT) { + deleteString(document, newTextRangeMarker.startOffset, newTextRangeMarker.endOffset + newLineInserted) + } } } + editor.markupModel.removeAllHighlighters() } - editor.markupModel.removeAllHighlighters() } } + } catch (e: Exception) { + logger.warn {"Error when showing inline chat diff in editor: ${e.message} \n ${e.stackTraceToString()}"} + throw Exception("Unexpected error, please try again.") } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt index 987aca60af2..ea9956cc86d 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt @@ -48,7 +48,7 @@ class InlineChatEditorHint { coloredText.appendToComponent(component) val shortcutComponent = HintUtil.createInformationComponent() if (!SystemInfo.isWindows) { - val shortCutIcon = AwsIcons.Misc.AWS_Q_INLINECHAT_SHORTCUT + val shortCutIcon = AwsIcons.Resources.InlineChat.AWS_Q_INLINECHAT_SHORTCUT shortcutComponent.isIconOnTheRight = true; shortcutComponent.icon = shortCutIcon } else { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt index d461e86adb5..c15534e4f3a 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt @@ -28,10 +28,8 @@ class OpenChatInputAction : AnAction() { private fun createCaretListener(editor: Editor): CaretListener { return object : CaretListener { override fun caretPositionChanged(event: CaretEvent) { - // Remove the popup when the caret moves inlineChatController?.disposePopup() - // Remove the listener after closing the popup editor.caretModel.removeCaretListener(this) caretListener = null inlineChatController = null diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt index 8d799b7f8f3..7bd89e6c438 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt @@ -4,7 +4,6 @@ package software.aws.toolkits.jetbrains.services.cwc.inline.listeners import com.intellij.ide.DataManager -import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.Presentation import com.intellij.openapi.editor.Editor diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt index 06f067bd2f9..f5923e7a672 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt @@ -9,7 +9,6 @@ import com.intellij.openapi.fileEditor.FileEditorManagerEvent import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Disposer class InlineChatFileListener(project: Project) : FileEditorManagerListener { private var currentEditor: Editor? = null diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatSelectionListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatSelectionListener.kt index 2024013dae5..106e672486d 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatSelectionListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatSelectionListener.kt @@ -7,7 +7,6 @@ import com.intellij.openapi.Disposable import com.intellij.openapi.editor.event.SelectionEvent import com.intellij.openapi.editor.event.SelectionListener import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatEditorHint -import java.awt.Point class InlineChatSelectionListener : SelectionListener, Disposable { private val inlineChatEditorHint = InlineChatEditorHint() diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/inlineChat/InlineChatControllerTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/inlineChat/InlineChatControllerTest.kt deleted file mode 100644 index 63b5670da1f..00000000000 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/inlineChat/InlineChatControllerTest.kt +++ /dev/null @@ -1,4 +0,0 @@ -package software.aws.toolkits.jetbrains.services.amazonq.inlineChat - -class InlineChatControllerTest { -} diff --git a/plugins/core/jetbrains-community/resources/icons/logos/Amazon_Q_grey.svg b/plugins/core/jetbrains-community/resources/icons/logos/Amazon_Q_grey.svg index fdbfcc26d71..f6a94b98b5f 100644 --- a/plugins/core/jetbrains-community/resources/icons/logos/Amazon_Q_grey.svg +++ b/plugins/core/jetbrains-community/resources/icons/logos/Amazon_Q_grey.svg @@ -1,4 +1,4 @@ - + diff --git a/plugins/core/jetbrains-community/resources/icons/misc/Q_inlineChat_shortcut.svg b/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_shortcut.svg similarity index 100% rename from plugins/core/jetbrains-community/resources/icons/misc/Q_inlineChat_shortcut.svg rename to plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_shortcut.svg diff --git a/plugins/core/jetbrains-community/src/icons/AwsIcons.kt b/plugins/core/jetbrains-community/src/icons/AwsIcons.kt index 8f55089f67c..8af5f335663 100644 --- a/plugins/core/jetbrains-community/src/icons/AwsIcons.kt +++ b/plugins/core/jetbrains-community/src/icons/AwsIcons.kt @@ -32,7 +32,7 @@ object AwsIcons { @JvmField val AWS_Q = load("/icons/logos/AWS_Q.svg") // 13x13 - @JvmField val AWS_Q_GREY = load("/icons/logos/Amazon_Q_grey.svg") + @JvmField val AWS_Q_GREY = load("/icons/logos/Amazon_Q_grey.svg") // 16x16 @JvmField val AWS_Q_GRADIENT = load("/icons/logos/Amazon-Q-Icon_Gradient_Large.svg") // 54x54 @@ -59,8 +59,6 @@ object AwsIcons { @JvmField val CSHARP = load("/icons/misc/csharp.svg") // 16x16 @JvmField val NEW = load("/icons/misc/new.svg") // 16x16 - - @JvmField val AWS_Q_INLINECHAT_SHORTCUT = load("/icons/misc/Q_inlineChat_shortcut.svg") } object Resources { @@ -130,8 +128,12 @@ object AwsIcons { object InlineChat { @JvmField val CONFIRM = load("/icons/resources/inlinechat/amazonq_inline_chat_confirm_icon.svg") + @JvmField val CANCEL = load("/icons/resources/inlinechat/amazonq_inline_chat_cancel_icon.svg") + @JvmField val REJECT = load("/icons/resources/inlinechat/amazonq_inline_chat_reject.svg") + + @JvmField val AWS_Q_INLINECHAT_SHORTCUT = load("/icons/resources/inlinechat/amazonq_inline_chat_shortcut.svg") } } From f09a5fd053c5668e5c9658b390b06b5058b449b3 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Thu, 17 Oct 2024 08:21:56 -0700 Subject: [PATCH 17/39] detekt --- .../chat/messenger/ChatPromptHandler.kt | 1 + .../chat/telemetry/TelemetryHelper.kt | 37 ++-- .../project/ProjectContextEditorListener.kt | 2 +- .../cwc/inline/InlineChatController.kt | 195 +++++++++--------- .../cwc/inline/InlineChatEditorHint.kt | 28 ++- .../inline/InlineChatGutterIconRenderer.kt | 5 +- .../cwc/inline/InlineChatPopupFactory.kt | 30 ++- .../cwc/inline/InlineChatPopupPanel.kt | 60 +++--- .../cwc/inline/OpenChatInputAction.kt | 16 +- .../cwc/inline/listeners/ChatCaretListener.kt | 23 +-- 10 files changed, 188 insertions(+), 209 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt index 8a289d18f27..592ddb3968c 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt @@ -141,6 +141,7 @@ class ChatPromptHandler(private val telemetryHelper: TelemetryHelper) { if(isInlineChat) processChatEvent(tabId, triggerId, responseEvent, shouldAddIndexInProgressMessage)?.let { emit(it) } } .collect { responseEvent -> + if(!isInlineChat) processChatEvent( tabId, triggerId, diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt index 695c9811b61..17e689f6e1f 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt @@ -14,7 +14,6 @@ import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.info import software.aws.toolkits.core.utils.warn -import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomization import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator @@ -148,22 +147,26 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: } } - fun recordInlineChatTelemetry(requestId: String, - inputLength: Int?, - numSelectedLines: Int?, - codeIntent: Boolean?, - userDecision: InlineChatUserDecision?, - responseStartLatency: Double?, - responseEndLatency: Double?, - numSuggestionAddChars: Int?, - numSuggestionAddLines: Int?, - numSuggestionDelChars: Int?, - numSuggestionDelLines: Int?, - charactersAdded: Int?, - charactersRemoved: Int?) { - CodeWhispererClientAdaptor.getInstance(project).sendInlineChatTelemetry(requestId, inputLength, numSelectedLines, codeIntent, userDecision, - responseStartLatency, responseEndLatency,numSuggestionAddChars, numSuggestionAddLines, numSuggestionDelChars, numSuggestionDelLines, - charactersAdded, charactersRemoved).also { + fun recordInlineChatTelemetry( + requestId: String, + inputLength: Int?, + numSelectedLines: Int?, + codeIntent: Boolean?, + userDecision: InlineChatUserDecision?, + responseStartLatency: Double?, + responseEndLatency: Double?, + numSuggestionAddChars: Int?, + numSuggestionAddLines: Int?, + numSuggestionDelChars: Int?, + numSuggestionDelLines: Int?, + charactersAdded: Int?, + charactersRemoved: Int? + ) { + CodeWhispererClientAdaptor.getInstance(project).sendInlineChatTelemetry( + requestId, inputLength, numSelectedLines, codeIntent, userDecision, + responseStartLatency, responseEndLatency, numSuggestionAddChars, numSuggestionAddLines, numSuggestionDelChars, numSuggestionDelLines, + charactersAdded, charactersRemoved + ).also { logger.debug { "Successfully sendTelemetryEvent for InlineChat with requestId=${it.responseMetadata().requestId()}" } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/project/ProjectContextEditorListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/project/ProjectContextEditorListener.kt index 235c876d3f2..8997254f7d0 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/project/ProjectContextEditorListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/project/ProjectContextEditorListener.kt @@ -7,7 +7,7 @@ import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.vfs.VirtualFile import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererSettings -class ProjectContextEditorListener() : FileEditorManagerListener { +class ProjectContextEditorListener : FileEditorManagerListener { override fun fileClosed(source: FileEditorManager, file: VirtualFile) { if (CodeWhispererSettings.getInstance().isProjectContextEnabled()) { ProjectContextController.getInstance(source.project).updateIndex(file.path) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 462878acefc..99e505fc00d 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -7,7 +7,6 @@ import com.github.difflib.text.DiffRow import com.github.difflib.text.DiffRowGenerator import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.EDT import com.intellij.openapi.application.ReadAction import com.intellij.openapi.application.invokeLater import com.intellij.openapi.application.runInEdt @@ -34,7 +33,6 @@ import com.intellij.ui.JBColor import com.intellij.ui.jcef.JBCefApp import com.jetbrains.rd.util.AtomicInteger import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.onEach @@ -111,7 +109,8 @@ class InlineChatController( var charactersRemoved: Int? = null, ) - private val popupSubmitHandler: suspend (String, String, Int, Editor) -> String = { prompt: String, selectedCode: String, selectedLineStart: Int, editor: Editor -> + private val popupSubmitHandler: suspend (String, String, Int, Editor) -> String = { + prompt: String, selectedCode: String, selectedLineStart: Int, editor: Editor -> runBlocking { isInProgress.set(true) val message = handleChat(prompt, selectedCode, editor, selectedLineStart) @@ -121,7 +120,7 @@ class InlineChatController( val popupCancelHandler: () -> Unit = { if (isPopupAborted.get() && currentPopup != null) { - scope.launch(Dispatchers.EDT) { + scope.launch(EDT) { while (partialUndoActions.isNotEmpty()) { val action = partialUndoActions.pop() runChangeAction(project, action) @@ -136,7 +135,7 @@ class InlineChatController( } private fun recordInlineChatTelemetry(decision: InlineChatUserDecision) { - if(metrics == null) return + if (metrics == null) return metrics?.userDecision = decision if (decision == InlineChatUserDecision.ACCEPT) { metrics?.charactersAdded = metrics?.numSuggestionAddChars @@ -163,32 +162,31 @@ class InlineChatController( } private val diffAcceptHandler: () -> Unit = { - scope.launch(Dispatchers.EDT) { + scope.launch(EDT) { partialUndoActions.clear() - while (partialAcceptActions.isNotEmpty()) { - val action = partialAcceptActions.pop() - runChangeAction(project, action) - } + while (partialAcceptActions.isNotEmpty()) { + val action = partialAcceptActions.pop() + runChangeAction(project, action) + } invokeLater { hidePopup() } } ApplicationManager.getApplication().executeOnPooledThread { recordInlineChatTelemetry(InlineChatUserDecision.ACCEPT) } - } - private val diffRejectHandler: () -> Unit = { - scope.launch(Dispatchers.EDT) { - while (partialUndoActions.isNotEmpty()) { - val action = partialUndoActions.pop() - runChangeAction(project, action) - } - partialAcceptActions.clear() - invokeLater { hidePopup() } + private val diffRejectHandler: () -> Unit = { + scope.launch(EDT) { + while (partialUndoActions.isNotEmpty()) { + val action = partialUndoActions.pop() + runChangeAction(project, action) } - ApplicationManager.getApplication().executeOnPooledThread { + partialAcceptActions.clear() + invokeLater { hidePopup() } + } + ApplicationManager.getApplication().executeOnPooledThread { recordInlineChatTelemetry(InlineChatUserDecision.REJECT) - } + } } private fun addPopupListeners(popup: JBPopup) { @@ -203,19 +201,18 @@ class InlineChatController( popup.addListener(popupListener) } - - fun initPopup (editor: Editor) { + fun initPopup(editor: Editor) { currentPopup?.let { Disposer.dispose(it) } - currentPopup = InlineChatPopupFactory(acceptHandler = diffAcceptHandler, rejectHandler = diffRejectHandler, editor = editor, - submitHandler = popupSubmitHandler, cancelHandler = popupCancelHandler).createPopup(scope) + currentPopup = InlineChatPopupFactory( + acceptHandler = diffAcceptHandler, rejectHandler = diffRejectHandler, editor = editor, + submitHandler = popupSubmitHandler, cancelHandler = popupCancelHandler + ).createPopup(scope) addPopupListeners(currentPopup!!) Disposer.register(this, currentPopup!!) isPopupAborted.set(true) - } private fun highlightCodeWithBackgroundColor(editor: Editor, startOffset: Int, endOffset: Int, isGreen: Boolean) { - val greenBackgroundAttributes = TextAttributes().apply { backgroundColor = JBColor(0x66BB6A, 0x006400) effectColor = JBColor(0x66BB6A, 0x006400) @@ -226,7 +223,7 @@ class InlineChatController( effectColor = JBColor(0xEF9A9A, 0x8B0000) } val attributes = if (isGreen) greenBackgroundAttributes else redBackgroundAttributes - rangeHighlighter= editor.markupModel.addRangeHighlighter( + rangeHighlighter = editor.markupModel.addRangeHighlighter( startOffset, endOffset, HighlighterLayer.SELECTION + 1, attributes, HighlighterTargetArea.EXACT_RANGE ) @@ -241,7 +238,6 @@ class InlineChatController( } } - private fun hidePopup() { isPopupAborted.set(false) currentPopup?.closeOk(null) @@ -261,17 +257,15 @@ class InlineChatController( while (currentIndex < src.length) { val startIndex = src.indexOf("```", currentIndex) - if (startIndex == -1) break + if (startIndex == -1) return codeBlocks val endIndex = src.indexOf("```", startIndex + 3) - if (endIndex == -1) break + if (endIndex == -1) return codeBlocks val code = src.substring(startIndex + 3, endIndex) codeBlocks.add(code) - currentIndex = endIndex + 3 } - return codeBlocks } @@ -281,18 +275,15 @@ class InlineChatController( return rows } - - private fun unescape(s: String): String { - return StringEscapeUtils.unescapeHtml3(s) - .replace(""", "\"") - .replace("'", "'") - .replace("=>", "=>") - } + private fun unescape(s: String): String = StringEscapeUtils.unescapeHtml3(s) + .replace(""", "\"") + .replace("'", "'") + .replace("=>", "=>") private suspend fun processChatMessage(selectedCode: String, event: ChatMessage, editor: Editor, selectedLineStart: Int, prevMessage: String) { - if(event.message?.isNotEmpty() == true) { + if (event.message?.isNotEmpty() == true) { val codeBlocks = getCodeBlocks(event.message) - if(codeBlocks.isEmpty()) { + if (codeBlocks.isEmpty()) { logger.info { "No code block found in inline chat response with requestId: ${event.messageId}" } return } @@ -308,7 +299,7 @@ class InlineChatController( selectionStartLine = AtomicInteger(selectedLineStart) var currentDocumentLine = selectedLineStart var insertLine = selectedLineStart - if(event.codeReference?.isNotEmpty() == true) { + if (event.codeReference?.isNotEmpty() == true) { editor.project?.let { ReferenceLogController.addReferenceLog(recommendation, event.codeReference, editor, it) } } @@ -336,14 +327,14 @@ class InlineChatController( showCodeChangeInEditor(row, currentDocumentLine, editor) if (row.tag == DiffRow.Tag.CHANGE) { - insertLine+=2 - currentDocumentLine+=2 + insertLine += 2 + currentDocumentLine += 2 } else { insertLine++ currentDocumentLine++ } deletedLinesCount++ - deletedCharsCount += row.oldLine?.length?: 0 + deletedCharsCount += row.oldLine?.length ?: 0 } DiffRow.Tag.INSERT -> { isAllEqual = false @@ -351,7 +342,7 @@ class InlineChatController( insertLine++ addedLinesCount++ - addedCharsCount += row.newLine?.length?: 0 + addedCharsCount += row.newLine?.length ?: 0 } } } @@ -368,7 +359,7 @@ class InlineChatController( } else { if (event.messageType == ChatMessageType.Answer) { val codeBlocks = getCodeBlocks(prevMessage) - if(codeBlocks.isEmpty()) { + if (codeBlocks.isEmpty()) { logger.warn { "No code block found in inline chat response with requestId: ${event.messageId} \nresponse: ${event.message}" } isInProgress.set(false) throw Exception("No recommendation provided. Please try again with a different question.") @@ -377,8 +368,7 @@ class InlineChatController( } } - - private suspend fun insertNewLineIfNeeded(row: Int, editor: Editor) : Int { + private suspend fun insertNewLineIfNeeded(row: Int, editor: Editor): Int { var newLineInserted = 0 while (row > editor.document.lineCount - 1) { insertString(editor, editor.document.textLength, "\n") @@ -387,36 +377,31 @@ class InlineChatController( return newLineInserted } - private fun getLineStartOffset(document: Document, row: Int): Int { - return ReadAction.compute { - document.getLineStartOffset(row) - } + private fun getLineStartOffset(document: Document, row: Int): Int = ReadAction.compute { + document.getLineStartOffset(row) } - private fun getLineEndOffset(document: Document, row: Int): Int { - return ReadAction.compute { - if (row == document.lineCount - 1) { - document.getLineEndOffset(row) - } else { - document.getLineEndOffset(row) + 1 - } + private fun getLineEndOffset(document: Document, row: Int): Int = ReadAction.compute { + if (row == document.lineCount - 1) { + document.getLineEndOffset(row) + } else { + document.getLineEndOffset(row) + 1 } } - private suspend fun runChangeAction(project: Project, action: () -> Unit, shouldRecordForUndo: Boolean = false) { + private suspend fun runChangeAction(project: Project, action: () -> Unit) { withContext(EDT) { CommandProcessor.getInstance().executeCommand(project, { - ApplicationManager.getApplication().runWriteAction { - WriteCommandAction.runWriteCommandAction(project) { - action() - } + ApplicationManager.getApplication().runWriteAction { + WriteCommandAction.runWriteCommandAction(project) { + action() } - + } }, "", null, UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, false) } } - private suspend fun insertString(editor: Editor, offset: Int, text: String) : RangeMarker { + private suspend fun insertString(editor: Editor, offset: Int, text: String): RangeMarker { var rangeMarker: RangeMarker? = null val action = { editor.document.insertString(offset, text) @@ -436,7 +421,7 @@ class InlineChatController( runChangeAction(project, action) } - private suspend fun highlightString(editor: Editor, start: Int, end: Int, isInsert: Boolean) : RangeMarker { + private suspend fun highlightString(editor: Editor, start: Int, end: Int, isInsert: Boolean): RangeMarker { var rangeMarker: RangeMarker? = null val action = { rangeMarker = editor.document.createRangeMarker(start, end) @@ -459,7 +444,7 @@ class InlineChatController( } partialAcceptActions.add { if (rangeMarker.isValid) { - scope.launch(Dispatchers.EDT) { + scope.launch(EDT) { deleteString(document, rangeMarker.startOffset, rangeMarker.endOffset) } } @@ -474,7 +459,7 @@ class InlineChatController( val rangeMarker = insertString(editor, insertOffset, textToInsert) partialUndoActions.add { if (rangeMarker.isValid) { - scope.launch(Dispatchers.EDT) { + scope.launch(EDT) { deleteString(document, rangeMarker.startOffset, rangeMarker.endOffset + newLineInserted) } } @@ -490,7 +475,7 @@ class InlineChatController( val changeEndOffset = getLineEndOffset(document, row) val oldTextRangeMarker = highlightString(editor, changeOffset, changeEndOffset, false) partialAcceptActions.add { - scope.launch(Dispatchers.EDT) { + scope.launch(EDT) { if (oldTextRangeMarker.isValid) { deleteString(document, oldTextRangeMarker.startOffset, oldTextRangeMarker.endOffset) } @@ -504,7 +489,7 @@ class InlineChatController( partialUndoActions.add { WriteCommandAction.runWriteCommandAction(project) { if (newTextRangeMarker.isValid) { - scope.launch(Dispatchers.EDT) { + scope.launch(EDT) { deleteString(document, newTextRangeMarker.startOffset, newTextRangeMarker.endOffset + newLineInserted) } } @@ -514,13 +499,12 @@ class InlineChatController( } } } catch (e: Exception) { - logger.warn {"Error when showing inline chat diff in editor: ${e.message} \n ${e.stackTraceToString()}"} + logger.warn { "Error when showing inline chat diff in editor: ${e.message} \n ${e.stackTraceToString()}" } throw Exception("Unexpected error, please try again.") } } - - private suspend fun handleChat (message: String, selectedCode: String = "", editor: Editor, selectedLineStart: Int) : String { + private suspend fun handleChat(message: String, selectedCode: String = "", editor: Editor, selectedLineStart: Int): String { val authController = AuthController() val credentialState = authController.getAuthNeededStates(project).chat if (credentialState != null) { @@ -547,20 +531,23 @@ class InlineChatController( // This is temporary. TODO: remove this after prompt added on service side var prompt = "" if (selectedCode.isNotBlank()) { - prompt = "You are a code transformation assistant. Your task is to modify a selection of lines from a given code file according to a specific instruction.\n"+ + prompt = "You are a code transformation assistant." + + " Your task is to modify a selection of lines from a given code file according to a specific instruction.\n" + "Follow these steps carefully:\n" + "- You will be given some selected code from a file to be transformed, enclosed in XML tags\n" + "- You will receive an instruction for how to transform the selected code, enclosed in XML tags\n" + "- You will be given the contents of that same file as context, enclosed in XML tags\n" - "- Your task is to:\n" + + "- Your task is to:\n" + "- Apply the transformation instruction to the selected code\n" + "- Ensure that the transformation is applied correctly and consistently\n" + "- Reuse existing functions and other code from the context wherever possible\n" + "- Important rules to follow:\n" + "- Maintain the original indentation of the selected code\n" + - "- If the instruction asks to provide explanations or answer questions about the code, add these as new comment lines above the relevant lines in the code; do not change the code lines themselves\n" + + "- If the instruction asks to provide explanations or answer questions about the code," + + " add these as new comment lines above the relevant lines in the code; do not change the code lines themselves\n" + "- If the instruction is unclear or cannot be applied, do not make any changes to the code\n" + - "- After performing the transformation, return the transformed code. If the transformation generates new code but does not modify the selected code, be sure to include the selected code in your response.\n" + "- After performing the transformation, return the transformed code." + + " If the transformation generates new code but does not modify the selected code, be sure to include the selected code in your response.\n" } else { prompt = "You are a coding assistant. Your task is to generate code according to an specific instruction.\n" + "Follow these steps carefully:\n" + @@ -570,7 +557,8 @@ class InlineChatController( "- Generate code according to the instruction\n" + "- Reuse existing functions and other code from the context wherever possible\n" + "- Important rules to follow:\n" + - "- If the instruction asks to provide explanations or answer questions about the code, add these as new comment lines above the relevant lines in the code; do not change the code lines themselves\n" + + "- If the instruction asks to provide explanations or answer questions about the code," + + " add these as new comment lines above the relevant lines in the code; do not change the code lines themselves\n" + "- If the instruction is unclear or cannot be applied, do not make any changes to the code\n" + "- After generating the code, return ONLY the new code you generated; do not include any existing lines of code from the context.\n" } @@ -603,29 +591,38 @@ class InlineChatController( var errorMessage = "" var prevMessage = "" val chat = sessionInfo.scope.async { - ChatPromptHandler(telemetryHelper).handle("inlineChat-editor", triggerId, requestData, sessionInfo, false, true) - .catch { e -> - logger.warn { "Error in inline chat request: ${e.message}" } - errorMessage = e.message ?: "" - } - .onEach{ event: ChatMessage -> - if (event.message?.isNotEmpty() == true && prevMessage != event.message) { - runBlocking { processChatMessage(selectedCode, event, editor, selectedLineStart, prevMessage) } - prevMessage = event.message + ChatPromptHandler(telemetryHelper).handle( + "inlineChat-editor", + triggerId, + requestData, + sessionInfo, + false, + true + ) + .catch { e -> + logger.warn { "Error in inline chat request: ${e.message}" } + errorMessage = e.message ?: "" } - if (messages.isEmpty()) { - firstResponseLatency = (System.currentTimeMillis() - startTime).toDouble() + .onEach { event: ChatMessage -> + if (event.message?.isNotEmpty() == true && prevMessage != event.message) { + runBlocking { processChatMessage(selectedCode, event, editor, selectedLineStart, prevMessage) } + prevMessage = event.message + } + if (messages.isEmpty()) { + firstResponseLatency = (System.currentTimeMillis() - startTime).toDouble() + } + messages.add(event) } - messages.add(event) - } - .toList() + .toList() } chat.await() val lastResponseLatency = (System.currentTimeMillis() - startTime).toDouble() val requestId = messages.lastOrNull()?.messageId - requestId?.let{ - metrics = InlineChatMetrics(requestId = it, inputLength = message.length, numSelectedLines = selectedCode.split("\n").size, - codeIntent = true, responseStartLatency = firstResponseLatency, responseEndLatency = lastResponseLatency) + requestId?.let { + metrics = InlineChatMetrics( + requestId = it, inputLength = message.length, numSelectedLines = selectedCode.split("\n").size, + codeIntent = true, responseStartLatency = firstResponseLatency, responseEndLatency = lastResponseLatency + ) } return errorMessage @@ -641,5 +638,3 @@ class InlineChatController( hidePopup() } } - - diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt index ea9956cc86d..36ec9981785 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt @@ -17,30 +17,29 @@ import java.awt.BorderLayout import java.awt.Point import javax.swing.JPanel - class InlineChatEditorHint { private val hint = createHint() - private val HINT_LOCATION_BUFFER = 50 + private val hintLocationBuffer = 50 - private fun getHintLocation (editor: Editor): Point { + private fun getHintLocation(editor: Editor): Point { val selectionModel = editor.selectionModel val lineSelected = selectionModel.selectedText?.split("\n")?.size - val offset: Int = if (lineSelected != null && lineSelected > 1) {(lineSelected - 1).times(editor.lineHeight)} else {0} + val offset: Int = if (lineSelected != null && lineSelected > 1) { (lineSelected - 1).times(editor.lineHeight) } else { 0 } val bestPosition = JBPopupFactory.getInstance().guessBestPopupLocation(editor).point val visibleArea = editor.scrollingModel.visibleArea - val adjustedX = (bestPosition.x + 200).coerceAtMost(visibleArea.x + visibleArea.width - HINT_LOCATION_BUFFER) - val adjustedY = (bestPosition.y + offset).coerceAtMost(visibleArea.y + visibleArea.height - HINT_LOCATION_BUFFER) + val adjustedX = (bestPosition.x + 200).coerceAtMost(visibleArea.x + visibleArea.width - hintLocationBuffer) + val adjustedY = (bestPosition.y + offset).coerceAtMost(visibleArea.y + visibleArea.height - hintLocationBuffer) val adjustedPosition = Point(adjustedX, adjustedY) return adjustedPosition } - private fun createHint (): LightweightHint{ + private fun createHint(): LightweightHint { val icon = AwsIcons.Logos.AWS_Q_GREY val component = HintUtil.createInformationComponent() - component.isIconOnTheRight = false; + component.isIconOnTheRight = false component.icon = icon val coloredText = SimpleColoredText(message("amazonqInlineChat.hint.edit"), SimpleTextAttributes.REGULAR_ATTRIBUTES) @@ -49,7 +48,7 @@ class InlineChatEditorHint { val shortcutComponent = HintUtil.createInformationComponent() if (!SystemInfo.isWindows) { val shortCutIcon = AwsIcons.Resources.InlineChat.AWS_Q_INLINECHAT_SHORTCUT - shortcutComponent.isIconOnTheRight = true; + shortcutComponent.isIconOnTheRight = true shortcutComponent.icon = shortCutIcon } else { val shortcutText = @@ -72,9 +71,12 @@ class InlineChatEditorHint { fun show(editor: Editor) { val location = getHintLocation(editor) HintManagerImpl.getInstanceImpl().showEditorHint( - hint, editor, location, + hint, + editor, + location, HintManager.HIDE_BY_TEXT_CHANGE or HintManager.HIDE_BY_SCROLLING, - 0, false, + 0, + false, HintManagerImpl.createHintHint(editor, location, hint, HintManager.RIGHT_UNDER).setContentActive(false) ) } @@ -83,7 +85,3 @@ class InlineChatEditorHint { hint.hide() } } - - - - diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt index b7c519752ae..ee17b2959c8 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt @@ -10,7 +10,7 @@ import com.intellij.openapi.editor.markup.GutterIconRenderer import software.aws.toolkits.resources.AmazonQBundle.message import javax.swing.Icon -class InlineChatGutterIconRenderer (private val icon: Icon) : GutterIconRenderer() { +class InlineChatGutterIconRenderer(private val icon: Icon) : GutterIconRenderer() { private var clickAction: (() -> Unit)? = null override fun equals(other: Any?): Boolean { if (other is InlineChatGutterIconRenderer) { @@ -32,7 +32,7 @@ class InlineChatGutterIconRenderer (private val icon: Icon) : GutterIconRenderer override fun update(e: AnActionEvent) = Unit } - fun setClickAction (action: () -> Unit) { + fun setClickAction(action: () -> Unit) { clickAction = action } @@ -40,4 +40,3 @@ class InlineChatGutterIconRenderer (private val icon: Icon) : GutterIconRenderer override fun getAlignment(): Alignment = Alignment.CENTER } - diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index bea9a0db84c..900390d2a3f 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -20,7 +20,6 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhisperer import software.aws.toolkits.resources.AmazonQBundle.message import java.awt.Point - class InlineChatPopupFactory( private val editor: Editor, private val submitHandler: suspend (String, String, Int, Editor) -> String, @@ -29,22 +28,20 @@ class InlineChatPopupFactory( private val cancelHandler: () -> Unit, ) : Disposable { - private fun getSelectedText(editor: Editor): String { - return ReadAction.compute { - val selectionStartOffset = editor.selectionModel.selectionStart - val selectionEndOffset = editor.selectionModel.selectionEnd - if (selectionEndOffset > selectionStartOffset) { - val selectionLineStart = editor.document.getLineStartOffset(editor.document.getLineNumber(selectionStartOffset)) - val selectionLineEnd = editor.document.getLineEndOffset(editor.document.getLineNumber(selectionEndOffset)) - editor.document.getText(TextRange(selectionLineStart, selectionLineEnd)) - } else "" + private fun getSelectedText(editor: Editor): String = ReadAction.compute { + val selectionStartOffset = editor.selectionModel.selectionStart + val selectionEndOffset = editor.selectionModel.selectionEnd + if (selectionEndOffset > selectionStartOffset) { + val selectionLineStart = editor.document.getLineStartOffset(editor.document.getLineNumber(selectionStartOffset)) + val selectionLineEnd = editor.document.getLineEndOffset(editor.document.getLineNumber(selectionEndOffset)) + editor.document.getText(TextRange(selectionLineStart, selectionLineEnd)) + } else { + "" } } - private fun getSelectionStartLine(editor: Editor): Int { - return ReadAction.compute { - editor.document.getLineNumber(editor.selectionModel.selectionStart) - } + private fun getSelectionStartLine(editor: Editor): Int = ReadAction.compute { + editor.document.getLineNumber(editor.selectionModel.selectionStart) } fun createPopup(scope: CoroutineScope): JBPopup { @@ -78,7 +75,7 @@ class InlineChatPopupFactory( val rejectAction = { rejectHandler.invoke() } - addCodeActionsPanel(acceptAction , rejectAction) + addCodeActionsPanel(acceptAction, rejectAction) } } } @@ -92,7 +89,7 @@ class InlineChatPopupFactory( } private fun showPopupInEditor(popup: JBPopup, popupPanel: InlineChatPopupPanel, editor: Editor) { - val popupHeight = popupPanel.POPUP_HEIGHT + val popupHeight = popupPanel.popupHeight val editorComponent = editor.component val locationOnScreen = editorComponent.locationOnScreen val popupPoint = JBPopupFactory.getInstance().guessBestPopupLocation(editor).point @@ -146,4 +143,3 @@ class InlineChatPopupFactory( cancelHandler.invoke() } } - diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index a06e2b210fb..6a442771f4c 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -29,20 +29,19 @@ import javax.swing.SwingConstants class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { private var submitClickListener: (() -> Unit)? = null - private val POPUP_BUTTON_FONT_SIZE = 14f - private val POPUP_WIDTH = 600 - val POPUP_HEIGHT = 90 - private val POPUP_BUTTON_HEIGHT = 30 - private val POPUP_BUTTON_WIDTH = 80 - private val POPUP_INPUT_HEIGHT = 40 - private val POPUP_INPUT_WIDTH = 500 - + private val popupButtonFontSize = 14f + private val popupWidth = 600 + val popupHeight = 90 + private val popupButtonHeight = 30 + private val popupButtonWidth = 80 + private val popupInputHeight = 40 + private val popupInputWidth = 500 val textField = createTextField() val submitButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, message("amazonqInlineChat.popup.confirm")) - private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CANCEL, message("amazonqInlineChat.popup.cancel")).apply { + private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CANCEL, message("amazonqInlineChat.popup.cancel")).apply { addActionListener { Disposer.dispose(parentDisposable) } } @@ -55,11 +54,11 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() private val acceptButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, message("amazonqInlineChat.popup.accept")) private val rejectButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.REJECT, message("amazonqInlineChat.popup.reject")) private val textLabel = JLabel(message("amazonqInlineChat.popup.editCode"), AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { - font = font.deriveFont(POPUP_BUTTON_FONT_SIZE) + font = font.deriveFont(popupButtonFontSize) } private val errorLabel = JLabel("", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { - font = font.deriveFont(POPUP_BUTTON_FONT_SIZE) + font = font.deriveFont(popupButtonFontSize) } private val inputPanel = JPanel(BorderLayout()).apply { @@ -76,29 +75,25 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() add(buttonsPanel, BorderLayout.EAST) } - override fun getPreferredSize(): Dimension { - return Dimension(POPUP_WIDTH, POPUP_HEIGHT) - } + override fun getPreferredSize(): Dimension = Dimension(popupWidth, popupHeight) - private fun createTextField() : JTextField = JTextField().apply { + private fun createTextField(): JTextField = JTextField().apply { val editorColorsScheme = EditorColorsManager.getInstance().globalScheme - preferredSize = Dimension(POPUP_INPUT_WIDTH, POPUP_INPUT_HEIGHT) + preferredSize = Dimension(popupInputWidth, popupInputHeight) border = IdeBorderFactory.createRoundedBorder().apply { setColor(POPUP_BUTTON_BORDER) } font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } - private fun createButtonWithIcon(icon: Icon, text: String): JButton { - return JButton(text).apply { - horizontalTextPosition = SwingConstants.LEFT - preferredSize = Dimension(POPUP_BUTTON_WIDTH, POPUP_BUTTON_HEIGHT) - setIcon(icon) - isOpaque = false - isContentAreaFilled = false - isBorderPainted = false - font = font.deriveFont(POPUP_BUTTON_FONT_SIZE) - } + private fun createButtonWithIcon(icon: Icon, text: String): JButton = JButton(text).apply { + horizontalTextPosition = SwingConstants.LEFT + preferredSize = Dimension(popupButtonWidth, popupButtonHeight) + setIcon(icon) + isOpaque = false + isContentAreaFilled = false + isBorderPainted = false + font = font.deriveFont(popupButtonFontSize) } init { @@ -125,17 +120,14 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() Disposer.register(parentDisposable, restorer) } - private fun getEditorActionHandler(action: () -> Unit) : EditorActionHandler { - val handler = object : EditorActionHandler() { - override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { - action.invoke() - Disposer.dispose(parentDisposable) - } + private fun getEditorActionHandler(action: () -> Unit): EditorActionHandler = object : EditorActionHandler() { + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { + action.invoke() + Disposer.dispose(parentDisposable) } - return handler } - fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit ) { + fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit) { textLabel.text = message("amazonqInlineChat.popup.editCode") // this is a workaround somehow the textField will interfere with the enter handler val emptyTextField = createTextField() diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt index c15534e4f3a..86af0904377 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt @@ -10,7 +10,7 @@ import com.intellij.openapi.editor.event.CaretEvent import com.intellij.openapi.editor.event.CaretListener import org.jetbrains.plugins.terminal.exp.TerminalDataContextUtils.editor -class OpenChatInputAction : AnAction() { +class OpenChatInputAction : AnAction() { private var inlineChatController: InlineChatController? = null private var caretListener: CaretListener? = null override fun actionPerformed(e: AnActionEvent) { @@ -25,15 +25,13 @@ class OpenChatInputAction : AnAction() { } } - private fun createCaretListener(editor: Editor): CaretListener { - return object : CaretListener { - override fun caretPositionChanged(event: CaretEvent) { - inlineChatController?.disposePopup() + private fun createCaretListener(editor: Editor): CaretListener = object : CaretListener { + override fun caretPositionChanged(event: CaretEvent) { + inlineChatController?.disposePopup() - editor.caretModel.removeCaretListener(this) - caretListener = null - inlineChatController = null - } + editor.caretModel.removeCaretListener(this) + caretListener = null + inlineChatController = null } } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt index 7bd89e6c438..8142d46d5b7 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt @@ -21,19 +21,16 @@ import software.aws.toolkits.jetbrains.services.cwc.inline.OpenChatInputAction class ChatCaretListener : CaretListener { private var currentHighlighter: RangeHighlighter? = null - - private fun createGutterIconRenderer(editor: Editor): GutterIconRenderer{ - return InlineChatGutterIconRenderer(AwsIcons.Logos.AWS_Q_GREY).apply { - setClickAction { - val action = OpenChatInputAction() - val dataContext = DataManager.getInstance().getDataContext(editor.component) - val e = AnActionEvent.createFromDataContext( - "GutterIconClick", - Presentation(), - dataContext - ) - action.actionPerformed(e) - } + private fun createGutterIconRenderer(editor: Editor): GutterIconRenderer = InlineChatGutterIconRenderer(AwsIcons.Logos.AWS_Q_GREY).apply { + setClickAction { + val action = OpenChatInputAction() + val dataContext = DataManager.getInstance().getDataContext(editor.component) + val e = AnActionEvent.createFromDataContext( + "GutterIconClick", + Presentation(), + dataContext + ) + action.actionPerformed(e) } } From 611bf643adf0f74650ce4c754c8988ada2c22a1d Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Thu, 17 Oct 2024 08:35:10 -0700 Subject: [PATCH 18/39] rebase --- .../amazonq/startup/AmazonQStartupActivity.kt | 2 +- .../chat/messenger/ChatPromptHandler.kt | 21 ++++++++++--------- .../chat/telemetry/TelemetryHelper.kt | 2 +- .../cwc/inline/InlineChatController.kt | 2 +- .../credentials/CodeWhispererClientAdaptor.kt | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt index a9b9e15f79e..fb9385888e2 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt @@ -20,9 +20,9 @@ import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWi import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererSettings import software.aws.toolkits.jetbrains.services.cwc.editor.context.project.ProjectContextController +import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatController import java.lang.management.ManagementFactory import java.time.Duration -import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatController import java.util.concurrent.atomic.AtomicBoolean class AmazonQStartupActivity : ProjectActivity { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt index 592ddb3968c..b0f49af3fee 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt @@ -56,7 +56,7 @@ class ChatPromptHandler(private val telemetryHelper: TelemetryHelper) { data: ChatRequestData, sessionInfo: ChatSessionInfo, shouldAddIndexInProgressMessage: Boolean, - isInlineChat: Boolean = false + isInlineChat: Boolean = false, ) = flow { val session = sessionInfo.session session.chat(data) @@ -138,17 +138,18 @@ class ChatPromptHandler(private val telemetryHelper: TelemetryHelper) { } } .onEach { responseEvent -> - if(isInlineChat) processChatEvent(tabId, triggerId, responseEvent, shouldAddIndexInProgressMessage)?.let { emit(it) } + if (isInlineChat) processChatEvent(tabId, triggerId, responseEvent, shouldAddIndexInProgressMessage)?.let { emit(it) } } .collect { responseEvent -> - if(!isInlineChat) - processChatEvent( - tabId, - triggerId, - data, - responseEvent, - shouldAddIndexInProgressMessage - )?.let { emit(it) } + if (!isInlineChat) { + processChatEvent( + tabId, + triggerId, + data, + responseEvent, + shouldAddIndexInProgressMessage + )?.let { emit(it) } + } } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt index 17e689f6e1f..9dfcb76ef5b 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt @@ -160,7 +160,7 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: numSuggestionDelChars: Int?, numSuggestionDelLines: Int?, charactersAdded: Int?, - charactersRemoved: Int? + charactersRemoved: Int?, ) { CodeWhispererClientAdaptor.getInstance(project).sendInlineChatTelemetry( requestId, inputLength, numSelectedLines, codeIntent, userDecision, diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 99e505fc00d..d8c5c649a7b 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -73,7 +73,7 @@ import java.util.concurrent.atomic.AtomicBoolean @Service(Service.Level.PROJECT) class InlineChatController( private val project: Project, - private val scope: CoroutineScope + private val scope: CoroutineScope, ) : Disposable { private var currentPopup: JBPopup? = null private var rangeHighlighter: RangeHighlighter? = null diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt index 921a0033780..556bfeee9c3 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt @@ -578,7 +578,7 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW numSuggestionDelChars: Int?, numSuggestionDelLines: Int?, charactersAdded: Int?, - charactersRemoved: Int? + charactersRemoved: Int?, ): SendTelemetryEventResponse = bearerClient().sendTelemetryEvent { requestBuilder -> requestBuilder.telemetryEvent { telemetryEventBuilder -> telemetryEventBuilder.inlineChatEvent { From 1c9a9281c606da131a4221d553417dcf0eb4854a Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Thu, 17 Oct 2024 09:31:38 -0700 Subject: [PATCH 19/39] fix after rebase --- .../chat/messenger/ChatPromptHandler.kt | 2 +- .../chat/telemetry/TelemetryHelper.kt | 24 +++++++------------ .../cwc/inline/InlineChatController.kt | 7 +++--- .../cwc/inline/InlineChatPopupPanel.kt | 9 +++++-- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt index b0f49af3fee..0b6c01b805b 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/messenger/ChatPromptHandler.kt @@ -138,7 +138,7 @@ class ChatPromptHandler(private val telemetryHelper: TelemetryHelper) { } } .onEach { responseEvent -> - if (isInlineChat) processChatEvent(tabId, triggerId, responseEvent, shouldAddIndexInProgressMessage)?.let { emit(it) } + if (isInlineChat) processChatEvent(tabId, triggerId, data, responseEvent, shouldAddIndexInProgressMessage)?.let { emit(it) } } .collect { responseEvent -> if (!isInlineChat) { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt index 9dfcb76ef5b..b0260e017e7 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt @@ -90,7 +90,7 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: cwsprChatUserIntent = data.userIntent?.let { getTelemetryUserIntent(it) }, cwsprChatHasCodeSnippet = data.activeFileContext.focusAreaContext?.codeSelection?.isNotEmpty() ?: false, cwsprChatProgrammingLanguage = data.activeFileContext.fileContext?.fileLanguage, - credentialStartUrl = getStartUrl(context.project), + credentialStartUrl = getStartUrl(project), cwsprChatHasProjectContext = getIsProjectContextEnabled() && data.useRelevantDocuments && data.relevantTextDocuments.isNotEmpty() ) } @@ -140,11 +140,7 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: numberOfCodeBlocks, getMessageHasProjectContext(response.messageId), data.customization - ).also { - logger.debug { - "Successfully sendTelemetryEvent for ChatAddMessage with requestId=${it.responseMetadata().requestId()}" - } - } + ) } fun recordInlineChatTelemetry( @@ -166,11 +162,7 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: requestId, inputLength, numSelectedLines, codeIntent, userDecision, responseStartLatency, responseEndLatency, numSuggestionAddChars, numSuggestionAddLines, numSuggestionDelChars, numSuggestionDelLines, charactersAdded, charactersRemoved - ).also { - logger.debug { - "Successfully sendTelemetryEvent for InlineChat with requestId=${it.responseMetadata().requestId()}" - } - } + ) } fun recordMessageResponseError(data: ChatRequestData, tabId: String, responseCode: Int) { @@ -201,7 +193,7 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: "downvote" -> CwsprChatInteractionType.Downvote else -> CwsprChatInteractionType.Unknown }, - credentialStartUrl = getStartUrl(context.project), + credentialStartUrl = getStartUrl(project), cwsprChatHasProjectContext = getMessageHasProjectContext(message.messageId) ) ChatInteractWithMessageEvent.builder().apply { @@ -223,7 +215,7 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: cwsprChatConversationId = getConversationId(message.tabId).orEmpty(), cwsprChatMessageId = message.messageId.orEmpty(), cwsprChatInteractionType = CwsprChatInteractionType.ClickFollowUp, - credentialStartUrl = getStartUrl(context.project), + credentialStartUrl = getStartUrl(project), cwsprChatHasProjectContext = getMessageHasProjectContext(message.messageId.orEmpty()) ) ChatInteractWithMessageEvent.builder().apply { @@ -243,7 +235,7 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: cwsprChatAcceptedCharactersLength = message.code.length.toLong(), cwsprChatInteractionTarget = message.insertionTargetType, cwsprChatHasReference = null, - credentialStartUrl = getStartUrl(context.project), + credentialStartUrl = getStartUrl(project), cwsprChatCodeBlockIndex = message.codeBlockIndex?.toLong(), cwsprChatTotalCodeBlocks = message.totalCodeBlocks?.toLong(), cwsprChatHasProjectContext = getMessageHasProjectContext(message.messageId), @@ -269,7 +261,7 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: cwsprChatAcceptedNumberOfLines = message.code.lines().size.toLong(), cwsprChatInteractionTarget = message.insertionTargetType, cwsprChatHasReference = null, - credentialStartUrl = getStartUrl(context.project), + credentialStartUrl = getStartUrl(project), cwsprChatCodeBlockIndex = message.codeBlockIndex?.toLong(), cwsprChatTotalCodeBlocks = message.totalCodeBlocks?.toLong(), cwsprChatHasProjectContext = getMessageHasProjectContext(message.messageId), @@ -301,7 +293,7 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: cwsprChatInteractionType = linkInteractionType, cwsprChatInteractionTarget = message.link, cwsprChatHasReference = null, - credentialStartUrl = getStartUrl(context.project), + credentialStartUrl = getStartUrl(project), cwsprChatHasProjectContext = getMessageHasProjectContext(message.messageId) ) ChatInteractWithMessageEvent.builder().apply { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index d8c5c649a7b..8d50804b18a 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -510,7 +510,7 @@ class InlineChatController( if (credentialState != null) { // handle auth if (!JBCefApp.isSupported()) { - requestCredentialsForQ(project) + requestCredentialsForQ(project, isReauth = false) } else { runInEdt { QWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.Q)) @@ -578,10 +578,11 @@ class InlineChatController( tabId = "inlineChat-editor", message = prompt, activeFileContext = fileContext, - userIntent = intentRecognizer.getUserIntentFromPromptChatMessage(message), + userIntent = intentRecognizer.getUserIntentFromPromptChatMessage(message, null), triggerType = TriggerType.Click, customization = CodeWhispererModelConfigurator.getInstance().activeCustomization(project), - relevantTextDocuments = emptyList() + relevantTextDocuments = emptyList(), + useRelevantDocuments = false ) val sessionInfo = sessionStorage.getSession("inlineChat-editor", project) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index 6a442771f4c..e2716b64452 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -57,10 +57,15 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() font = font.deriveFont(popupButtonFontSize) } - private val errorLabel = JLabel("", AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { + private val errorLabel = JLabel("").apply { font = font.deriveFont(popupButtonFontSize) } + private val errorPanel = JPanel(BorderLayout()).apply { + add(errorLabel, BorderLayout.CENTER) + border = BorderFactory.createEmptyBorder(10, 10, 10, 10) + } + private val inputPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(10, 10, 5, 10) } @@ -153,7 +158,7 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() fun setErrorMessage(text: String) { errorLabel.text = text - add(errorLabel, BorderLayout.CENTER) + add(errorPanel, BorderLayout.CENTER) remove(inputPanel) remove(bottomPanel) revalidate() From 22a1dd9de97e132e1849bcdfcabed0818bdb7207 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Thu, 17 Oct 2024 11:42:48 -0700 Subject: [PATCH 20/39] small fix --- .../cwc/inline/InlineChatController.kt | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 8d50804b18a..7ea8cc17034 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -293,7 +293,7 @@ class InlineChatController( val diff = compareDiffs(selectedCode.split("\n"), recommendation.split("\n")) while (partialUndoActions.isNotEmpty()) { val action = partialUndoActions.pop() - action.invoke() + runChangeAction(project, action) } partialAcceptActions.clear() selectionStartLine = AtomicInteger(selectedLineStart) @@ -321,25 +321,32 @@ class InlineChatController( currentDocumentLine++ insertLine++ } - DiffRow.Tag.DELETE, DiffRow.Tag.CHANGE -> { - if (row.tag == DiffRow.Tag.CHANGE && row.newLine.trimIndent() == row.oldLine?.trimIndent()) return + DiffRow.Tag.DELETE -> { isAllEqual = false showCodeChangeInEditor(row, currentDocumentLine, editor) - - if (row.tag == DiffRow.Tag.CHANGE) { + insertLine++ + currentDocumentLine++ + deletedLinesCount++ + deletedCharsCount += row.oldLine?.length ?: 0 + } + DiffRow.Tag.CHANGE -> { + if (row.newLine.trimIndent() != row.oldLine?.trimIndent()){ + isAllEqual = false + showCodeChangeInEditor(row, currentDocumentLine, editor) insertLine += 2 currentDocumentLine += 2 + deletedLinesCount++ + deletedCharsCount += row.oldLine?.length ?: 0 + addedLinesCount++ + addedCharsCount += row.newLine?.length ?: 0 } else { - insertLine++ currentDocumentLine++ + insertLine++ } - deletedLinesCount++ - deletedCharsCount += row.oldLine?.length ?: 0 } DiffRow.Tag.INSERT -> { isAllEqual = false showCodeChangeInEditor(row, insertLine, editor) - insertLine++ addedLinesCount++ addedCharsCount += row.newLine?.length ?: 0 From 52ec61dd74ac27e2b65957d4e9235f47d1b46899 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Fri, 18 Oct 2024 09:07:41 -0700 Subject: [PATCH 21/39] remove gutter icon and improve popup location --- .../cwc/inline/InlineChatController.kt | 8 ++- .../cwc/inline/InlineChatEditorHint.kt | 44 +++++++++---- .../inline/InlineChatGutterIconRenderer.kt | 42 ------------- .../cwc/inline/InlineChatPopupFactory.kt | 27 ++++---- .../cwc/inline/InlineChatPopupPanel.kt | 4 +- .../cwc/inline/listeners/ChatCaretListener.kt | 61 ------------------- .../listeners/InlineChatFileListener.kt | 9 --- 7 files changed, 55 insertions(+), 140 deletions(-) delete mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt delete mode 100644 plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 7ea8cc17034..7d8b91c2510 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -613,7 +613,13 @@ class InlineChatController( } .onEach { event: ChatMessage -> if (event.message?.isNotEmpty() == true && prevMessage != event.message) { - runBlocking { processChatMessage(selectedCode, event, editor, selectedLineStart, prevMessage) } + runBlocking { + try { + processChatMessage(selectedCode, event, editor, selectedLineStart, prevMessage) + } catch (e: Exception) { + errorMessage = e.message ?: "" + } + } prevMessage = event.message } if (messages.isEmpty()) { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt index 36ec9981785..397b6671b27 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt @@ -6,8 +6,10 @@ import com.intellij.codeInsight.hint.HintManager import com.intellij.codeInsight.hint.HintManagerImpl import com.intellij.codeInsight.hint.HintUtil import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.VisualPosition import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.util.SystemInfo +import com.intellij.openapi.util.TextRange import com.intellij.ui.LightweightHint import com.intellij.ui.SimpleColoredText import com.intellij.ui.SimpleTextAttributes @@ -19,20 +21,40 @@ import javax.swing.JPanel class InlineChatEditorHint { private val hint = createHint() - private val hintLocationBuffer = 50 private fun getHintLocation(editor: Editor): Point { - val selectionModel = editor.selectionModel - val lineSelected = selectionModel.selectedText?.split("\n")?.size - val offset: Int = if (lineSelected != null && lineSelected > 1) { (lineSelected - 1).times(editor.lineHeight) } else { 0 } - val bestPosition = JBPopupFactory.getInstance().guessBestPopupLocation(editor).point - val visibleArea = editor.scrollingModel.visibleArea + val range = editor.calculateVisibleRange() + val document = editor.document + val selectionEnd = editor.selectionModel.selectionEnd + val isOneLineSelection = isOneLineSelection(editor) + val isBelow = editor.offsetToXY(selectionEnd) !in editor.scrollingModel.visibleArea + val areEdgesOutsideOfVisibleArea = editor.selectionModel.selectionStart !in range && editor.selectionModel.selectionEnd !in range + val offsetForHint = when { + isOneLineSelection -> selectionEnd + areEdgesOutsideOfVisibleArea -> document.getLineEndOffset(getLineByVisualStart(editor, editor.caretModel.offset, true)) + isBelow -> document.getLineEndOffset(getLineByVisualStart(editor, selectionEnd, true)) + else -> document.getLineEndOffset(getLineByVisualStart(editor, selectionEnd, false)) + } + val visualPosition = editor.offsetToVisualPosition(offsetForHint) + val hintPoint = HintManagerImpl.getHintPosition(hint, editor, visualPosition, HintManager.RIGHT) + hintPoint.translate(0, if (isBelow) editor.lineHeight else 0) + return hintPoint + } - val adjustedX = (bestPosition.x + 200).coerceAtMost(visibleArea.x + visibleArea.width - hintLocationBuffer) - val adjustedY = (bestPosition.y + offset).coerceAtMost(visibleArea.y + visibleArea.height - hintLocationBuffer) - val adjustedPosition = Point(adjustedX, adjustedY) + private fun isOneLineSelection(editor: Editor): Boolean { + val document = editor.document + val selectionModel = editor.selectionModel + val startLine = document.getLineNumber(selectionModel.selectionStart) + val endLine = document.getLineNumber(selectionModel.selectionEnd) + return startLine == endLine + } - return adjustedPosition + private fun getLineByVisualStart(editor: Editor, offset: Int, skipLineStartOffset: Boolean): Int { + val visualPosition = editor.offsetToVisualPosition(offset) + val skipCurrentLine = skipLineStartOffset && visualPosition.column == 0 + val line = if (skipCurrentLine) maxOf(visualPosition.line - 1, 0) else visualPosition.line + val lineStartPosition = VisualPosition(line, 0) + return editor.visualToLogicalPosition(lineStartPosition).line } private fun createHint(): LightweightHint { @@ -77,7 +99,7 @@ class InlineChatEditorHint { HintManager.HIDE_BY_TEXT_CHANGE or HintManager.HIDE_BY_SCROLLING, 0, false, - HintManagerImpl.createHintHint(editor, location, hint, HintManager.RIGHT_UNDER).setContentActive(false) + HintManagerImpl.createHintHint(editor, location, hint, HintManager.RIGHT).setContentActive(false) ) } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt deleted file mode 100644 index ee17b2959c8..00000000000 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatGutterIconRenderer.kt +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.cwc.inline - -import com.intellij.openapi.actionSystem.ActionGroup -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.editor.markup.GutterIconRenderer -import software.aws.toolkits.resources.AmazonQBundle.message -import javax.swing.Icon - -class InlineChatGutterIconRenderer(private val icon: Icon) : GutterIconRenderer() { - private var clickAction: (() -> Unit)? = null - override fun equals(other: Any?): Boolean { - if (other is InlineChatGutterIconRenderer) { - return icon == other.icon - } - return false - } - - override fun hashCode(): Int = icon.hashCode() - - override fun getIcon(): Icon = icon - - override fun getTooltipText(): String = message("amazonqInlineChat.gutter.tooltip") - - override fun isNavigateAction(): Boolean = false - - override fun getClickAction(): AnAction = object : AnAction() { - override fun actionPerformed(e: AnActionEvent) = clickAction?.invoke() ?: Unit - override fun update(e: AnActionEvent) = Unit - } - - fun setClickAction(action: () -> Unit) { - clickAction = action - } - - override fun getPopupMenuActions(): ActionGroup? = null - - override fun getAlignment(): Alignment = Alignment.CENTER -} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index 900390d2a3f..f94a949d1b1 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -26,6 +26,7 @@ class InlineChatPopupFactory( private val acceptHandler: () -> Unit, private val rejectHandler: () -> Unit, private val cancelHandler: () -> Unit, + private val popupBufferHeight: Int = 150 ) : Disposable { private fun getSelectedText(editor: Editor): String = ReadAction.compute { @@ -89,22 +90,21 @@ class InlineChatPopupFactory( } private fun showPopupInEditor(popup: JBPopup, popupPanel: InlineChatPopupPanel, editor: Editor) { - val popupHeight = popupPanel.popupHeight - val editorComponent = editor.component - val locationOnScreen = editorComponent.locationOnScreen - val popupPoint = JBPopupFactory.getInstance().guessBestPopupLocation(editor).point + val selectionEnd = editor.selectionModel.selectionEnd + val selectionStart = editor.selectionModel.selectionStart + val preferredXY = editor.offsetToXY(selectionStart) + val visibleArea = editor.scrollingModel.visibleArea + val isBelow = preferredXY.y - visibleArea.y < popupBufferHeight + val preferredX = editor.contentComponent.locationOnScreen.x + editor.contentComponent.width / 2 - popupPanel.popupWidth / 2 - val spaceAbove = popupPoint.y - locationOnScreen.y - val spaceNeeded = popupHeight + 15 // Add a small buffer - - val adjustedPoint = if (spaceAbove >= spaceNeeded) { - // Position above the caret - Point(popupPoint.x, popupPoint.y - spaceNeeded) + if (isBelow) { + val offsetXY = editor.offsetToXY(selectionEnd) + val point = Point(preferredX, offsetXY.y - visibleArea.y + popupBufferHeight) + popup.show(RelativePoint(point)) } else { - // Position below the caret - popupPoint + val popupXY = Point(preferredX, preferredXY.y - visibleArea.y - editor.lineHeight) + popup.show(RelativePoint(popupXY)) } - popup.show(RelativePoint(adjustedPoint)) popupPanel.textField.requestFocusInWindow() popupPanel.textField.addActionListener { e -> @@ -129,7 +129,6 @@ class InlineChatPopupFactory( } .setShowBorder(true) .setCancelOnWindowDeactivation(false) - .setAlpha(0.2F) .setCancelOnClickOutside(false) .setCancelOnOtherWindowOpen(false) .setFocusable(true) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index e2716b64452..bf8679ecfaa 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -30,7 +30,7 @@ import javax.swing.SwingConstants class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { private var submitClickListener: (() -> Unit)? = null private val popupButtonFontSize = 14f - private val popupWidth = 600 + val popupWidth = 600 val popupHeight = 90 private val popupButtonHeight = 30 private val popupButtonWidth = 80 @@ -41,7 +41,7 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() val submitButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, message("amazonqInlineChat.popup.confirm")) - private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CANCEL, message("amazonqInlineChat.popup.cancel")).apply { + private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.REJECT, message("amazonqInlineChat.popup.cancel")).apply { addActionListener { Disposer.dispose(parentDisposable) } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt deleted file mode 100644 index 8142d46d5b7..00000000000 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/ChatCaretListener.kt +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.jetbrains.services.cwc.inline.listeners - -import com.intellij.ide.DataManager -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.Presentation -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.event.CaretEvent -import com.intellij.openapi.editor.event.CaretListener -import com.intellij.openapi.editor.markup.GutterIconRenderer -import com.intellij.openapi.editor.markup.HighlighterLayer -import com.intellij.openapi.editor.markup.HighlighterTargetArea -import com.intellij.openapi.editor.markup.MarkupModel -import com.intellij.openapi.editor.markup.RangeHighlighter -import icons.AwsIcons -import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatGutterIconRenderer -import software.aws.toolkits.jetbrains.services.cwc.inline.OpenChatInputAction - -class ChatCaretListener : CaretListener { - private var currentHighlighter: RangeHighlighter? = null - - private fun createGutterIconRenderer(editor: Editor): GutterIconRenderer = InlineChatGutterIconRenderer(AwsIcons.Logos.AWS_Q_GREY).apply { - setClickAction { - val action = OpenChatInputAction() - val dataContext = DataManager.getInstance().getDataContext(editor.component) - val e = AnActionEvent.createFromDataContext( - "GutterIconClick", - Presentation(), - dataContext - ) - action.actionPerformed(e) - } - } - - override fun caretPositionChanged(event: CaretEvent) { - val editor = event.editor - val lineNumber = event.newPosition.line - val startOffset = editor.document.getLineStartOffset(lineNumber) - val endOffset = editor.document.getLineEndOffset(lineNumber) - val markupModel: MarkupModel = editor.markupModel - - if (event.oldPosition.line != event.newPosition.line) { - currentHighlighter?.let { - editor.markupModel.removeHighlighter(it) - } - markupModel.apply { - val highlighter = addRangeHighlighter( - startOffset, - endOffset, - HighlighterLayer.CARET_ROW, - null, - HighlighterTargetArea.LINES_IN_RANGE - ) - currentHighlighter = highlighter - highlighter.gutterIconRenderer = createGutterIconRenderer(editor) - } - } - } -} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt index f5923e7a672..fb1d7ed3770 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt @@ -12,7 +12,6 @@ import com.intellij.openapi.project.Project class InlineChatFileListener(project: Project) : FileEditorManagerListener { private var currentEditor: Editor? = null - private var caretListener: ChatCaretListener? = null private var selectionListener: InlineChatSelectionListener? = null init { @@ -33,24 +32,16 @@ class InlineChatFileListener(project: Project) : FileEditorManagerListener { } private fun setupListenersForEditor(editor: Editor) { - caretListener = ChatCaretListener().also { listener -> - editor.caretModel.addCaretListener(listener) - } - selectionListener = InlineChatSelectionListener().also { listener -> editor.selectionModel.addSelectionListener(listener) } } private fun removeListenersFromCurrentEditor(editor: Editor) { - caretListener?.let { listener -> - editor.caretModel.removeCaretListener(listener) - } selectionListener?.let { listener -> editor.selectionModel.removeSelectionListener(listener) listener.dispose() } - caretListener = null selectionListener = null } From e1cb9304c61eefba0b0f6bbd4ad9be9f29a00a66 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Fri, 18 Oct 2024 11:36:19 -0700 Subject: [PATCH 22/39] fix undo and donot close popup when there is diff --- .../cwc/inline/InlineChatController.kt | 27 ++++++++++--------- .../cwc/inline/InlineChatEditorHint.kt | 2 -- .../cwc/inline/InlineChatPopupFactory.kt | 4 +-- .../cwc/inline/InlineChatPopupPanel.kt | 9 +++++-- .../cwc/inline/OpenChatInputAction.kt | 2 +- .../listeners/InlineChatFileListener.kt | 4 ++- 6 files changed, 28 insertions(+), 20 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 7d8b91c2510..4dc67451918 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -40,7 +40,6 @@ import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import migration.software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator import org.apache.commons.text.StringEscapeUtils import software.amazon.awssdk.services.codewhispererruntime.model.InlineChatUserDecision import software.aws.toolkits.core.utils.debug @@ -53,6 +52,7 @@ import software.aws.toolkits.jetbrains.core.webview.BrowserState import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AMAZON_Q_WINDOW_ID +import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerType import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController @@ -85,10 +85,10 @@ class InlineChatController( private val shouldShowActions = AtomicBoolean(false) private val isInProgress = AtomicBoolean(false) private var metrics: InlineChatMetrics? = null - private var isPopupAborted = AtomicBoolean(true) + private var canPopupAbort = AtomicBoolean(true) init { - InlineChatFileListener(project).apply { + InlineChatFileListener(project, this).apply { project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this) } } @@ -119,7 +119,7 @@ class InlineChatController( } val popupCancelHandler: () -> Unit = { - if (isPopupAborted.get() && currentPopup != null) { + if (canPopupAbort.get() && currentPopup != null) { scope.launch(EDT) { while (partialUndoActions.isNotEmpty()) { val action = partialUndoActions.pop() @@ -193,7 +193,7 @@ class InlineChatController( val popupListener = object : JBPopupListener { override fun onClosed(event: LightweightWindowEvent) { - if (isPopupAborted.get() && event.asPopup().isDisposed) { + if (canPopupAbort.get() && event.asPopup().isDisposed) { popupCancelHandler.invoke() } } @@ -209,7 +209,7 @@ class InlineChatController( ).createPopup(scope) addPopupListeners(currentPopup!!) Disposer.register(this, currentPopup!!) - isPopupAborted.set(true) + canPopupAbort.set(true) } private fun highlightCodeWithBackgroundColor(editor: Editor, startOffset: Int, endOffset: Int, isGreen: Boolean) { @@ -239,16 +239,18 @@ class InlineChatController( } private fun hidePopup() { - isPopupAborted.set(false) + canPopupAbort.set(false) currentPopup?.closeOk(null) currentPopup = null isInProgress.set(false) shouldShowActions.set(false) } - fun disposePopup() { - currentPopup?.let { Disposer.dispose(it) } - hidePopup() + fun disposePopup(isFromFileChange: Boolean) { + if (currentPopup != null && !shouldShowActions.get() || isFromFileChange) { + currentPopup?.let { Disposer.dispose(it) } + hidePopup() + } } private fun getCodeBlocks(src: String): List { @@ -330,7 +332,7 @@ class InlineChatController( deletedCharsCount += row.oldLine?.length ?: 0 } DiffRow.Tag.CHANGE -> { - if (row.newLine.trimIndent() != row.oldLine?.trimIndent()){ + if (row.newLine.trimIndent() != row.oldLine?.trimIndent()) { isAllEqual = false showCodeChangeInEditor(row, currentDocumentLine, editor) insertLine += 2 @@ -369,6 +371,7 @@ class InlineChatController( if (codeBlocks.isEmpty()) { logger.warn { "No code block found in inline chat response with requestId: ${event.messageId} \nresponse: ${event.message}" } isInProgress.set(false) + shouldShowActions.set(false) throw Exception("No recommendation provided. Please try again with a different question.") } } @@ -404,7 +407,7 @@ class InlineChatController( action() } } - }, "", null, UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, false) + }, "", "q-inline-chat", UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, false) } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt index 397b6671b27..cb134c64d5d 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt @@ -7,9 +7,7 @@ import com.intellij.codeInsight.hint.HintManagerImpl import com.intellij.codeInsight.hint.HintUtil import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.VisualPosition -import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.util.SystemInfo -import com.intellij.openapi.util.TextRange import com.intellij.ui.LightweightHint import com.intellij.ui.SimpleColoredText import com.intellij.ui.SimpleTextAttributes diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index f94a949d1b1..93335f6fd47 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -26,7 +26,7 @@ class InlineChatPopupFactory( private val acceptHandler: () -> Unit, private val rejectHandler: () -> Unit, private val cancelHandler: () -> Unit, - private val popupBufferHeight: Int = 150 + private val popupBufferHeight: Int = 150, ) : Disposable { private fun getSelectedText(editor: Editor): String = ReadAction.compute { @@ -95,7 +95,7 @@ class InlineChatPopupFactory( val preferredXY = editor.offsetToXY(selectionStart) val visibleArea = editor.scrollingModel.visibleArea val isBelow = preferredXY.y - visibleArea.y < popupBufferHeight - val preferredX = editor.contentComponent.locationOnScreen.x + editor.contentComponent.width / 2 - popupPanel.popupWidth / 2 + val preferredX = editor.contentComponent.locationOnScreen.x + popupPanel.popupWidth / 2 if (isBelow) { val offsetXY = editor.offsetToXY(selectionEnd) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index bf8679ecfaa..4c2bd085233 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -13,6 +13,7 @@ import com.intellij.openapi.editor.actionSystem.EditorActionManager import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.util.Disposer import com.intellij.ui.IdeBorderFactory +import com.intellij.ui.components.JBTextArea import icons.AwsIcons import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER import software.aws.toolkits.resources.AmazonQBundle.message @@ -30,6 +31,7 @@ import javax.swing.SwingConstants class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { private var submitClickListener: (() -> Unit)? = null private val popupButtonFontSize = 14f + private val popupTextAreaFontSize = 12f val popupWidth = 600 val popupHeight = 90 private val popupButtonHeight = 30 @@ -57,8 +59,11 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() font = font.deriveFont(popupButtonFontSize) } - private val errorLabel = JLabel("").apply { - font = font.deriveFont(popupButtonFontSize) + private val errorLabel = JBTextArea("").apply { + font = font.deriveFont(popupTextAreaFontSize) + lineWrap = true + wrapStyleWord = true + isOpaque = false } private val errorPanel = JPanel(BorderLayout()).apply { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt index 86af0904377..7da3e26bee4 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt @@ -27,7 +27,7 @@ class OpenChatInputAction : AnAction() { private fun createCaretListener(editor: Editor): CaretListener = object : CaretListener { override fun caretPositionChanged(event: CaretEvent) { - inlineChatController?.disposePopup() + inlineChatController?.disposePopup(false) editor.caretModel.removeCaretListener(this) caretListener = null diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt index fb1d7ed3770..47f8e984dbc 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt @@ -9,8 +9,9 @@ import com.intellij.openapi.fileEditor.FileEditorManagerEvent import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.project.Project +import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatController -class InlineChatFileListener(project: Project) : FileEditorManagerListener { +class InlineChatFileListener(project: Project, private val controller: InlineChatController) : FileEditorManagerListener { private var currentEditor: Editor? = null private var selectionListener: InlineChatSelectionListener? = null @@ -28,6 +29,7 @@ class InlineChatFileListener(project: Project) : FileEditorManagerListener { currentEditor?.let { removeListenersFromCurrentEditor(it) } setupListenersForEditor(newEditor) currentEditor = newEditor + controller.disposePopup(true) } } From 6e44d7698ad3918f8cf1bf369fb8c2c893b77c05 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Fri, 18 Oct 2024 15:29:15 -0700 Subject: [PATCH 23/39] fix error message and popup location, disable client prompt --- .../cwc/clients/chat/model/Requests.kt | 1 + .../chat/telemetry/TelemetryHelper.kt | 2 +- .../cwc/inline/InlineChatController.kt | 81 ++++++++++--------- .../cwc/inline/InlineChatPopupFactory.kt | 28 ++++++- .../codewhispererstreaming/service-2.json | 3 +- 5 files changed, 72 insertions(+), 43 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/model/Requests.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/model/Requests.kt index 0813d80db11..3d686e99727 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/model/Requests.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/model/Requests.kt @@ -14,6 +14,7 @@ enum class TriggerType { ContextMenu, Hotkeys, CodeScanButton, + Inline } data class ChatRequestData( diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt index b0260e017e7..a1696e78886 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt @@ -64,7 +64,7 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: } private fun getTelemetryTriggerType(triggerType: TriggerType): CwsprChatTriggerInteraction = when (triggerType) { - TriggerType.Click, TriggerType.CodeScanButton -> CwsprChatTriggerInteraction.Click + TriggerType.Click, TriggerType.CodeScanButton, TriggerType.Inline -> CwsprChatTriggerInteraction.Click TriggerType.ContextMenu, TriggerType.Hotkeys -> CwsprChatTriggerInteraction.ContextMenu } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 4dc67451918..2cd993d5bb8 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -356,7 +356,7 @@ class InlineChatController( } } if (isAllEqual) { - throw Exception("No recommendation provided. Please try again with a different question.") + throw Exception("No suggestions from Q; please try a different instruction.") } isInProgress.set(false) @@ -372,7 +372,7 @@ class InlineChatController( logger.warn { "No code block found in inline chat response with requestId: ${event.messageId} \nresponse: ${event.message}" } isInProgress.set(false) shouldShowActions.set(false) - throw Exception("No recommendation provided. Please try again with a different question.") + throw Exception("No suggestions from Q; please try a different instruction.") } } } @@ -540,41 +540,41 @@ class InlineChatController( // val language = editor.project?.let { languageExtractor.extractProgrammingLanguageNameFromCurrentFile(editor, it) } ?: "" // This is temporary. TODO: remove this after prompt added on service side var prompt = "" - if (selectedCode.isNotBlank()) { - prompt = "You are a code transformation assistant." + - " Your task is to modify a selection of lines from a given code file according to a specific instruction.\n" + - "Follow these steps carefully:\n" + - "- You will be given some selected code from a file to be transformed, enclosed in XML tags\n" + - "- You will receive an instruction for how to transform the selected code, enclosed in XML tags\n" + - "- You will be given the contents of that same file as context, enclosed in XML tags\n" - "- Your task is to:\n" + - "- Apply the transformation instruction to the selected code\n" + - "- Ensure that the transformation is applied correctly and consistently\n" + - "- Reuse existing functions and other code from the context wherever possible\n" + - "- Important rules to follow:\n" + - "- Maintain the original indentation of the selected code\n" + - "- If the instruction asks to provide explanations or answer questions about the code," + - " add these as new comment lines above the relevant lines in the code; do not change the code lines themselves\n" + - "- If the instruction is unclear or cannot be applied, do not make any changes to the code\n" + - "- After performing the transformation, return the transformed code." + - " If the transformation generates new code but does not modify the selected code, be sure to include the selected code in your response.\n" - } else { - prompt = "You are a coding assistant. Your task is to generate code according to an specific instruction.\n" + - "Follow these steps carefully:\n" + - "- You will receive an instruction for how to generate code, enclosed in XML tags\n" + - "- You will be given the contents of that same file as context, enclosed in XML tags\n" + - "- Your task is to:\n" + - "- Generate code according to the instruction\n" + - "- Reuse existing functions and other code from the context wherever possible\n" + - "- Important rules to follow:\n" + - "- If the instruction asks to provide explanations or answer questions about the code," + - " add these as new comment lines above the relevant lines in the code; do not change the code lines themselves\n" + - "- If the instruction is unclear or cannot be applied, do not make any changes to the code\n" + - "- After generating the code, return ONLY the new code you generated; do not include any existing lines of code from the context.\n" - } - - prompt += "- Respond with the code in markdown format. Do not include any explanations or other text outside of the code itself\n" + - "Remember, your output should contain nothing but the transformed code or code comments.\n" +// if (selectedCode.isNotBlank()) { +// prompt = "You are a code transformation assistant." + +// " Your task is to modify a selection of lines from a given code file according to a specific instruction.\n" + +// "Follow these steps carefully:\n" + +// "- You will be given some selected code from a file to be transformed, enclosed in XML tags\n" + +// "- You will receive an instruction for how to transform the selected code, enclosed in XML tags\n" + +// "- You will be given the contents of that same file as context, enclosed in XML tags\n" +// "- Your task is to:\n" + +// "- Apply the transformation instruction to the selected code\n" + +// "- Ensure that the transformation is applied correctly and consistently\n" + +// "- Reuse existing functions and other code from the context wherever possible\n" + +// "- Important rules to follow:\n" + +// "- Maintain the original indentation of the selected code\n" + +// "- If the instruction asks to provide explanations or answer questions about the code," + +// " add these as new comment lines above the relevant lines in the code; do not change the code lines themselves\n" + +// "- If the instruction is unclear or cannot be applied, do not make any changes to the code\n" + +// "- After performing the transformation, return the transformed code." + +// " If the transformation generates new code but does not modify the selected code, be sure to include the selected code in your response.\n" +// } else { +// prompt = "You are a coding assistant. Your task is to generate code according to an specific instruction.\n" + +// "Follow these steps carefully:\n" + +// "- You will receive an instruction for how to generate code, enclosed in XML tags\n" + +// "- You will be given the contents of that same file as context, enclosed in XML tags\n" + +// "- Your task is to:\n" + +// "- Generate code according to the instruction\n" + +// "- Reuse existing functions and other code from the context wherever possible\n" + +// "- Important rules to follow:\n" + +// "- If the instruction asks to provide explanations or answer questions about the code," + +// " add these as new comment lines above the relevant lines in the code; do not change the code lines themselves\n" + +// "- If the instruction is unclear or cannot be applied, do not make any changes to the code\n" + +// "- After generating the code, return ONLY the new code you generated; do not include any existing lines of code from the context.\n" +// } +// +// prompt += "- Respond with the code in markdown format. Do not include any explanations or other text outside of the code itself\n" + +// "Remember, your output should contain nothing but the transformed code or code comments.\n" if (selectedCode.isNotBlank()) { prompt += "$selectedCode\n" } prompt += "$message\n" prompt += "${editor.document.text.take(8000)}" @@ -589,7 +589,7 @@ class InlineChatController( message = prompt, activeFileContext = fileContext, userIntent = intentRecognizer.getUserIntentFromPromptChatMessage(message, null), - triggerType = TriggerType.Click, + triggerType = TriggerType.Inline, customization = CodeWhispererModelConfigurator.getInstance().activeCustomization(project), relevantTextDocuments = emptyList(), useRelevantDocuments = false @@ -612,7 +612,8 @@ class InlineChatController( ) .catch { e -> logger.warn { "Error in inline chat request: ${e.message}" } - errorMessage = e.message ?: "" + errorMessage = "Error processing request; please try again.\n" + + " ${e.message}" } .onEach { event: ChatMessage -> if (event.message?.isNotEmpty() == true && prevMessage != event.message) { @@ -620,7 +621,7 @@ class InlineChatController( try { processChatMessage(selectedCode, event, editor, selectedLineStart, prevMessage) } catch (e: Exception) { - errorMessage = e.message ?: "" + errorMessage = e.message ?: "Error processing request; please try again." } } prevMessage = event.message diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index 93335f6fd47..6aaa9722117 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -7,6 +7,7 @@ import com.intellij.icons.AllIcons import com.intellij.openapi.Disposable import com.intellij.openapi.application.ReadAction import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.VisualPosition import com.intellij.openapi.ui.popup.IconButton import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.ui.popup.JBPopupFactory @@ -95,7 +96,8 @@ class InlineChatPopupFactory( val preferredXY = editor.offsetToXY(selectionStart) val visibleArea = editor.scrollingModel.visibleArea val isBelow = preferredXY.y - visibleArea.y < popupBufferHeight - val preferredX = editor.contentComponent.locationOnScreen.x + popupPanel.popupWidth / 2 + val xOffset = getLineByVisualStart(editor, selectionStart) + val preferredX = editor.contentComponent.locationOnScreen.x + xOffset if (isBelow) { val offsetXY = editor.offsetToXY(selectionEnd) @@ -115,6 +117,30 @@ class InlineChatPopupFactory( } } + fun getIndentationForLine(editor: Editor, lineNumber: Int): Int { + val document = editor.document + val lineStartOffset = document.getLineStartOffset(lineNumber) + val lineEndOffset = document.getLineEndOffset(lineNumber) + val lineText = document.getText(TextRange(lineStartOffset, lineEndOffset)) + + // Find the index of the first non-whitespace character + val firstNonWhitespace = lineText.indexOfFirst { !it.isWhitespace() } + + return if (firstNonWhitespace == -1) { + 0 + } else { + firstNonWhitespace + 1 + } + } + + private fun getLineByVisualStart(editor: Editor, offset: Int): Int { + val visualPosition = editor.offsetToVisualPosition(offset) + val line = visualPosition.line + val column = getIndentationForLine(editor, line) + val lineStartPosition = VisualPosition(line, column) + return editor.visualToLogicalPosition(lineStartPosition).line + } + private fun initPopup(panel: InlineChatPopupPanel): JBPopup { val cancelButton = IconButton(message("amazonqInlineChat.popup.cancel"), AllIcons.Actions.Cancel) val popup = JBPopupFactory.getInstance() diff --git a/plugins/core/sdk-codegen/codegen-resources/codewhispererstreaming/service-2.json b/plugins/core/sdk-codegen/codegen-resources/codewhispererstreaming/service-2.json index 863326fcb65..992c7ba8ac6 100644 --- a/plugins/core/sdk-codegen/codegen-resources/codewhispererstreaming/service-2.json +++ b/plugins/core/sdk-codegen/codegen-resources/codewhispererstreaming/service-2.json @@ -169,7 +169,8 @@ "type":"string", "enum":[ "MANUAL", - "DIAGNOSTIC" + "DIAGNOSTIC", + "INLINE" ] }, "CodeReferenceEvent":{ From 7775724b50cf6b0e468075cc0e320a3c7ee82e6f Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Mon, 21 Oct 2024 08:28:27 -0700 Subject: [PATCH 24/39] small fixes --- .../cwc/clients/chat/model/Requests.kt | 2 +- .../cwc/clients/chat/v1/ChatSessionV1.kt | 4 +- .../context/file/util/LanguageExtractor.kt | 8 -- .../focusArea/FocusAreaContextExtractor.kt | 3 +- .../cwc/inline/InlineChatController.kt | 90 +++++++++---------- .../cwc/inline/InlineChatPopupFactory.kt | 6 +- .../cwc/inline/InlineChatPopupPanel.kt | 8 +- .../cwc/inline/OpenChatInputAction.kt | 16 ++-- .../listeners/InlineChatFileListener.kt | 7 +- .../codewhispererstreaming/service-2.json | 2 +- 10 files changed, 71 insertions(+), 75 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/model/Requests.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/model/Requests.kt index 3d686e99727..0eeb2822e08 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/model/Requests.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/model/Requests.kt @@ -14,7 +14,7 @@ enum class TriggerType { ContextMenu, Hotkeys, CodeScanButton, - Inline + Inline, } data class ChatRequestData( diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt index a5057f797c8..26d2ebd8e97 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt @@ -50,6 +50,7 @@ import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.Recommend import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.Reference import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.SuggestedFollowUp import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.Suggestion +import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerType import software.aws.toolkits.jetbrains.services.cwc.editor.context.ActiveFileContext import software.aws.toolkits.jetbrains.services.cwc.editor.context.project.RelevantDocument @@ -200,6 +201,7 @@ class ChatSessionV1( val userInputMessageContextBuilder = UserInputMessageContext.builder() userInputMessageContextBuilder.editorState(activeFileContext.toEditorState(relevantTextDocuments, useRelevantDocuments)) val userInputMessageContext = userInputMessageContextBuilder.build() + val chatTriggerType = if (triggerType == TriggerType.Inline) ChatTriggerType.INLINE_CHAT else ChatTriggerType.MANUAL val userInput = UserInputMessage.builder() .content(message.take(ChatConstants.CUSTOMER_MESSAGE_SIZE_LIMIT)) @@ -209,7 +211,7 @@ class ChatSessionV1( val conversationState = ConversationState.builder() .conversationId(conversationId) .currentMessage(ChatMessage.fromUserInputMessage(userInput)) - .chatTriggerType(ChatTriggerType.MANUAL) + .chatTriggerType(chatTriggerType) .customizationArn(customization?.arn) .build() return GenerateAssistantResponseRequest.builder() diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt index 14227aea8e4..a63cc67d429 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt @@ -9,7 +9,6 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile -import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage class LanguageExtractor { fun extractLanguageNameFromCurrentFile(editor: Editor, project: Project): String? = @@ -18,11 +17,4 @@ class LanguageExtractor { val psiFile: PsiFile? = PsiDocumentManager.getInstance(project).getPsiFile(doc) psiFile?.fileType?.name?.lowercase() } - - fun extractProgrammingLanguageNameFromCurrentFile(editor: Editor, project: Project): String? = - runReadAction { - val doc: Document = editor.document - val psiFile: PsiFile? = PsiDocumentManager.getInstance(project).getPsiFile(doc) - psiFile?.programmingLanguage()?.toTelemetryType()?.toString() - } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt index 692a2618acd..f9a4a3c3a8d 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt @@ -140,7 +140,8 @@ class FocusAreaContextExtractor(private val fqnWebviewAdapter: FqnWebviewAdapter val requestString = ChatController.objectMapper.writeValueAsString(extractNamesRequest) codeNames = try { - val namesString = fqnWebviewAdapter?.let { it.extractNames(requestString) } ?: "{\"simpleNames\": [], \"fullyQualifiedNames\": {\"used\": []}}" + val namesString = fqnWebviewAdapter?.let { it.extractNames(requestString) } + ?: """{"simpleNames": [], "fullyQualifiedNames": {"used": []}}""" ChatController.objectMapper.readValue(namesString, CodeNamesImpl::class.java) } catch (e: Exception) { getLogger().warn(e) { "Failed to extract names from file" } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 2cd993d5bb8..9400c8c996a 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -11,7 +11,6 @@ import com.intellij.openapi.application.ReadAction import com.intellij.openapi.application.invokeLater import com.intellij.openapi.application.runInEdt import com.intellij.openapi.command.CommandProcessor -import com.intellij.openapi.command.UndoConfirmationPolicy import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.components.Service import com.intellij.openapi.components.service @@ -39,7 +38,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext import org.apache.commons.text.StringEscapeUtils import software.amazon.awssdk.services.codewhispererruntime.model.InlineChatUserDecision import software.aws.toolkits.core.utils.debug @@ -123,7 +121,7 @@ class InlineChatController( scope.launch(EDT) { while (partialUndoActions.isNotEmpty()) { val action = partialUndoActions.pop() - runChangeAction(project, action) + action.invoke() } partialAcceptActions.clear() } @@ -166,7 +164,7 @@ class InlineChatController( partialUndoActions.clear() while (partialAcceptActions.isNotEmpty()) { val action = partialAcceptActions.pop() - runChangeAction(project, action) + action.invoke() } invokeLater { hidePopup() } } @@ -179,7 +177,7 @@ class InlineChatController( scope.launch(EDT) { while (partialUndoActions.isNotEmpty()) { val action = partialUndoActions.pop() - runChangeAction(project, action) + action.invoke() } partialAcceptActions.clear() invokeLater { hidePopup() } @@ -204,9 +202,9 @@ class InlineChatController( fun initPopup(editor: Editor) { currentPopup?.let { Disposer.dispose(it) } currentPopup = InlineChatPopupFactory( - acceptHandler = diffAcceptHandler, rejectHandler = diffRejectHandler, editor = editor, + acceptHandler = diffAcceptHandler, rejectHandler = diffRejectHandler, submitHandler = popupSubmitHandler, cancelHandler = popupCancelHandler - ).createPopup(scope) + ).createPopup(editor, scope) addPopupListeners(currentPopup!!) Disposer.register(this, currentPopup!!) canPopupAbort.set(true) @@ -282,7 +280,7 @@ class InlineChatController( .replace("'", "'") .replace("=>", "=>") - private suspend fun processChatMessage(selectedCode: String, event: ChatMessage, editor: Editor, selectedLineStart: Int, prevMessage: String) { + private fun processChatMessage(selectedCode: String, event: ChatMessage, editor: Editor, selectedLineStart: Int, prevMessage: String) { if (event.message?.isNotEmpty() == true) { val codeBlocks = getCodeBlocks(event.message) if (codeBlocks.isEmpty()) { @@ -295,7 +293,7 @@ class InlineChatController( val diff = compareDiffs(selectedCode.split("\n"), recommendation.split("\n")) while (partialUndoActions.isNotEmpty()) { val action = partialUndoActions.pop() - runChangeAction(project, action) + action.invoke() } partialAcceptActions.clear() selectionStartLine = AtomicInteger(selectedLineStart) @@ -378,7 +376,7 @@ class InlineChatController( } } - private suspend fun insertNewLineIfNeeded(row: Int, editor: Editor): Int { + private fun insertNewLineIfNeeded(row: Int, editor: Editor): Int { var newLineInserted = 0 while (row > editor.document.lineCount - 1) { insertString(editor, editor.document.textLength, "\n") @@ -399,49 +397,49 @@ class InlineChatController( } } - private suspend fun runChangeAction(project: Project, action: () -> Unit) { - withContext(EDT) { - CommandProcessor.getInstance().executeCommand(project, { - ApplicationManager.getApplication().runWriteAction { - WriteCommandAction.runWriteCommandAction(project) { - action() - } + private fun insertString(editor: Editor, offset: Int, text: String): RangeMarker { + var rangeMarker: RangeMarker? = null + + ApplicationManager.getApplication().invokeAndWait { + CommandProcessor.getInstance().runUndoTransparentAction { + WriteCommandAction.runWriteCommandAction(project) { + editor.document.insertString(offset, text) + val row = editor.document.getLineNumber(offset) + rangeMarker = editor.document.createRangeMarker(offset, getLineEndOffset(editor.document, row)) + } + rangeMarker?.let { marker -> + highlightCodeWithBackgroundColor(editor, marker.startOffset, marker.endOffset, true) } - }, "", "q-inline-chat", UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, false) + } } - } - private suspend fun insertString(editor: Editor, offset: Int, text: String): RangeMarker { - var rangeMarker: RangeMarker? = null - val action = { - editor.document.insertString(offset, text) -// CodeStyleManager.getInstance(project).adjustLineIndent(document, offset) - val row = editor.document.getLineNumber(offset) - rangeMarker = editor.document.createRangeMarker(offset, getLineEndOffset(editor.document, row)) - highlightCodeWithBackgroundColor(editor, rangeMarker!!.startOffset, rangeMarker!!.endOffset, true) - } - runChangeAction(project, action) return rangeMarker!! } - private suspend fun deleteString(document: Document, start: Int, end: Int) { - val action = { - document.deleteString(start, end) + private fun deleteString(document: Document, start: Int, end: Int) { + ApplicationManager.getApplication().invokeAndWait { + CommandProcessor.getInstance().runUndoTransparentAction { + WriteCommandAction.runWriteCommandAction(project) { + document.deleteString(start, end) + } + } } - runChangeAction(project, action) } - private suspend fun highlightString(editor: Editor, start: Int, end: Int, isInsert: Boolean): RangeMarker { + private fun highlightString(editor: Editor, start: Int, end: Int, isInsert: Boolean): RangeMarker { var rangeMarker: RangeMarker? = null - val action = { - rangeMarker = editor.document.createRangeMarker(start, end) - highlightCodeWithBackgroundColor(editor, rangeMarker!!.startOffset, rangeMarker!!.endOffset, isInsert) + ApplicationManager.getApplication().invokeAndWait { + CommandProcessor.getInstance().runUndoTransparentAction { + WriteCommandAction.runWriteCommandAction(project) { + rangeMarker = editor.document.createRangeMarker(start, end) + highlightCodeWithBackgroundColor(editor, rangeMarker!!.startOffset, rangeMarker!!.endOffset, isInsert) + } + } } - runChangeAction(project, action) return rangeMarker!! } - private suspend fun showCodeChangeInEditor(diffRow: DiffRow, row: Int, editor: Editor) { + private fun showCodeChangeInEditor(diffRow: DiffRow, row: Int, editor: Editor) { try { val document = editor.document when (diffRow.tag) { @@ -470,7 +468,7 @@ class InlineChatController( partialUndoActions.add { if (rangeMarker.isValid) { scope.launch(EDT) { - deleteString(document, rangeMarker.startOffset, rangeMarker.endOffset + newLineInserted) + deleteString(document, rangeMarker.startOffset, (rangeMarker.endOffset + newLineInserted).coerceAtMost(document.textLength)) } } editor.markupModel.removeAllHighlighters() @@ -572,7 +570,7 @@ class InlineChatController( // "- If the instruction is unclear or cannot be applied, do not make any changes to the code\n" + // "- After generating the code, return ONLY the new code you generated; do not include any existing lines of code from the context.\n" // } -// + // prompt += "- Respond with the code in markdown format. Do not include any explanations or other text outside of the code itself\n" + // "Remember, your output should contain nothing but the transformed code or code comments.\n" if (selectedCode.isNotBlank()) { prompt += "$selectedCode\n" } @@ -617,12 +615,10 @@ class InlineChatController( } .onEach { event: ChatMessage -> if (event.message?.isNotEmpty() == true && prevMessage != event.message) { - runBlocking { - try { - processChatMessage(selectedCode, event, editor, selectedLineStart, prevMessage) - } catch (e: Exception) { - errorMessage = e.message ?: "Error processing request; please try again." - } + try { + processChatMessage(selectedCode, event, editor, selectedLineStart, prevMessage) + } catch (e: Exception) { + errorMessage = e.message ?: "Error processing request; please try again." } prevMessage = event.message } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index 6aaa9722117..0462824ebd0 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -22,7 +22,6 @@ import software.aws.toolkits.resources.AmazonQBundle.message import java.awt.Point class InlineChatPopupFactory( - private val editor: Editor, private val submitHandler: suspend (String, String, Int, Editor) -> String, private val acceptHandler: () -> Unit, private val rejectHandler: () -> Unit, @@ -46,7 +45,7 @@ class InlineChatPopupFactory( editor.document.getLineNumber(editor.selectionModel.selectionStart) } - fun createPopup(scope: CoroutineScope): JBPopup { + fun createPopup(editor: Editor, scope: CoroutineScope): JBPopup { val popupPanel = InlineChatPopupPanel(this).apply { border = IdeBorderFactory.createRoundedBorder(10).apply { setColor(POPUP_BUTTON_BORDER) @@ -54,6 +53,7 @@ class InlineChatPopupFactory( val submitListener: () -> Unit = { submitButton.isEnabled = false + cancelButton.isEnabled = false textField.isEnabled = false val prompt = textField.text if (prompt.isNotBlank()) { @@ -117,7 +117,7 @@ class InlineChatPopupFactory( } } - fun getIndentationForLine(editor: Editor, lineNumber: Int): Int { + private fun getIndentationForLine(editor: Editor, lineNumber: Int): Int { val document = editor.document val lineStartOffset = document.getLineStartOffset(lineNumber) val lineEndOffset = document.getLineEndOffset(lineNumber) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index 4c2bd085233..23f20db9002 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -43,8 +43,12 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() val submitButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, message("amazonqInlineChat.popup.confirm")) - private val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.REJECT, message("amazonqInlineChat.popup.cancel")).apply { - addActionListener { Disposer.dispose(parentDisposable) } + val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.REJECT, message("amazonqInlineChat.popup.cancel")).apply { + addActionListener { + if (!Disposer.isDisposed(parentDisposable)) { + Disposer.dispose(parentDisposable) + } + } } private val buttonsPanel = JPanel(BorderLayout()).apply { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt index 7da3e26bee4..28570a55a02 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt @@ -14,15 +14,15 @@ class OpenChatInputAction : AnAction() { private var inlineChatController: InlineChatController? = null private var caretListener: CaretListener? = null override fun actionPerformed(e: AnActionEvent) { - e.editor?.let { editor -> - e.editor?.project?.let { project -> - inlineChatController = InlineChatController.getInstance(project) - inlineChatController?.initPopup(editor) + val editor = e.editor + val project = editor?.project - caretListener = createCaretListener(editor) - editor.caretModel.addCaretListener(caretListener!!) - } - } + if (editor == null || project == null) return + inlineChatController = InlineChatController.getInstance(project) + inlineChatController?.initPopup(editor) + + caretListener = createCaretListener(editor) + editor.caretModel.addCaretListener(caretListener!!) } private fun createCaretListener(editor: Editor): CaretListener = object : CaretListener { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt index 47f8e984dbc..424bb77ce29 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt @@ -9,6 +9,7 @@ import com.intellij.openapi.fileEditor.FileEditorManagerEvent import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatController class InlineChatFileListener(project: Project, private val controller: InlineChatController) : FileEditorManagerListener { @@ -24,8 +25,8 @@ class InlineChatFileListener(project: Project, private val controller: InlineCha } override fun selectionChanged(event: FileEditorManagerEvent) { - val newEditor = (event.newEditor as? TextEditor)?.editor - if (newEditor != null && newEditor != currentEditor) { + val newEditor = (event.newEditor as? TextEditor)?.editor ?: return + if (newEditor != currentEditor) { currentEditor?.let { removeListenersFromCurrentEditor(it) } setupListenersForEditor(newEditor) currentEditor = newEditor @@ -42,7 +43,7 @@ class InlineChatFileListener(project: Project, private val controller: InlineCha private fun removeListenersFromCurrentEditor(editor: Editor) { selectionListener?.let { listener -> editor.selectionModel.removeSelectionListener(listener) - listener.dispose() + Disposer.dispose(listener) } selectionListener = null } diff --git a/plugins/core/sdk-codegen/codegen-resources/codewhispererstreaming/service-2.json b/plugins/core/sdk-codegen/codegen-resources/codewhispererstreaming/service-2.json index 992c7ba8ac6..39344997397 100644 --- a/plugins/core/sdk-codegen/codegen-resources/codewhispererstreaming/service-2.json +++ b/plugins/core/sdk-codegen/codegen-resources/codewhispererstreaming/service-2.json @@ -170,7 +170,7 @@ "enum":[ "MANUAL", "DIAGNOSTIC", - "INLINE" + "INLINE_CHAT" ] }, "CodeReferenceEvent":{ From b8839bddad04ed91f7c968fddcba71919976ceab Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Mon, 21 Oct 2024 22:07:03 -0700 Subject: [PATCH 25/39] refactor --- .../cwc/inline/InlineChatController.kt | 215 +++++++++--------- 1 file changed, 110 insertions(+), 105 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 9400c8c996a..044f2f48318 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -51,6 +51,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AMAZON_Q_WINDOW_ID import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator +import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerType import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController @@ -78,6 +79,7 @@ class InlineChatController( private val partialUndoActions = Stack<() -> Unit>() private val partialAcceptActions = Stack<() -> Unit>() private var selectionStartLine = AtomicInteger(0) + private var insertionLine = AtomicInteger(-1) private val sessionStorage = ChatSessionStorage() private val telemetryHelper = TelemetryHelper(project, sessionStorage) private val shouldShowActions = AtomicBoolean(false) @@ -227,19 +229,10 @@ class InlineChatController( ) } - private fun extractContentAfterFirstNewline(input: String): String { - val newlineIndex = input.indexOf('\n') - return if (newlineIndex != -1) { - input.substring(newlineIndex + 1) - } else { - "" - } - } - private fun hidePopup() { canPopupAbort.set(false) currentPopup?.closeOk(null) - currentPopup = null +// currentPopup = null isInProgress.set(false) shouldShowActions.set(false) } @@ -248,27 +241,10 @@ class InlineChatController( if (currentPopup != null && !shouldShowActions.get() || isFromFileChange) { currentPopup?.let { Disposer.dispose(it) } hidePopup() + currentPopup = null } } - private fun getCodeBlocks(src: String): List { - val codeBlocks = mutableListOf() - var currentIndex = 0 - - while (currentIndex < src.length) { - val startIndex = src.indexOf("```", currentIndex) - if (startIndex == -1) return codeBlocks - - val endIndex = src.indexOf("```", startIndex + 3) - if (endIndex == -1) return codeBlocks - - val code = src.substring(startIndex + 3, endIndex) - codeBlocks.add(code) - currentIndex = endIndex + 3 - } - return codeBlocks - } - private fun compareDiffs(original: List, recommendation: List): List { val generator = DiffRowGenerator.create().showInlineDiffs(false).build() val rows: List = generator.generateDiffRows(original, recommendation) @@ -280,102 +256,127 @@ class InlineChatController( .replace("'", "'") .replace("=>", "=>") - private fun processChatMessage(selectedCode: String, event: ChatMessage, editor: Editor, selectedLineStart: Int, prevMessage: String) { - if (event.message?.isNotEmpty() == true) { - val codeBlocks = getCodeBlocks(event.message) - if (codeBlocks.isEmpty()) { - logger.info { "No code block found in inline chat response with requestId: ${event.messageId}" } - return - } - val recommendation = unescape(extractContentAfterFirstNewline(codeBlocks.first())) - logger.info { "Recived Inline chat code recommendation:\n ```$recommendation``` \nfrom requestId: ${event.messageId}" } - logger.info { "Original selected code:\n ```$selectedCode```" } - val diff = compareDiffs(selectedCode.split("\n"), recommendation.split("\n")) + private fun processNewCode(editor: Editor, line: Int, code: String, prevMessage: String) { + logger.info ("received inline chat recommendation with code: \n $code") + var insertLine = line + var linesToAdd = emptyList() + val prevLines = prevMessage.split("\n") + if (prevLines.size > 1 && code.startsWith(prevMessage)) { + if(insertionLine.get() != -1) insertLine = insertionLine.get() + linesToAdd = code.split("\n").drop(prevLines.size - 1) + } else { while (partialUndoActions.isNotEmpty()) { val action = partialUndoActions.pop() action.invoke() } partialAcceptActions.clear() - selectionStartLine = AtomicInteger(selectedLineStart) - var currentDocumentLine = selectedLineStart - var insertLine = selectedLineStart - if (event.codeReference?.isNotEmpty() == true) { - editor.project?.let { ReferenceLogController.addReferenceLog(recommendation, event.codeReference, editor, it) } - } + linesToAdd = code.split("\n") + } + if(linesToAdd.last() == "") linesToAdd = linesToAdd.dropLast(1) + linesToAdd.forEach{ l -> + val row = DiffRow(DiffRow.Tag.INSERT, "", l) + showCodeChangeInEditor(row, insertLine, editor) + insertLine++ + insertionLine.set(insertLine) + } + } - var deletedCharsCount = 0 - var addedCharsCount = 0 - var addedLinesCount = 0 - var deletedLinesCount = 0 + private fun processDiffRows(diffRows: List, startLine: Int, editor: Editor): Boolean { + var isAllEqual = true + selectionStartLine = AtomicInteger(startLine) + var currentDocumentLine = startLine + var insertLine = startLine + var deletedCharsCount = 0 + var addedCharsCount = 0 + var addedLinesCount = 0 + var deletedLinesCount = 0 + diffRows.forEach { row -> + when (row.tag) { + DiffRow.Tag.EQUAL -> { + currentDocumentLine++ + insertLine++ + } - if (currentPopup?.isVisible != true) { - logger.debug { "inline chat popup cancelled before diff is shown" } - isInProgress.set(false) - recordInlineChatTelemetry(InlineChatUserDecision.DISMISS) - return - } - var isAllEqual = true - diff.forEach { row -> - when (row.tag) { - DiffRow.Tag.EQUAL -> { - currentDocumentLine++ - insertLine++ - } - DiffRow.Tag.DELETE -> { + DiffRow.Tag.DELETE -> { + isAllEqual = false + showCodeChangeInEditor(row, currentDocumentLine, editor) + insertLine++ + currentDocumentLine++ + deletedLinesCount++ + deletedCharsCount += row.oldLine?.length ?: 0 + } + + DiffRow.Tag.CHANGE -> { + if (row.newLine.trimIndent() != row.oldLine?.trimIndent()) { isAllEqual = false showCodeChangeInEditor(row, currentDocumentLine, editor) - insertLine++ - currentDocumentLine++ + insertLine += 2 + currentDocumentLine += 2 deletedLinesCount++ deletedCharsCount += row.oldLine?.length ?: 0 - } - DiffRow.Tag.CHANGE -> { - if (row.newLine.trimIndent() != row.oldLine?.trimIndent()) { - isAllEqual = false - showCodeChangeInEditor(row, currentDocumentLine, editor) - insertLine += 2 - currentDocumentLine += 2 - deletedLinesCount++ - deletedCharsCount += row.oldLine?.length ?: 0 - addedLinesCount++ - addedCharsCount += row.newLine?.length ?: 0 - } else { - currentDocumentLine++ - insertLine++ - } - } - DiffRow.Tag.INSERT -> { - isAllEqual = false - showCodeChangeInEditor(row, insertLine, editor) - insertLine++ addedLinesCount++ addedCharsCount += row.newLine?.length ?: 0 + } else { + currentDocumentLine++ + insertLine++ } } + + DiffRow.Tag.INSERT -> { + isAllEqual = false + showCodeChangeInEditor(row, insertLine, editor) + insertLine++ + addedLinesCount++ + addedCharsCount += row.newLine?.length ?: 0 + } + } + } + metrics?.numSuggestionAddChars = addedCharsCount + metrics?.numSuggestionAddLines = addedLinesCount + metrics?.numSuggestionDelChars = deletedCharsCount + metrics?.numSuggestionDelLines = deletedLinesCount + return isAllEqual + } + + private fun processChatDiff(selectedCode: String, event: ChatMessage, editor: Editor, selectedLineStart: Int) { + if (event.message?.isNotEmpty() == true) { + runBlocking { + while (partialUndoActions.isNotEmpty()) { + val action = partialUndoActions.pop() + runBlocking { action.invoke() } + } + partialAcceptActions.clear() + return@runBlocking } - if (isAllEqual) { - throw Exception("No suggestions from Q; please try a different instruction.") + val recommendation = unescape(event.message) + logger.info { "Received Inline chat code recommendation:\n ```$recommendation``` \nfrom requestId: ${event.messageId}" } + logger.info { "Original selected code:\n ```$selectedCode```" } + var selection = selectedCode.split("\n") + val recommendationList = recommendation.split("\n") + val diff = compareDiffs(selection, recommendationList) + if (event.codeReference?.isNotEmpty() == true) { + editor.project?.let { ReferenceLogController.addReferenceLog(recommendation, event.codeReference, editor, it) } } - isInProgress.set(false) - shouldShowActions.set(true) - metrics?.numSuggestionAddChars = addedCharsCount - metrics?.numSuggestionAddLines = addedLinesCount - metrics?.numSuggestionDelChars = deletedCharsCount - metrics?.numSuggestionDelLines = deletedLinesCount - } else { - if (event.messageType == ChatMessageType.Answer) { - val codeBlocks = getCodeBlocks(prevMessage) - if (codeBlocks.isEmpty()) { - logger.warn { "No code block found in inline chat response with requestId: ${event.messageId} \nresponse: ${event.message}" } - isInProgress.set(false) - shouldShowActions.set(false) + if (currentPopup?.isVisible != true) { + logger.debug { "inline chat popup cancelled before diff is shown" } + isInProgress.set(false) + recordInlineChatTelemetry(InlineChatUserDecision.DISMISS) + return + } + invokeLater { + val isAllEqual = processDiffRows(diff, selectedLineStart, editor) + if (isAllEqual) { throw Exception("No suggestions from Q; please try a different instruction.") } + + isInProgress.set(false) + shouldShowActions.set(true) } } } + private fun insertNewLineIfNeeded(row: Int, editor: Editor): Int { var newLineInserted = 0 while (row > editor.document.lineCount - 1) { @@ -534,8 +535,7 @@ class InlineChatController( val triggerId = UUID.randomUUID().toString() val intentRecognizer = UserIntentRecognizer() -// val languageExtractor = LanguageExtractor() -// val language = editor.project?.let { languageExtractor.extractProgrammingLanguageNameFromCurrentFile(editor, it) } ?: "" + val language = editor.virtualFile?.programmingLanguage() // This is temporary. TODO: remove this after prompt added on service side var prompt = "" // if (selectedCode.isNotBlank()) { @@ -575,7 +575,7 @@ class InlineChatController( // "Remember, your output should contain nothing but the transformed code or code comments.\n" if (selectedCode.isNotBlank()) { prompt += "$selectedCode\n" } prompt += "$message\n" - prompt += "${editor.document.text.take(8000)}" + prompt += "${if (editor.document.text.isNotEmpty()) editor.document.text.take(8000) else "file written in $language"}" logger.info { "Inline chat prompt: $prompt" } @@ -616,11 +616,11 @@ class InlineChatController( .onEach { event: ChatMessage -> if (event.message?.isNotEmpty() == true && prevMessage != event.message) { try { - processChatMessage(selectedCode, event, editor, selectedLineStart, prevMessage) + processNewCode(editor, selectedLineStart, unescape(event.message), prevMessage) } catch (e: Exception) { errorMessage = e.message ?: "Error processing request; please try again." } - prevMessage = event.message + prevMessage = unescape(event.message) } if (messages.isEmpty()) { firstResponseLatency = (System.currentTimeMillis() - startTime).toDouble() @@ -630,6 +630,11 @@ class InlineChatController( .toList() } chat.await() + val finalMessage = messages.lastOrNull { m -> m.messageType == ChatMessageType.AnswerPart } + if(selectedCode.isNotEmpty() && finalMessage != null) { + processChatDiff(selectedCode, finalMessage, editor, selectedLineStart) + } + insertionLine.set(-1) val lastResponseLatency = (System.currentTimeMillis() - startTime).toDouble() val requestId = messages.lastOrNull()?.messageId requestId?.let { From cc03925ca76356d589b84402380a0fee3a1ed1f1 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Tue, 22 Oct 2024 16:44:36 -0700 Subject: [PATCH 26/39] cleanup --- .../cwc/inline/InlineChatController.kt | 239 +++++++++--------- .../cwc/inline/InlineChatPopupFactory.kt | 2 +- .../cwc/inline/InlineChatPopupPanel.kt | 19 -- 3 files changed, 117 insertions(+), 143 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 044f2f48318..87d6c846a2f 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -31,13 +31,10 @@ import com.intellij.openapi.wm.ToolWindowManager import com.intellij.ui.JBColor import com.intellij.ui.jcef.JBCefApp import com.jetbrains.rd.util.AtomicInteger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async +import kotlinx.coroutines.* import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import org.apache.commons.text.StringEscapeUtils import software.amazon.awssdk.services.codewhispererruntime.model.InlineChatUserDecision import software.aws.toolkits.core.utils.debug @@ -54,7 +51,6 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.customization.Code import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerType -import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController import software.aws.toolkits.jetbrains.services.cwc.controller.chat.messenger.ChatPromptHandler import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.TelemetryHelper import software.aws.toolkits.jetbrains.services.cwc.controller.chat.userIntent.UserIntentRecognizer @@ -78,7 +74,6 @@ class InlineChatController( private var rangeHighlighter: RangeHighlighter? = null private val partialUndoActions = Stack<() -> Unit>() private val partialAcceptActions = Stack<() -> Unit>() - private var selectionStartLine = AtomicInteger(0) private var insertionLine = AtomicInteger(-1) private val sessionStorage = ChatSessionStorage() private val telemetryHelper = TelemetryHelper(project, sessionStorage) @@ -86,6 +81,7 @@ class InlineChatController( private val isInProgress = AtomicBoolean(false) private var metrics: InlineChatMetrics? = null private var canPopupAbort = AtomicBoolean(true) + private var currentSelectionRange: RangeMarker? = null init { InlineChatFileListener(project, this).apply { @@ -118,15 +114,10 @@ class InlineChatController( } } - val popupCancelHandler: () -> Unit = { + val popupCancelHandler: (editor: Editor) -> Unit = { editor -> if (canPopupAbort.get() && currentPopup != null) { - scope.launch(EDT) { - while (partialUndoActions.isNotEmpty()) { - val action = partialUndoActions.pop() - action.invoke() - } - partialAcceptActions.clear() - } + undoChanges() + restoreSelection(editor) ApplicationManager.getApplication().executeOnPooledThread { recordInlineChatTelemetry(InlineChatUserDecision.DISMISS) } @@ -161,6 +152,16 @@ class InlineChatController( metrics = null } + private fun undoChanges () { + scope.launch(EDT) { + while (partialUndoActions.isNotEmpty()) { + val action = partialUndoActions.pop() + action.invoke() + } + partialAcceptActions.clear() + } + } + private val diffAcceptHandler: () -> Unit = { scope.launch(EDT) { partialUndoActions.clear() @@ -175,26 +176,21 @@ class InlineChatController( } } - private val diffRejectHandler: () -> Unit = { - scope.launch(EDT) { - while (partialUndoActions.isNotEmpty()) { - val action = partialUndoActions.pop() - action.invoke() - } - partialAcceptActions.clear() - invokeLater { hidePopup() } - } + private val diffRejectHandler: (editor: Editor) -> Unit = { editor -> + undoChanges() + invokeLater { hidePopup() } + restoreSelection(editor) ApplicationManager.getApplication().executeOnPooledThread { recordInlineChatTelemetry(InlineChatUserDecision.REJECT) } } - private fun addPopupListeners(popup: JBPopup) { + private fun addPopupListeners(popup: JBPopup, editor: Editor) { val popupListener = object : JBPopupListener { override fun onClosed(event: LightweightWindowEvent) { if (canPopupAbort.get() && event.asPopup().isDisposed) { - popupCancelHandler.invoke() + popupCancelHandler.invoke(editor) } } } @@ -204,14 +200,31 @@ class InlineChatController( fun initPopup(editor: Editor) { currentPopup?.let { Disposer.dispose(it) } currentPopup = InlineChatPopupFactory( - acceptHandler = diffAcceptHandler, rejectHandler = diffRejectHandler, - submitHandler = popupSubmitHandler, cancelHandler = popupCancelHandler + acceptHandler = diffAcceptHandler, rejectHandler = { diffRejectHandler(editor) }, + submitHandler = popupSubmitHandler, cancelHandler = { popupCancelHandler(editor) } ).createPopup(editor, scope) - addPopupListeners(currentPopup!!) + addPopupListeners(currentPopup!!, editor) Disposer.register(this, currentPopup!!) canPopupAbort.set(true) } + private fun removeSelection(editor: Editor) { + scope.launch(EDT) { + val selectionModel = editor.selectionModel + selectionModel.removeSelection() + } + } + + private fun restoreSelection(editor: Editor) { + currentSelectionRange?.let { range -> + scope.launch(EDT) { + val selectionModel = editor.selectionModel + selectionModel.setSelection(range.startOffset, range.endOffset) + } + } + currentSelectionRange = null + } + private fun highlightCodeWithBackgroundColor(editor: Editor, startOffset: Int, endOffset: Int, isGreen: Boolean) { val greenBackgroundAttributes = TextAttributes().apply { backgroundColor = JBColor(0x66BB6A, 0x006400) @@ -232,9 +245,9 @@ class InlineChatController( private fun hidePopup() { canPopupAbort.set(false) currentPopup?.closeOk(null) -// currentPopup = null isInProgress.set(false) shouldShowActions.set(false) + currentSelectionRange = null } fun disposePopup(isFromFileChange: Boolean) { @@ -257,12 +270,12 @@ class InlineChatController( .replace("=>", "=>") private fun processNewCode(editor: Editor, line: Int, code: String, prevMessage: String) { - logger.info ("received inline chat recommendation with code: \n $code") + logger.info("received inline chat recommendation with code: \n $code") var insertLine = line var linesToAdd = emptyList() val prevLines = prevMessage.split("\n") if (prevLines.size > 1 && code.startsWith(prevMessage)) { - if(insertionLine.get() != -1) insertLine = insertionLine.get() + if (insertionLine.get() != -1) insertLine = insertionLine.get() linesToAdd = code.split("\n").drop(prevLines.size - 1) } else { while (partialUndoActions.isNotEmpty()) { @@ -272,8 +285,8 @@ class InlineChatController( partialAcceptActions.clear() linesToAdd = code.split("\n") } - if(linesToAdd.last() == "") linesToAdd = linesToAdd.dropLast(1) - linesToAdd.forEach{ l -> + if (linesToAdd.last() == "") linesToAdd = linesToAdd.dropLast(1) + linesToAdd.forEach { l -> val row = DiffRow(DiffRow.Tag.INSERT, "", l) showCodeChangeInEditor(row, insertLine, editor) insertLine++ @@ -281,26 +294,25 @@ class InlineChatController( } } - private fun processDiffRows(diffRows: List, startLine: Int, editor: Editor): Boolean { + private fun processDiffRows(diffRows: List, selectionRange: RangeMarker, editor: Editor): Boolean { var isAllEqual = true - selectionStartLine = AtomicInteger(startLine) + val startLine = getLineNumber(editor.document, selectionRange.startOffset) var currentDocumentLine = startLine - var insertLine = startLine + var deletedCharsCount = 0 var addedCharsCount = 0 var addedLinesCount = 0 var deletedLinesCount = 0 + removeSelection(editor) diffRows.forEach { row -> when (row.tag) { DiffRow.Tag.EQUAL -> { currentDocumentLine++ - insertLine++ } DiffRow.Tag.DELETE -> { isAllEqual = false showCodeChangeInEditor(row, currentDocumentLine, editor) - insertLine++ currentDocumentLine++ deletedLinesCount++ deletedCharsCount += row.oldLine?.length ?: 0 @@ -310,7 +322,6 @@ class InlineChatController( if (row.newLine.trimIndent() != row.oldLine?.trimIndent()) { isAllEqual = false showCodeChangeInEditor(row, currentDocumentLine, editor) - insertLine += 2 currentDocumentLine += 2 deletedLinesCount++ deletedCharsCount += row.oldLine?.length ?: 0 @@ -318,14 +329,13 @@ class InlineChatController( addedCharsCount += row.newLine?.length ?: 0 } else { currentDocumentLine++ - insertLine++ } } DiffRow.Tag.INSERT -> { isAllEqual = false - showCodeChangeInEditor(row, insertLine, editor) - insertLine++ + showCodeChangeInEditor(row, currentDocumentLine, editor) + currentDocumentLine++ addedLinesCount++ addedCharsCount += row.newLine?.length ?: 0 } @@ -338,7 +348,7 @@ class InlineChatController( return isAllEqual } - private fun processChatDiff(selectedCode: String, event: ChatMessage, editor: Editor, selectedLineStart: Int) { + private fun processChatDiff(selectedCode: String, event: ChatMessage, editor: Editor, selectionRange: RangeMarker) { if (event.message?.isNotEmpty() == true) { runBlocking { while (partialUndoActions.isNotEmpty()) { @@ -346,26 +356,25 @@ class InlineChatController( runBlocking { action.invoke() } } partialAcceptActions.clear() - return@runBlocking - } - val recommendation = unescape(event.message) - logger.info { "Received Inline chat code recommendation:\n ```$recommendation``` \nfrom requestId: ${event.messageId}" } - logger.info { "Original selected code:\n ```$selectedCode```" } - var selection = selectedCode.split("\n") - val recommendationList = recommendation.split("\n") - val diff = compareDiffs(selection, recommendationList) - if (event.codeReference?.isNotEmpty() == true) { - editor.project?.let { ReferenceLogController.addReferenceLog(recommendation, event.codeReference, editor, it) } - } - if (currentPopup?.isVisible != true) { - logger.debug { "inline chat popup cancelled before diff is shown" } - isInProgress.set(false) - recordInlineChatTelemetry(InlineChatUserDecision.DISMISS) - return - } - invokeLater { - val isAllEqual = processDiffRows(diff, selectedLineStart, editor) + val recommendation = unescape(event.message) + logger.info { "Received Inline chat code recommendation:\n ```$recommendation``` \nfrom requestId: ${event.messageId}" } + logger.info { "Original selected code:\n ```$selectedCode```" } + if (selectedCode == recommendation) { + throw Exception("No suggestions from Q; please try a different instruction.") + } + val selection = selectedCode.split("\n") + val recommendationList = recommendation.split("\n") + val diff = compareDiffs(selection, recommendationList) + + if (currentPopup?.isVisible != true) { + logger.debug { "inline chat popup cancelled before diff is shown" } + isInProgress.set(false) + recordInlineChatTelemetry(InlineChatUserDecision.DISMISS) + return@runBlocking + } + + val isAllEqual = processDiffRows(diff, selectionRange, editor) if (isAllEqual) { throw Exception("No suggestions from Q; please try a different instruction.") } @@ -376,7 +385,6 @@ class InlineChatController( } } - private fun insertNewLineIfNeeded(row: Int, editor: Editor): Int { var newLineInserted = 0 while (row > editor.document.lineCount - 1) { @@ -398,6 +406,10 @@ class InlineChatController( } } + private fun getLineNumber(document: Document, offset: Int): Int = ReadAction.compute { + document.getLineNumber(offset) + } + private fun insertString(editor: Editor, offset: Int, text: String): RangeMarker { var rangeMarker: RangeMarker? = null @@ -453,9 +465,7 @@ class InlineChatController( } partialAcceptActions.add { if (rangeMarker.isValid) { - scope.launch(EDT) { - deleteString(document, rangeMarker.startOffset, rangeMarker.endOffset) - } + deleteString(document, rangeMarker.startOffset, rangeMarker.endOffset) } editor.markupModel.removeAllHighlighters() } @@ -468,9 +478,7 @@ class InlineChatController( val rangeMarker = insertString(editor, insertOffset, textToInsert) partialUndoActions.add { if (rangeMarker.isValid) { - scope.launch(EDT) { - deleteString(document, rangeMarker.startOffset, (rangeMarker.endOffset + newLineInserted).coerceAtMost(document.textLength)) - } + deleteString(document, rangeMarker.startOffset, (rangeMarker.endOffset + newLineInserted).coerceAtMost(document.textLength)) } editor.markupModel.removeAllHighlighters() } @@ -484,10 +492,8 @@ class InlineChatController( val changeEndOffset = getLineEndOffset(document, row) val oldTextRangeMarker = highlightString(editor, changeOffset, changeEndOffset, false) partialAcceptActions.add { - scope.launch(EDT) { - if (oldTextRangeMarker.isValid) { - deleteString(document, oldTextRangeMarker.startOffset, oldTextRangeMarker.endOffset) - } + if (oldTextRangeMarker.isValid) { + deleteString(document, oldTextRangeMarker.startOffset, oldTextRangeMarker.endOffset) } editor.markupModel.removeAllHighlighters() } @@ -496,12 +502,8 @@ class InlineChatController( val textToInsert = unescape(diffRow.newLine) + "\n" val newTextRangeMarker = insertString(editor, insertOffset, textToInsert) partialUndoActions.add { - WriteCommandAction.runWriteCommandAction(project) { - if (newTextRangeMarker.isValid) { - scope.launch(EDT) { - deleteString(document, newTextRangeMarker.startOffset, newTextRangeMarker.endOffset + newLineInserted) - } - } + if (newTextRangeMarker.isValid) { + deleteString(document, newTextRangeMarker.startOffset, newTextRangeMarker.endOffset + newLineInserted) } editor.markupModel.removeAllHighlighters() } @@ -513,11 +515,10 @@ class InlineChatController( } } - private suspend fun handleChat(message: String, selectedCode: String = "", editor: Editor, selectedLineStart: Int): String { + private fun checkCredentials(): String? { val authController = AuthController() val credentialState = authController.getAuthNeededStates(project).chat if (credentialState != null) { - // handle auth if (!JBCefApp.isSupported()) { requestCredentialsForQ(project, isReauth = false) } else { @@ -527,8 +528,30 @@ class InlineChatController( ToolWindowManager.getInstance(project).getToolWindow(AMAZON_Q_WINDOW_ID)?.show() } } + scope.launch { + delay(3000) + withContext(EDT) { + disposePopup(true) + } + } return "Please sign in to Amazon Q" } + return null + } + + private suspend fun handleChat(message: String, selectedCode: String = "", editor: Editor, selectedLineStart: Int): String { + val authError = checkCredentials() + if (authError != null) { + return authError + } + val selectionStart = getLineStartOffset(editor.document, selectedLineStart) + var selectionRange: RangeMarker? = null + if (selectedCode.isNotEmpty()) { + WriteCommandAction.runWriteCommandAction(project) { + selectionRange = editor.document.createRangeMarker(selectionStart, selectionStart + selectedCode.length) + currentSelectionRange = selectionRange + } + } val startTime = System.currentTimeMillis() var firstResponseLatency = 0.0 val messages = mutableListOf() @@ -536,43 +559,7 @@ class InlineChatController( val intentRecognizer = UserIntentRecognizer() val language = editor.virtualFile?.programmingLanguage() -// This is temporary. TODO: remove this after prompt added on service side var prompt = "" -// if (selectedCode.isNotBlank()) { -// prompt = "You are a code transformation assistant." + -// " Your task is to modify a selection of lines from a given code file according to a specific instruction.\n" + -// "Follow these steps carefully:\n" + -// "- You will be given some selected code from a file to be transformed, enclosed in XML tags\n" + -// "- You will receive an instruction for how to transform the selected code, enclosed in XML tags\n" + -// "- You will be given the contents of that same file as context, enclosed in XML tags\n" -// "- Your task is to:\n" + -// "- Apply the transformation instruction to the selected code\n" + -// "- Ensure that the transformation is applied correctly and consistently\n" + -// "- Reuse existing functions and other code from the context wherever possible\n" + -// "- Important rules to follow:\n" + -// "- Maintain the original indentation of the selected code\n" + -// "- If the instruction asks to provide explanations or answer questions about the code," + -// " add these as new comment lines above the relevant lines in the code; do not change the code lines themselves\n" + -// "- If the instruction is unclear or cannot be applied, do not make any changes to the code\n" + -// "- After performing the transformation, return the transformed code." + -// " If the transformation generates new code but does not modify the selected code, be sure to include the selected code in your response.\n" -// } else { -// prompt = "You are a coding assistant. Your task is to generate code according to an specific instruction.\n" + -// "Follow these steps carefully:\n" + -// "- You will receive an instruction for how to generate code, enclosed in XML tags\n" + -// "- You will be given the contents of that same file as context, enclosed in XML tags\n" + -// "- Your task is to:\n" + -// "- Generate code according to the instruction\n" + -// "- Reuse existing functions and other code from the context wherever possible\n" + -// "- Important rules to follow:\n" + -// "- If the instruction asks to provide explanations or answer questions about the code," + -// " add these as new comment lines above the relevant lines in the code; do not change the code lines themselves\n" + -// "- If the instruction is unclear or cannot be applied, do not make any changes to the code\n" + -// "- After generating the code, return ONLY the new code you generated; do not include any existing lines of code from the context.\n" -// } - -// prompt += "- Respond with the code in markdown format. Do not include any explanations or other text outside of the code itself\n" + -// "Remember, your output should contain nothing but the transformed code or code comments.\n" if (selectedCode.isNotBlank()) { prompt += "$selectedCode\n" } prompt += "$message\n" prompt += "${if (editor.document.text.isNotEmpty()) editor.document.text.take(8000) else "file written in $language"}" @@ -595,7 +582,6 @@ class InlineChatController( val sessionInfo = sessionStorage.getSession("inlineChat-editor", project) - // Save the request in the history sessionInfo.history.add(requestData) var errorMessage = "" var prevMessage = "" @@ -610,14 +596,14 @@ class InlineChatController( ) .catch { e -> logger.warn { "Error in inline chat request: ${e.message}" } - errorMessage = "Error processing request; please try again.\n" + - " ${e.message}" + errorMessage = "Error processing request; please try again" } .onEach { event: ChatMessage -> if (event.message?.isNotEmpty() == true && prevMessage != event.message) { try { processNewCode(editor, selectedLineStart, unescape(event.message), prevMessage) } catch (e: Exception) { + logger.info("error streaming chat message to editor: ${e.stackTraceToString()}") errorMessage = e.message ?: "Error processing request; please try again." } prevMessage = unescape(event.message) @@ -631,8 +617,13 @@ class InlineChatController( } chat.await() val finalMessage = messages.lastOrNull { m -> m.messageType == ChatMessageType.AnswerPart } - if(selectedCode.isNotEmpty() && finalMessage != null) { - processChatDiff(selectedCode, finalMessage, editor, selectedLineStart) + if (selectionRange != null && finalMessage != null) { + try { + processChatDiff(selectedCode, finalMessage, editor, selectionRange!!) + } catch (e: Exception) { + logger.info("error precessing chat diff in editor: ${e.stackTraceToString()}") + errorMessage = "Error processing request; please try again." + } } insertionLine.set(-1) val lastResponseLatency = (System.currentTimeMillis() - startTime).toDouble() @@ -643,7 +634,9 @@ class InlineChatController( codeIntent = true, responseStartLatency = firstResponseLatency, responseEndLatency = lastResponseLatency ) } - + if(errorMessage.isNotEmpty()) { + undoChanges() + } return errorMessage } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index 0462824ebd0..2c0121cd436 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -68,7 +68,7 @@ class InlineChatPopupFactory( errorMessage = submitHandler(prompt, selectedCode, selectedLineStart, editor) } if (errorMessage.isNotEmpty()) { - setErrorMessage(errorMessage) + setLabel(errorMessage) revalidate() } else { val acceptAction = { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index 23f20db9002..67a5ac8f3f7 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -13,7 +13,6 @@ import com.intellij.openapi.editor.actionSystem.EditorActionManager import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.util.Disposer import com.intellij.ui.IdeBorderFactory -import com.intellij.ui.components.JBTextArea import icons.AwsIcons import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER import software.aws.toolkits.resources.AmazonQBundle.message @@ -63,17 +62,6 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() font = font.deriveFont(popupButtonFontSize) } - private val errorLabel = JBTextArea("").apply { - font = font.deriveFont(popupTextAreaFontSize) - lineWrap = true - wrapStyleWord = true - isOpaque = false - } - - private val errorPanel = JPanel(BorderLayout()).apply { - add(errorLabel, BorderLayout.CENTER) - border = BorderFactory.createEmptyBorder(10, 10, 10, 10) - } private val inputPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(10, 10, 5, 10) @@ -165,13 +153,6 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() revalidate() } - fun setErrorMessage(text: String) { - errorLabel.text = text - add(errorPanel, BorderLayout.CENTER) - remove(inputPanel) - remove(bottomPanel) - revalidate() - } fun setLabel(text: String) { textLabel.text = text From 2988b00728554fd71e2dcc803e96b5e7f4a0e65c Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Tue, 22 Oct 2024 16:54:08 -0700 Subject: [PATCH 27/39] cleanup logs --- .../services/cwc/inline/InlineChatController.kt | 13 +++++-------- .../services/cwc/inline/InlineChatPopupPanel.kt | 1 - .../cwc/inline/listeners/InlineChatFileListener.kt | 3 ++- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 87d6c846a2f..c972dac6c38 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -39,7 +39,6 @@ import org.apache.commons.text.StringEscapeUtils import software.amazon.awssdk.services.codewhispererruntime.model.InlineChatUserDecision import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger -import software.aws.toolkits.core.utils.info import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.core.coroutines.EDT import software.aws.toolkits.jetbrains.core.gettingstarted.requestCredentialsForQ @@ -84,7 +83,7 @@ class InlineChatController( private var currentSelectionRange: RangeMarker? = null init { - InlineChatFileListener(project, this).apply { + InlineChatFileListener(project).apply { project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this) } } @@ -270,7 +269,7 @@ class InlineChatController( .replace("=>", "=>") private fun processNewCode(editor: Editor, line: Int, code: String, prevMessage: String) { - logger.info("received inline chat recommendation with code: \n $code") + logger.debug { "received inline chat recommendation with code: \n $code" } var insertLine = line var linesToAdd = emptyList() val prevLines = prevMessage.split("\n") @@ -358,8 +357,6 @@ class InlineChatController( partialAcceptActions.clear() val recommendation = unescape(event.message) - logger.info { "Received Inline chat code recommendation:\n ```$recommendation``` \nfrom requestId: ${event.messageId}" } - logger.info { "Original selected code:\n ```$selectedCode```" } if (selectedCode == recommendation) { throw Exception("No suggestions from Q; please try a different instruction.") } @@ -564,7 +561,7 @@ class InlineChatController( prompt += "$message\n" prompt += "${if (editor.document.text.isNotEmpty()) editor.document.text.take(8000) else "file written in $language"}" - logger.info { "Inline chat prompt: $prompt" } + logger.debug { "Inline chat prompt: $prompt" } val contextExtractor = ActiveFileContextExtractor.create(fqnWebviewAdapter = null, project = project) val fileContext = contextExtractor.extractContextForTrigger(ExtractionTriggerType.ChatMessage) @@ -603,7 +600,7 @@ class InlineChatController( try { processNewCode(editor, selectedLineStart, unescape(event.message), prevMessage) } catch (e: Exception) { - logger.info("error streaming chat message to editor: ${e.stackTraceToString()}") + logger.warn { "error streaming chat message to editor: ${e.stackTraceToString()}" } errorMessage = e.message ?: "Error processing request; please try again." } prevMessage = unescape(event.message) @@ -621,7 +618,7 @@ class InlineChatController( try { processChatDiff(selectedCode, finalMessage, editor, selectionRange!!) } catch (e: Exception) { - logger.info("error precessing chat diff in editor: ${e.stackTraceToString()}") + logger.warn {"error precessing chat diff in editor: ${e.stackTraceToString()}" } errorMessage = "Error processing request; please try again." } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index 67a5ac8f3f7..50a7a99eec2 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -30,7 +30,6 @@ import javax.swing.SwingConstants class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { private var submitClickListener: (() -> Unit)? = null private val popupButtonFontSize = 14f - private val popupTextAreaFontSize = 12f val popupWidth = 600 val popupHeight = 90 private val popupButtonHeight = 30 diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt index 424bb77ce29..5d3d5cae038 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt @@ -12,9 +12,10 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatController -class InlineChatFileListener(project: Project, private val controller: InlineChatController) : FileEditorManagerListener { +class InlineChatFileListener(project: Project) : FileEditorManagerListener { private var currentEditor: Editor? = null private var selectionListener: InlineChatSelectionListener? = null + private val controller = InlineChatController.getInstance(project) init { val editor = project.let { FileEditorManager.getInstance(it).selectedTextEditor } From 897e3a485d0c40bd43cbc818430218f39ab75d7d Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Tue, 22 Oct 2024 18:32:59 -0700 Subject: [PATCH 28/39] fix build --- .../jetbrains/services/cwc/inline/InlineChatController.kt | 6 +++--- .../jetbrains/services/cwc/inline/OpenChatInputAction.kt | 6 +++--- .../services/cwc/inline/listeners/InlineChatFileListener.kt | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index c972dac6c38..a1f25fbf44c 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -83,7 +83,7 @@ class InlineChatController( private var currentSelectionRange: RangeMarker? = null init { - InlineChatFileListener(project).apply { + InlineChatFileListener(project, this).apply { project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this) } } @@ -508,7 +508,7 @@ class InlineChatController( } } catch (e: Exception) { logger.warn { "Error when showing inline chat diff in editor: ${e.message} \n ${e.stackTraceToString()}" } - throw Exception("Unexpected error, please try again.") + throw Exception("Error processing request; please try again.") } } @@ -601,7 +601,7 @@ class InlineChatController( processNewCode(editor, selectedLineStart, unescape(event.message), prevMessage) } catch (e: Exception) { logger.warn { "error streaming chat message to editor: ${e.stackTraceToString()}" } - errorMessage = e.message ?: "Error processing request; please try again." + errorMessage = "Error processing request; please try again." } prevMessage = unescape(event.message) } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt index 28570a55a02..bd1897cce89 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt @@ -5,17 +5,17 @@ package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.event.CaretEvent import com.intellij.openapi.editor.event.CaretListener -import org.jetbrains.plugins.terminal.exp.TerminalDataContextUtils.editor class OpenChatInputAction : AnAction() { private var inlineChatController: InlineChatController? = null private var caretListener: CaretListener? = null override fun actionPerformed(e: AnActionEvent) { - val editor = e.editor - val project = editor?.project + val editor = e.getRequiredData(CommonDataKeys.EDITOR) + val project = editor.project if (editor == null || project == null) return inlineChatController = InlineChatController.getInstance(project) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt index 5d3d5cae038..424bb77ce29 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt @@ -12,10 +12,9 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatController -class InlineChatFileListener(project: Project) : FileEditorManagerListener { +class InlineChatFileListener(project: Project, private val controller: InlineChatController) : FileEditorManagerListener { private var currentEditor: Editor? = null private var selectionListener: InlineChatSelectionListener? = null - private val controller = InlineChatController.getInstance(project) init { val editor = project.let { FileEditorManager.getInstance(it).selectedTextEditor } From cdfdef2a119cd60b562120fe191292e0ea472150 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Wed, 23 Oct 2024 11:17:59 -0700 Subject: [PATCH 29/39] detekt and pr feedback --- .../amazonq/startup/AmazonQStartupActivity.kt | 1 - .../focusArea/FocusAreaContextExtractor.kt | 6 ++-- .../cwc/inline/InlineChatController.kt | 28 +++++++++++++------ .../cwc/inline/InlineChatEditorHint.kt | 6 ++-- .../cwc/inline/InlineChatPopupPanel.kt | 2 -- .../cwc/inline/OpenChatInputAction.kt | 22 ++------------- .../listeners/InlineChatFileListener.kt | 6 ++-- .../jetbrains-community/src/icons/AwsIcons.kt | 2 -- 8 files changed, 34 insertions(+), 39 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt index 7eb2d55e818..a752eefb86b 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt @@ -20,7 +20,6 @@ import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWi import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatController -import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import java.lang.management.ManagementFactory import java.time.Duration import java.util.concurrent.atomic.AtomicBoolean diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt index f9a4a3c3a8d..58abdd8e64f 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt @@ -14,6 +14,7 @@ import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.services.amazonq.webview.FqnWebviewAdapter import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.CodeNames import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.CodeNamesImpl +import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.FullyQualifiedNames import software.aws.toolkits.jetbrains.services.cwc.controller.ChatController import software.aws.toolkits.jetbrains.services.cwc.editor.context.file.util.LanguageExtractor import software.aws.toolkits.jetbrains.utils.computeOnEdt @@ -140,9 +141,8 @@ class FocusAreaContextExtractor(private val fqnWebviewAdapter: FqnWebviewAdapter val requestString = ChatController.objectMapper.writeValueAsString(extractNamesRequest) codeNames = try { - val namesString = fqnWebviewAdapter?.let { it.extractNames(requestString) } - ?: """{"simpleNames": [], "fullyQualifiedNames": {"used": []}}""" - ChatController.objectMapper.readValue(namesString, CodeNamesImpl::class.java) + fqnWebviewAdapter?.let { ChatController.objectMapper.readValue(it.extractNames(requestString), CodeNamesImpl::class.java) + } ?: CodeNamesImpl(simpleNames = emptyList(), fullyQualifiedNames = FullyQualifiedNames(used = emptyList())) } catch (e: Exception) { getLogger().warn(e) { "Failed to extract names from file" } null diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index a1f25fbf44c..bddb7399a83 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -17,6 +17,8 @@ import com.intellij.openapi.components.service import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.RangeMarker +import com.intellij.openapi.editor.event.CaretEvent +import com.intellij.openapi.editor.event.CaretListener import com.intellij.openapi.editor.markup.HighlighterLayer import com.intellij.openapi.editor.markup.HighlighterTargetArea import com.intellij.openapi.editor.markup.RangeHighlighter @@ -81,11 +83,11 @@ class InlineChatController( private var metrics: InlineChatMetrics? = null private var canPopupAbort = AtomicBoolean(true) private var currentSelectionRange: RangeMarker? = null + private val listener = InlineChatFileListener(project, this) init { - InlineChatFileListener(project, this).apply { - project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this) - } + Disposer.register(this, listener) + project.messageBus.connect(this).subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, listener) } data class InlineChatMetrics( @@ -151,7 +153,7 @@ class InlineChatController( metrics = null } - private fun undoChanges () { + private fun undoChanges() { scope.launch(EDT) { while (partialUndoActions.isNotEmpty()) { val action = partialUndoActions.pop() @@ -203,10 +205,20 @@ class InlineChatController( submitHandler = popupSubmitHandler, cancelHandler = { popupCancelHandler(editor) } ).createPopup(editor, scope) addPopupListeners(currentPopup!!, editor) + val caretListener = createCaretListener(editor) + editor.caretModel.addCaretListener(caretListener) Disposer.register(this, currentPopup!!) canPopupAbort.set(true) } + private fun createCaretListener(editor: Editor): CaretListener = object : CaretListener { + override fun caretPositionChanged(event: CaretEvent) { + disposePopup(false) + + editor.caretModel.removeCaretListener(this) + } + } + private fun removeSelection(editor: Editor) { scope.launch(EDT) { val selectionModel = editor.selectionModel @@ -588,8 +600,8 @@ class InlineChatController( triggerId, requestData, sessionInfo, - false, - true + shouldAddIndexInProgressMessage = false, + isInlineChat = true ) .catch { e -> logger.warn { "Error in inline chat request: ${e.message}" } @@ -618,7 +630,7 @@ class InlineChatController( try { processChatDiff(selectedCode, finalMessage, editor, selectionRange!!) } catch (e: Exception) { - logger.warn {"error precessing chat diff in editor: ${e.stackTraceToString()}" } + logger.warn { "error precessing chat diff in editor: ${e.stackTraceToString()}" } errorMessage = "Error processing request; please try again." } } @@ -631,7 +643,7 @@ class InlineChatController( codeIntent = true, responseStartLatency = firstResponseLatency, responseEndLatency = lastResponseLatency ) } - if(errorMessage.isNotEmpty()) { + if (errorMessage.isNotEmpty()) { undoChanges() } return errorMessage diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt index cb134c64d5d..a8ffc1bb3b6 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatEditorHint.kt @@ -7,6 +7,7 @@ import com.intellij.codeInsight.hint.HintManagerImpl import com.intellij.codeInsight.hint.HintUtil import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.VisualPosition +import com.intellij.openapi.keymap.KeymapUtil import com.intellij.openapi.util.SystemInfo import com.intellij.ui.LightweightHint import com.intellij.ui.SimpleColoredText @@ -66,13 +67,14 @@ class InlineChatEditorHint { coloredText.appendToComponent(component) val shortcutComponent = HintUtil.createInformationComponent() - if (!SystemInfo.isWindows) { + val shortCut = KeymapUtil.getShortcutText("aws.toolkit.jetbrains.core.services.cwc.inline.openChat") + if (!SystemInfo.isWindows && shortCut == "⌃I") { val shortCutIcon = AwsIcons.Resources.InlineChat.AWS_Q_INLINECHAT_SHORTCUT shortcutComponent.isIconOnTheRight = true shortcutComponent.icon = shortCutIcon } else { val shortcutText = - SimpleColoredText(message("amazonqInlineChat.hint.windows.shortCut"), SimpleTextAttributes.REGULAR_ATTRIBUTES) + SimpleColoredText(shortCut, SimpleTextAttributes.REGULAR_ATTRIBUTES) shortcutText.appendToComponent(shortcutComponent) } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index 50a7a99eec2..ee579c48e13 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -61,7 +61,6 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() font = font.deriveFont(popupButtonFontSize) } - private val inputPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(10, 10, 5, 10) } @@ -152,7 +151,6 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() revalidate() } - fun setLabel(text: String) { textLabel.text = text textField.isEnabled = false diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt index bd1897cce89..340585dd259 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt @@ -11,27 +11,11 @@ import com.intellij.openapi.editor.event.CaretEvent import com.intellij.openapi.editor.event.CaretListener class OpenChatInputAction : AnAction() { - private var inlineChatController: InlineChatController? = null - private var caretListener: CaretListener? = null override fun actionPerformed(e: AnActionEvent) { val editor = e.getRequiredData(CommonDataKeys.EDITOR) - val project = editor.project + val project = editor.project ?: return - if (editor == null || project == null) return - inlineChatController = InlineChatController.getInstance(project) - inlineChatController?.initPopup(editor) - - caretListener = createCaretListener(editor) - editor.caretModel.addCaretListener(caretListener!!) - } - - private fun createCaretListener(editor: Editor): CaretListener = object : CaretListener { - override fun caretPositionChanged(event: CaretEvent) { - inlineChatController?.disposePopup(false) - - editor.caretModel.removeCaretListener(this) - caretListener = null - inlineChatController = null - } + val inlineChatController = InlineChatController.getInstance(project) + inlineChatController.initPopup(editor) } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt index 424bb77ce29..96e2262a0eb 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt @@ -3,6 +3,7 @@ package software.aws.toolkits.jetbrains.services.cwc.inline.listeners +import com.intellij.openapi.Disposable import com.intellij.openapi.editor.Editor import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManagerEvent @@ -12,7 +13,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatController -class InlineChatFileListener(project: Project, private val controller: InlineChatController) : FileEditorManagerListener { +class InlineChatFileListener(project: Project, private val controller: InlineChatController) : FileEditorManagerListener, Disposable { private var currentEditor: Editor? = null private var selectionListener: InlineChatSelectionListener? = null @@ -37,6 +38,7 @@ class InlineChatFileListener(project: Project, private val controller: InlineCha private fun setupListenersForEditor(editor: Editor) { selectionListener = InlineChatSelectionListener().also { listener -> editor.selectionModel.addSelectionListener(listener) + Disposer.register(this, listener) } } @@ -48,7 +50,7 @@ class InlineChatFileListener(project: Project, private val controller: InlineCha selectionListener = null } - fun dispose() { + override fun dispose() { currentEditor?.let { removeListenersFromCurrentEditor(it) } currentEditor = null } diff --git a/plugins/core/jetbrains-community/src/icons/AwsIcons.kt b/plugins/core/jetbrains-community/src/icons/AwsIcons.kt index 8af5f335663..78488c95943 100644 --- a/plugins/core/jetbrains-community/src/icons/AwsIcons.kt +++ b/plugins/core/jetbrains-community/src/icons/AwsIcons.kt @@ -129,8 +129,6 @@ object AwsIcons { object InlineChat { @JvmField val CONFIRM = load("/icons/resources/inlinechat/amazonq_inline_chat_confirm_icon.svg") - @JvmField val CANCEL = load("/icons/resources/inlinechat/amazonq_inline_chat_cancel_icon.svg") - @JvmField val REJECT = load("/icons/resources/inlinechat/amazonq_inline_chat_reject.svg") @JvmField val AWS_Q_INLINECHAT_SHORTCUT = load("/icons/resources/inlinechat/amazonq_inline_chat_shortcut.svg") From cb2a169f003c25333eab6572560c9f384b9b0acb Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Thu, 24 Oct 2024 16:18:04 -0700 Subject: [PATCH 30/39] refactor new ux --- .../cwc/inline/InlineChatController.kt | 369 ++++++++++-------- 1 file changed, 207 insertions(+), 162 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index bddb7399a83..0f4a634671d 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -37,10 +37,13 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import org.apache.commons.text.StringEscapeUtils import software.amazon.awssdk.services.codewhispererruntime.model.InlineChatUserDecision import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.core.utils.info import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.core.coroutines.EDT import software.aws.toolkits.jetbrains.core.gettingstarted.requestCredentialsForQ @@ -62,9 +65,9 @@ import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessage import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType import software.aws.toolkits.jetbrains.services.cwc.storage.ChatSessionStorage import software.aws.toolkits.telemetry.FeatureId -import java.util.Stack import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean +import kotlin.math.max @Service(Service.Level.PROJECT) class InlineChatController( @@ -73,8 +76,8 @@ class InlineChatController( ) : Disposable { private var currentPopup: JBPopup? = null private var rangeHighlighter: RangeHighlighter? = null - private val partialUndoActions = Stack<() -> Unit>() - private val partialAcceptActions = Stack<() -> Unit>() + private var rejectAction: (() -> Unit)? = null + private var acceptAction: (() -> Unit)? = null private var insertionLine = AtomicInteger(-1) private val sessionStorage = ChatSessionStorage() private val telemetryHelper = TelemetryHelper(project, sessionStorage) @@ -84,12 +87,14 @@ class InlineChatController( private var canPopupAbort = AtomicBoolean(true) private var currentSelectionRange: RangeMarker? = null private val listener = InlineChatFileListener(project, this) + private var isAbandoned = AtomicBoolean(false) init { Disposer.register(this, listener) project.messageBus.connect(this).subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, listener) } + data class InlineChatMetrics( val requestId: String, val inputLength: Int? = null, @@ -133,9 +138,9 @@ class InlineChatController( metrics?.charactersAdded = metrics?.numSuggestionAddChars metrics?.charactersRemoved = metrics?.numSuggestionDelChars } - metrics?.requestId?.let { + if(metrics?.requestId?.isNotEmpty() == true){ telemetryHelper.recordInlineChatTelemetry( - it, + metrics?.requestId!!, metrics?.inputLength, metrics?.numSelectedLines, metrics?.codeIntent, @@ -155,21 +160,17 @@ class InlineChatController( private fun undoChanges() { scope.launch(EDT) { - while (partialUndoActions.isNotEmpty()) { - val action = partialUndoActions.pop() - action.invoke() - } - partialAcceptActions.clear() + rejectAction?.invoke() + rejectAction = null + acceptAction = null } } private val diffAcceptHandler: () -> Unit = { scope.launch(EDT) { - partialUndoActions.clear() - while (partialAcceptActions.isNotEmpty()) { - val action = partialAcceptActions.pop() - action.invoke() - } + rejectAction = null + acceptAction?.invoke() + acceptAction = null invokeLater { hidePopup() } } ApplicationManager.getApplication().executeOnPooledThread { @@ -205,10 +206,10 @@ class InlineChatController( submitHandler = popupSubmitHandler, cancelHandler = { popupCancelHandler(editor) } ).createPopup(editor, scope) addPopupListeners(currentPopup!!, editor) - val caretListener = createCaretListener(editor) - editor.caretModel.addCaretListener(caretListener) Disposer.register(this, currentPopup!!) canPopupAbort.set(true) + val caretListener = createCaretListener(editor) + editor.caretModel.addCaretListener(caretListener) } private fun createCaretListener(editor: Editor): CaretListener = object : CaretListener { @@ -281,111 +282,196 @@ class InlineChatController( .replace("=>", "=>") private fun processNewCode(editor: Editor, line: Int, code: String, prevMessage: String) { - logger.debug { "received inline chat recommendation with code: \n $code" } - var insertLine = line - var linesToAdd = emptyList() - val prevLines = prevMessage.split("\n") - if (prevLines.size > 1 && code.startsWith(prevMessage)) { - if (insertionLine.get() != -1) insertLine = insertionLine.get() - linesToAdd = code.split("\n").drop(prevLines.size - 1) - } else { - while (partialUndoActions.isNotEmpty()) { - val action = partialUndoActions.pop() - action.invoke() + if(isAbandoned.get()) return + runBlocking { + logger.debug { "received inline chat recommendation with code: \n $code" } + var insertLine = line + var linesToAdd = emptyList() + val prevLines = prevMessage.split("\n") + if (prevLines.size > 1 && code.startsWith(prevMessage)) { + if (insertionLine.get() != -1) insertLine = insertionLine.get() + linesToAdd = code.split("\n").drop(prevLines.size - 1) + } else { + linesToAdd = code.split("\n") + } + if (linesToAdd.last() == "") linesToAdd = linesToAdd.dropLast(1) + val stringToAdd = if (linesToAdd.size > 1) linesToAdd.joinToString(separator = "\n") else linesToAdd.first() + if (currentPopup?.isVisible != true) { + logger.debug { "inline chat popup cancelled before diff is shown" } + isInProgress.set(false) + isAbandoned.set(true) + recordInlineChatTelemetry(InlineChatUserDecision.DISMISS) + return@runBlocking } - partialAcceptActions.clear() - linesToAdd = code.split("\n") - } - if (linesToAdd.last() == "") linesToAdd = linesToAdd.dropLast(1) - linesToAdd.forEach { l -> - val row = DiffRow(DiffRow.Tag.INSERT, "", l) - showCodeChangeInEditor(row, insertLine, editor) - insertLine++ + launch(EDT) { + insertNewLineIfNeeded(insertLine, editor) + insertString(editor, getLineStartOffset(editor.document, insertLine), stringToAdd + "\n") + }.join() + insertLine += linesToAdd.size insertionLine.set(insertLine) + acceptAction = { + removeHighlighter(editor) + } + rejectAction = { + val startOffset = getLineStartOffset(editor.document, line) + val endOffset = getLineEndOffset(editor.document, line + code.split("\n").size - 1) + replaceString(editor.document, startOffset, endOffset, "") + removeHighlighter(editor) + } + isInProgress.set(false) + shouldShowActions.set(true) } } - private fun processDiffRows(diffRows: List, selectionRange: RangeMarker, editor: Editor): Boolean { - var isAllEqual = true - val startLine = getLineNumber(editor.document, selectionRange.startOffset) + private fun processHighlights(diffRows: List, startLine: Int, editor: Editor) { + removeHighlighter(editor) var currentDocumentLine = startLine - - var deletedCharsCount = 0 - var addedCharsCount = 0 - var addedLinesCount = 0 - var deletedLinesCount = 0 - removeSelection(editor) diffRows.forEach { row -> when (row.tag) { DiffRow.Tag.EQUAL -> { currentDocumentLine++ } + DiffRow.Tag.DELETE -> { + val startOffset = getLineStartOffset(editor.document, currentDocumentLine) + val endOffset = getLineEndOffset(editor.document, currentDocumentLine, true) + highlightString(editor, startOffset, endOffset, false ) + currentDocumentLine++ + } + + DiffRow.Tag.CHANGE -> { + val startOffset = getLineStartOffset(editor.document, currentDocumentLine) + val endOffset = getLineEndOffset(editor.document, currentDocumentLine, true) + highlightString(editor, startOffset, endOffset, false ) + val insetStartOffset = getLineStartOffset(editor.document, currentDocumentLine+1) + val insertEndOffset = getLineEndOffset(editor.document, currentDocumentLine+1, true) + highlightString(editor, insetStartOffset, insertEndOffset, true ) + currentDocumentLine+=2 + } + + DiffRow.Tag.INSERT -> { + val insetStartOffset = getLineStartOffset(editor.document, currentDocumentLine) + val insertEndOffset = getLineEndOffset(editor.document, currentDocumentLine, true) + highlightString(editor, insetStartOffset, insertEndOffset, true ) + currentDocumentLine++ + } + } + } + } + + private fun applyChunk (recommendation: String, editor: Editor, startLine: Int, endLine: Int, diff: List) { + val startOffset = getLineStartOffset(editor.document, startLine) + val endOffset = getLineEndOffset(editor.document, endLine) + replaceString(editor.document, startOffset, endOffset, recommendation) + } + + private fun constructPatch (diff: List): String { + var patchString = "" + diff.forEach { row -> + when (row.tag) { + DiffRow.Tag.EQUAL -> { + patchString += row.oldLine + "\n" + } + + DiffRow.Tag.DELETE -> { + patchString += row.oldLine + "\n" + } + + DiffRow.Tag.CHANGE -> { + patchString += row.oldLine + "\n" + patchString += row.newLine + "\n" + } + + DiffRow.Tag.INSERT -> { + patchString += row.newLine + "\n" + } + } + } + return unescape(patchString) + } + + private fun finalComputation(selectedCode: String, finalMessage: String?) { + if(finalMessage == null) { + throw Exception("No suggestions from Q; please try a different instruction.") + } + var numSuggestionAddChars = 0 + var numSuggestionAddLines = 0 + var numSuggestionDelChars = 0 + var numSuggestionDelLines = 0 + val selection = selectedCode.split("\n") + val recommendationList = unescape(finalMessage).split("\n") + val diff = compareDiffs(selection, recommendationList) + var isAllEqual = true + diff.forEach { row -> + when (row.tag) { + DiffRow.Tag.EQUAL -> { + // no-op + } DiffRow.Tag.DELETE -> { isAllEqual = false - showCodeChangeInEditor(row, currentDocumentLine, editor) - currentDocumentLine++ - deletedLinesCount++ - deletedCharsCount += row.oldLine?.length ?: 0 + numSuggestionDelLines += 1 + numSuggestionDelChars += row.oldLine.length } DiffRow.Tag.CHANGE -> { - if (row.newLine.trimIndent() != row.oldLine?.trimIndent()) { - isAllEqual = false - showCodeChangeInEditor(row, currentDocumentLine, editor) - currentDocumentLine += 2 - deletedLinesCount++ - deletedCharsCount += row.oldLine?.length ?: 0 - addedLinesCount++ - addedCharsCount += row.newLine?.length ?: 0 - } else { - currentDocumentLine++ - } + isAllEqual = false + numSuggestionDelLines += 1 + numSuggestionDelChars += row.oldLine.length + numSuggestionAddLines += 1 + numSuggestionAddChars = row.newLine.length } DiffRow.Tag.INSERT -> { isAllEqual = false - showCodeChangeInEditor(row, currentDocumentLine, editor) - currentDocumentLine++ - addedLinesCount++ - addedCharsCount += row.newLine?.length ?: 0 + numSuggestionAddLines += 1 + numSuggestionAddChars += row.newLine.length } } } - metrics?.numSuggestionAddChars = addedCharsCount - metrics?.numSuggestionAddLines = addedLinesCount - metrics?.numSuggestionDelChars = deletedCharsCount - metrics?.numSuggestionDelLines = deletedLinesCount - return isAllEqual + metrics?.numSuggestionAddChars = numSuggestionAddChars + metrics?.numSuggestionAddLines = numSuggestionAddLines + metrics?.numSuggestionDelChars = numSuggestionDelChars + metrics?.numSuggestionDelLines = numSuggestionDelLines + if (isAllEqual) { + throw Exception("No suggestions from Q; please try a different instruction.") + } } private fun processChatDiff(selectedCode: String, event: ChatMessage, editor: Editor, selectionRange: RangeMarker) { + if(isAbandoned.get()) return if (event.message?.isNotEmpty() == true) { + logger.info { "inline chat recommendation: \n ${event.message}"} runBlocking { - while (partialUndoActions.isNotEmpty()) { - val action = partialUndoActions.pop() - runBlocking { action.invoke() } - } - partialAcceptActions.clear() - val recommendation = unescape(event.message) - if (selectedCode == recommendation) { - throw Exception("No suggestions from Q; please try a different instruction.") - } val selection = selectedCode.split("\n") val recommendationList = recommendation.split("\n") val diff = compareDiffs(selection, recommendationList) - + val startLine = getLineNumber(editor.document, selectionRange.startOffset) + val endLine = getLineNumber(editor.document, selectionRange.endOffset) + val patchString = constructPatch(diff) if (currentPopup?.isVisible != true) { logger.debug { "inline chat popup cancelled before diff is shown" } isInProgress.set(false) + isAbandoned.set(true) recordInlineChatTelemetry(InlineChatUserDecision.DISMISS) return@runBlocking } - - val isAllEqual = processDiffRows(diff, selectionRange, editor) - if (isAllEqual) { - throw Exception("No suggestions from Q; please try a different instruction.") + launch(EDT) { + removeSelection(editor) + applyChunk(patchString, editor, startLine, endLine, diff) + processHighlights(diff, startLine, editor) + }.join() + acceptAction = { + val startOffset = getLineStartOffset(editor.document, startLine) + val endOffset = getLineEndOffset(editor.document, endLine) + replaceString(editor.document, startOffset, endOffset, recommendation) + removeHighlighter(editor) + } + rejectAction = { + val startOffset = getLineStartOffset(editor.document, startLine) + val endOffset = getLineEndOffset(editor.document, max((startLine + patchString.split("\n").size - 1), endLine)) + replaceString(editor.document, startOffset, endOffset, selectedCode) + removeHighlighter(editor) } isInProgress.set(false) @@ -407,11 +493,14 @@ class InlineChatController( document.getLineStartOffset(row) } - private fun getLineEndOffset(document: Document, row: Int): Int = ReadAction.compute { + private fun getLineEndOffset(document: Document, row: Int, includeLastNewLine: Boolean = false): Int = ReadAction.compute { if (row == document.lineCount - 1) { document.getLineEndOffset(row) + } else if (row < document.lineCount - 1){ + val lineEnd = document.getLineEndOffset(row) + if (includeLastNewLine) lineEnd + 1 else lineEnd } else { - document.getLineEndOffset(row) + 1 + document.getLineEndOffset((document.lineCount - 1).coerceAtLeast(0)) } } @@ -426,8 +515,7 @@ class InlineChatController( CommandProcessor.getInstance().runUndoTransparentAction { WriteCommandAction.runWriteCommandAction(project) { editor.document.insertString(offset, text) - val row = editor.document.getLineNumber(offset) - rangeMarker = editor.document.createRangeMarker(offset, getLineEndOffset(editor.document, row)) + rangeMarker = editor.document.createRangeMarker(offset, offset + text.length) } rangeMarker?.let { marker -> highlightCodeWithBackgroundColor(editor, marker.startOffset, marker.endOffset, true) @@ -438,11 +526,11 @@ class InlineChatController( return rangeMarker!! } - private fun deleteString(document: Document, start: Int, end: Int) { + private fun replaceString(document: Document, start: Int, end: Int, text: String) { ApplicationManager.getApplication().invokeAndWait { CommandProcessor.getInstance().runUndoTransparentAction { WriteCommandAction.runWriteCommandAction(project) { - document.deleteString(start, end) + document.replaceString(start, end, text) } } } @@ -461,66 +549,13 @@ class InlineChatController( return rangeMarker!! } - private fun showCodeChangeInEditor(diffRow: DiffRow, row: Int, editor: Editor) { - try { - val document = editor.document - when (diffRow.tag) { - DiffRow.Tag.DELETE -> { - val changeStartOffset = getLineStartOffset(document, row) - val changeEndOffset = getLineEndOffset(document, row) - val rangeMarker = highlightString(editor, changeStartOffset, changeEndOffset, false) - partialUndoActions.add { - editor.markupModel.removeAllHighlighters() - } - partialAcceptActions.add { - if (rangeMarker.isValid) { - deleteString(document, rangeMarker.startOffset, rangeMarker.endOffset) - } - editor.markupModel.removeAllHighlighters() - } - } - - DiffRow.Tag.INSERT -> { - val newLineInserted = insertNewLineIfNeeded(row, editor) - val insertOffset = getLineStartOffset(document, row) - val textToInsert = unescape(diffRow.newLine) + "\n" - val rangeMarker = insertString(editor, insertOffset, textToInsert) - partialUndoActions.add { - if (rangeMarker.isValid) { - deleteString(document, rangeMarker.startOffset, (rangeMarker.endOffset + newLineInserted).coerceAtMost(document.textLength)) - } - editor.markupModel.removeAllHighlighters() - } - partialAcceptActions.add { - editor.markupModel.removeAllHighlighters() - } - } - - else -> { - val changeOffset = getLineStartOffset(document, row) - val changeEndOffset = getLineEndOffset(document, row) - val oldTextRangeMarker = highlightString(editor, changeOffset, changeEndOffset, false) - partialAcceptActions.add { - if (oldTextRangeMarker.isValid) { - deleteString(document, oldTextRangeMarker.startOffset, oldTextRangeMarker.endOffset) - } - editor.markupModel.removeAllHighlighters() - } - val insertOffset = getLineEndOffset(document, row) - val newLineInserted = insertNewLineIfNeeded(row, editor) - val textToInsert = unescape(diffRow.newLine) + "\n" - val newTextRangeMarker = insertString(editor, insertOffset, textToInsert) - partialUndoActions.add { - if (newTextRangeMarker.isValid) { - deleteString(document, newTextRangeMarker.startOffset, newTextRangeMarker.endOffset + newLineInserted) - } - editor.markupModel.removeAllHighlighters() - } + private fun removeHighlighter(editor: Editor){ + ApplicationManager.getApplication().invokeAndWait { + CommandProcessor.getInstance().runUndoTransparentAction { + WriteCommandAction.runWriteCommandAction(project) { + editor.markupModel.removeAllHighlighters() } } - } catch (e: Exception) { - logger.warn { "Error when showing inline chat diff in editor: ${e.message} \n ${e.stackTraceToString()}" } - throw Exception("Error processing request; please try again.") } } @@ -549,6 +584,8 @@ class InlineChatController( } private suspend fun handleChat(message: String, selectedCode: String = "", editor: Editor, selectedLineStart: Int): String { + insertionLine.set(-1) + isAbandoned.set(false) val authError = checkCredentials() if (authError != null) { return authError @@ -590,6 +627,7 @@ class InlineChatController( ) val sessionInfo = sessionStorage.getSession("inlineChat-editor", project) + val mutex = Mutex() sessionInfo.history.add(requestData) var errorMessage = "" @@ -604,18 +642,26 @@ class InlineChatController( isInlineChat = true ) .catch { e -> + canPopupAbort.set(true) + undoChanges() logger.warn { "Error in inline chat request: ${e.message}" } errorMessage = "Error processing request; please try again" } .onEach { event: ChatMessage -> if (event.message?.isNotEmpty() == true && prevMessage != event.message) { - try { - processNewCode(editor, selectedLineStart, unescape(event.message), prevMessage) - } catch (e: Exception) { - logger.warn { "error streaming chat message to editor: ${e.stackTraceToString()}" } - errorMessage = "Error processing request; please try again." + mutex.withLock { + try { + if (selectionRange != null) { + processChatDiff(selectedCode, event, editor, selectionRange!!) + } else { + processNewCode(editor, selectedLineStart, unescape(event.message), prevMessage) + } + } catch (e: Exception) { + logger.warn { "error streaming chat message to editor: ${e.stackTraceToString()}" } + errorMessage = "Error processing request; please try again." + } + prevMessage = unescape(event.message) } - prevMessage = unescape(event.message) } if (messages.isEmpty()) { firstResponseLatency = (System.currentTimeMillis() - startTime).toDouble() @@ -626,15 +672,6 @@ class InlineChatController( } chat.await() val finalMessage = messages.lastOrNull { m -> m.messageType == ChatMessageType.AnswerPart } - if (selectionRange != null && finalMessage != null) { - try { - processChatDiff(selectedCode, finalMessage, editor, selectionRange!!) - } catch (e: Exception) { - logger.warn { "error precessing chat diff in editor: ${e.stackTraceToString()}" } - errorMessage = "Error processing request; please try again." - } - } - insertionLine.set(-1) val lastResponseLatency = (System.currentTimeMillis() - startTime).toDouble() val requestId = messages.lastOrNull()?.messageId requestId?.let { @@ -643,7 +680,15 @@ class InlineChatController( codeIntent = true, responseStartLatency = firstResponseLatency, responseEndLatency = lastResponseLatency ) } + if(finalMessage != null) { + try { + finalComputation(selectedCode, finalMessage.message) + } catch (e: Exception) { + errorMessage = e.message ?: "Error processing request; please try again." + } + } if (errorMessage.isNotEmpty()) { + canPopupAbort.set(true) undoChanges() } return errorMessage From ad0a3af512f6b7d6918004ee0306fd9f8c022b76 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Fri, 25 Oct 2024 09:40:46 -0700 Subject: [PATCH 31/39] fix request and prompt --- .../context/file/FileContextExtractor.kt | 2 +- .../context/file/util/LanguageExtractor.kt | 13 ++-------- .../focusArea/FocusAreaContextExtractor.kt | 2 +- .../cwc/inline/InlineChatController.kt | 24 +++++++------------ .../cwc/inline/InlineChatPopupFactory.kt | 6 ++++- 5 files changed, 18 insertions(+), 29 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/FileContextExtractor.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/FileContextExtractor.kt index 36249508b44..888bdb198bb 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/FileContextExtractor.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/FileContextExtractor.kt @@ -22,7 +22,7 @@ class FileContextExtractor(private val fqnWebviewAdapter: FqnWebviewAdapter?, pr } ?: return null val fileLanguage = computeOnEdt { - languageExtractor.extractLanguageNameFromCurrentFile(editor, project) + languageExtractor.extractLanguageNameFromCurrentFile(editor) } val fileText = computeOnEdt { editor.document.text diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt index a63cc67d429..e292fa2b351 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt @@ -3,18 +3,9 @@ package software.aws.toolkits.jetbrains.services.cwc.editor.context.file.util -import com.intellij.openapi.application.runReadAction -import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor -import com.intellij.openapi.project.Project -import com.intellij.psi.PsiDocumentManager -import com.intellij.psi.PsiFile +import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage class LanguageExtractor { - fun extractLanguageNameFromCurrentFile(editor: Editor, project: Project): String? = - runReadAction { - val doc: Document = editor.document - val psiFile: PsiFile? = PsiDocumentManager.getInstance(project).getPsiFile(doc) - psiFile?.fileType?.name?.lowercase() - } + fun extractLanguageNameFromCurrentFile(editor: Editor): String = editor.virtualFile.programmingLanguage().languageId } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt index 58abdd8e64f..62260f9e262 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt @@ -106,7 +106,7 @@ class FocusAreaContextExtractor(private val fqnWebviewAdapter: FqnWebviewAdapter // Retrieve from trimmedFileText val fileLanguage = computeOnEdt { - languageExtractor.extractLanguageNameFromCurrentFile(editor, project) + languageExtractor.extractLanguageNameFromCurrentFile(editor) } val fileText = editor.document.text val fileName = FileEditorManager.getInstance(project).selectedFiles.first().name diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 0f4a634671d..2a6ed7441a4 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -55,6 +55,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.customization.Code import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerType +import software.aws.toolkits.jetbrains.services.cwc.clients.chat.v1.ChatSessionFactoryV1 import software.aws.toolkits.jetbrains.services.cwc.controller.chat.messenger.ChatPromptHandler import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.TelemetryHelper import software.aws.toolkits.jetbrains.services.cwc.controller.chat.userIntent.UserIntentRecognizer @@ -63,6 +64,7 @@ import software.aws.toolkits.jetbrains.services.cwc.editor.context.ExtractionTri import software.aws.toolkits.jetbrains.services.cwc.inline.listeners.InlineChatFileListener import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessage import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType +import software.aws.toolkits.jetbrains.services.cwc.storage.ChatSessionInfo import software.aws.toolkits.jetbrains.services.cwc.storage.ChatSessionStorage import software.aws.toolkits.telemetry.FeatureId import java.util.UUID @@ -601,23 +603,16 @@ class InlineChatController( val startTime = System.currentTimeMillis() var firstResponseLatency = 0.0 val messages = mutableListOf() - val triggerId = UUID.randomUUID().toString() val intentRecognizer = UserIntentRecognizer() - val language = editor.virtualFile?.programmingLanguage() - var prompt = "" - if (selectedCode.isNotBlank()) { prompt += "$selectedCode\n" } - prompt += "$message\n" - prompt += "${if (editor.document.text.isNotEmpty()) editor.document.text.take(8000) else "file written in $language"}" - - logger.debug { "Inline chat prompt: $prompt" } - val contextExtractor = ActiveFileContextExtractor.create(fqnWebviewAdapter = null, project = project) val fileContext = contextExtractor.extractContextForTrigger(ExtractionTriggerType.ChatMessage) + val tabId = UUID.randomUUID().toString() + val requestData = ChatRequestData( - tabId = "inlineChat-editor", - message = prompt, + tabId = tabId, + message = message, activeFileContext = fileContext, userIntent = intentRecognizer.getUserIntentFromPromptChatMessage(message, null), triggerType = TriggerType.Inline, @@ -626,16 +621,15 @@ class InlineChatController( useRelevantDocuments = false ) - val sessionInfo = sessionStorage.getSession("inlineChat-editor", project) + val sessionInfo = sessionStorage.getSession(tabId, project) val mutex = Mutex() - sessionInfo.history.add(requestData) var errorMessage = "" var prevMessage = "" val chat = sessionInfo.scope.async { ChatPromptHandler(telemetryHelper).handle( - "inlineChat-editor", - triggerId, + tabId, + UUID.randomUUID().toString(), requestData, sessionInfo, shouldAddIndexInProgressMessage = false, diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index 2c0121cd436..762c60ee482 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -17,6 +17,8 @@ import com.intellij.ui.awt.RelativePoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import software.aws.toolkits.jetbrains.core.coroutines.EDT import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER import software.aws.toolkits.resources.AmazonQBundle.message import java.awt.Point @@ -77,7 +79,9 @@ class InlineChatPopupFactory( val rejectAction = { rejectHandler.invoke() } - addCodeActionsPanel(acceptAction, rejectAction) + withContext(EDT) { + addCodeActionsPanel(acceptAction, rejectAction) + } } } } From 377385a40abd4d9397d0094efaf7193c9833169a Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Fri, 25 Oct 2024 11:14:22 -0700 Subject: [PATCH 32/39] fix detekt --- .../focusArea/FocusAreaContextExtractor.kt | 3 +- .../cwc/inline/InlineChatController.kt | 67 +++++++++---------- .../cwc/inline/OpenChatInputAction.kt | 3 - 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt index 62260f9e262..b494ddbae4f 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt @@ -141,7 +141,8 @@ class FocusAreaContextExtractor(private val fqnWebviewAdapter: FqnWebviewAdapter val requestString = ChatController.objectMapper.writeValueAsString(extractNamesRequest) codeNames = try { - fqnWebviewAdapter?.let { ChatController.objectMapper.readValue(it.extractNames(requestString), CodeNamesImpl::class.java) + fqnWebviewAdapter?.let { + ChatController.objectMapper.readValue(it.extractNames(requestString), CodeNamesImpl::class.java) } ?: CodeNamesImpl(simpleNames = emptyList(), fullyQualifiedNames = FullyQualifiedNames(used = emptyList())) } catch (e: Exception) { getLogger().warn(e) { "Failed to extract names from file" } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 2a6ed7441a4..7d77851905d 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -33,12 +33,17 @@ import com.intellij.openapi.wm.ToolWindowManager import com.intellij.ui.JBColor import com.intellij.ui.jcef.JBCefApp import com.jetbrains.rd.util.AtomicInteger -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext import org.apache.commons.text.StringEscapeUtils import software.amazon.awssdk.services.codewhispererruntime.model.InlineChatUserDecision import software.aws.toolkits.core.utils.debug @@ -52,10 +57,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AMAZON_Q_WINDOW_ID import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator -import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerType -import software.aws.toolkits.jetbrains.services.cwc.clients.chat.v1.ChatSessionFactoryV1 import software.aws.toolkits.jetbrains.services.cwc.controller.chat.messenger.ChatPromptHandler import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.TelemetryHelper import software.aws.toolkits.jetbrains.services.cwc.controller.chat.userIntent.UserIntentRecognizer @@ -64,7 +67,6 @@ import software.aws.toolkits.jetbrains.services.cwc.editor.context.ExtractionTri import software.aws.toolkits.jetbrains.services.cwc.inline.listeners.InlineChatFileListener import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessage import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType -import software.aws.toolkits.jetbrains.services.cwc.storage.ChatSessionInfo import software.aws.toolkits.jetbrains.services.cwc.storage.ChatSessionStorage import software.aws.toolkits.telemetry.FeatureId import java.util.UUID @@ -96,7 +98,6 @@ class InlineChatController( project.messageBus.connect(this).subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, listener) } - data class InlineChatMetrics( val requestId: String, val inputLength: Int? = null, @@ -123,6 +124,7 @@ class InlineChatController( } val popupCancelHandler: (editor: Editor) -> Unit = { editor -> + isAbandoned.set(true) if (canPopupAbort.get() && currentPopup != null) { undoChanges() restoreSelection(editor) @@ -140,7 +142,7 @@ class InlineChatController( metrics?.charactersAdded = metrics?.numSuggestionAddChars metrics?.charactersRemoved = metrics?.numSuggestionDelChars } - if(metrics?.requestId?.isNotEmpty() == true){ + if (metrics?.requestId?.isNotEmpty() == true) { telemetryHelper.recordInlineChatTelemetry( metrics?.requestId!!, metrics?.inputLength, @@ -241,13 +243,13 @@ class InlineChatController( private fun highlightCodeWithBackgroundColor(editor: Editor, startOffset: Int, endOffset: Int, isGreen: Boolean) { val greenBackgroundAttributes = TextAttributes().apply { - backgroundColor = JBColor(0x66BB6A, 0x006400) - effectColor = JBColor(0x66BB6A, 0x006400) + backgroundColor = JBColor(0xAADEAA, 0x447152) + effectColor = JBColor(0xAADEAA, 0x447152) } val redBackgroundAttributes = TextAttributes().apply { - backgroundColor = JBColor(0xEF9A9A, 0x8B0000) - effectColor = JBColor(0xEF9A9A, 0x8B0000) + backgroundColor = JBColor(0xFFC8BD, 0x8F5247) + effectColor = JBColor(0xFFC8BD, 0x8F5247) } val attributes = if (isGreen) greenBackgroundAttributes else redBackgroundAttributes rangeHighlighter = editor.markupModel.addRangeHighlighter( @@ -284,7 +286,7 @@ class InlineChatController( .replace("=>", "=>") private fun processNewCode(editor: Editor, line: Int, code: String, prevMessage: String) { - if(isAbandoned.get()) return + if (isAbandoned.get()) return runBlocking { logger.debug { "received inline chat recommendation with code: \n $code" } var insertLine = line @@ -336,37 +338,37 @@ class InlineChatController( DiffRow.Tag.DELETE -> { val startOffset = getLineStartOffset(editor.document, currentDocumentLine) val endOffset = getLineEndOffset(editor.document, currentDocumentLine, true) - highlightString(editor, startOffset, endOffset, false ) + highlightString(editor, startOffset, endOffset, false) currentDocumentLine++ } DiffRow.Tag.CHANGE -> { val startOffset = getLineStartOffset(editor.document, currentDocumentLine) val endOffset = getLineEndOffset(editor.document, currentDocumentLine, true) - highlightString(editor, startOffset, endOffset, false ) - val insetStartOffset = getLineStartOffset(editor.document, currentDocumentLine+1) - val insertEndOffset = getLineEndOffset(editor.document, currentDocumentLine+1, true) - highlightString(editor, insetStartOffset, insertEndOffset, true ) - currentDocumentLine+=2 + highlightString(editor, startOffset, endOffset, false) + val insetStartOffset = getLineStartOffset(editor.document, currentDocumentLine + 1) + val insertEndOffset = getLineEndOffset(editor.document, currentDocumentLine + 1, true) + highlightString(editor, insetStartOffset, insertEndOffset, true) + currentDocumentLine += 2 } DiffRow.Tag.INSERT -> { val insetStartOffset = getLineStartOffset(editor.document, currentDocumentLine) val insertEndOffset = getLineEndOffset(editor.document, currentDocumentLine, true) - highlightString(editor, insetStartOffset, insertEndOffset, true ) + highlightString(editor, insetStartOffset, insertEndOffset, true) currentDocumentLine++ } } } } - private fun applyChunk (recommendation: String, editor: Editor, startLine: Int, endLine: Int, diff: List) { + private fun applyChunk(recommendation: String, editor: Editor, startLine: Int, endLine: Int) { val startOffset = getLineStartOffset(editor.document, startLine) val endOffset = getLineEndOffset(editor.document, endLine) replaceString(editor.document, startOffset, endOffset, recommendation) } - private fun constructPatch (diff: List): String { + private fun constructPatch(diff: List): String { var patchString = "" diff.forEach { row -> when (row.tag) { @@ -379,7 +381,7 @@ class InlineChatController( } DiffRow.Tag.CHANGE -> { - patchString += row.oldLine + "\n" + patchString += row.oldLine + "\n" patchString += row.newLine + "\n" } @@ -392,7 +394,7 @@ class InlineChatController( } private fun finalComputation(selectedCode: String, finalMessage: String?) { - if(finalMessage == null) { + if (finalMessage == null) { throw Exception("No suggestions from Q; please try a different instruction.") } var numSuggestionAddChars = 0 @@ -440,9 +442,9 @@ class InlineChatController( } private fun processChatDiff(selectedCode: String, event: ChatMessage, editor: Editor, selectionRange: RangeMarker) { - if(isAbandoned.get()) return + if (isAbandoned.get()) return if (event.message?.isNotEmpty() == true) { - logger.info { "inline chat recommendation: \n ${event.message}"} + logger.info { "inline chat recommendation: \n ${event.message}" } runBlocking { val recommendation = unescape(event.message) val selection = selectedCode.split("\n") @@ -460,10 +462,10 @@ class InlineChatController( } launch(EDT) { removeSelection(editor) - applyChunk(patchString, editor, startLine, endLine, diff) + applyChunk(patchString, editor, startLine, endLine) processHighlights(diff, startLine, editor) }.join() - acceptAction = { + acceptAction = { val startOffset = getLineStartOffset(editor.document, startLine) val endOffset = getLineEndOffset(editor.document, endLine) replaceString(editor.document, startOffset, endOffset, recommendation) @@ -498,7 +500,7 @@ class InlineChatController( private fun getLineEndOffset(document: Document, row: Int, includeLastNewLine: Boolean = false): Int = ReadAction.compute { if (row == document.lineCount - 1) { document.getLineEndOffset(row) - } else if (row < document.lineCount - 1){ + } else if (row < document.lineCount - 1) { val lineEnd = document.getLineEndOffset(row) if (includeLastNewLine) lineEnd + 1 else lineEnd } else { @@ -538,20 +540,17 @@ class InlineChatController( } } - private fun highlightString(editor: Editor, start: Int, end: Int, isInsert: Boolean): RangeMarker { - var rangeMarker: RangeMarker? = null + private fun highlightString(editor: Editor, start: Int, end: Int, isInsert: Boolean) { ApplicationManager.getApplication().invokeAndWait { CommandProcessor.getInstance().runUndoTransparentAction { WriteCommandAction.runWriteCommandAction(project) { - rangeMarker = editor.document.createRangeMarker(start, end) - highlightCodeWithBackgroundColor(editor, rangeMarker!!.startOffset, rangeMarker!!.endOffset, isInsert) + highlightCodeWithBackgroundColor(editor, start, end, isInsert) } } } - return rangeMarker!! } - private fun removeHighlighter(editor: Editor){ + private fun removeHighlighter(editor: Editor) { ApplicationManager.getApplication().invokeAndWait { CommandProcessor.getInstance().runUndoTransparentAction { WriteCommandAction.runWriteCommandAction(project) { @@ -674,7 +673,7 @@ class InlineChatController( codeIntent = true, responseStartLatency = firstResponseLatency, responseEndLatency = lastResponseLatency ) } - if(finalMessage != null) { + if (finalMessage != null) { try { finalComputation(selectedCode, finalMessage.message) } catch (e: Exception) { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt index 340585dd259..e35d5a1ee67 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt @@ -6,9 +6,6 @@ package software.aws.toolkits.jetbrains.services.cwc.inline import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.event.CaretEvent -import com.intellij.openapi.editor.event.CaretListener class OpenChatInputAction : AnAction() { override fun actionPerformed(e: AnActionEvent) { From dba54c2b25ac69a06ea47ae96424e5386a3a1484 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Sat, 26 Oct 2024 19:40:04 -0700 Subject: [PATCH 33/39] fixes --- .../resources/META-INF/plugin-chat.xml | 5 ++-- .../cwc/inline/InlineChatController.kt | 12 +++++--- .../cwc/inline/InlineChatPopupFactory.kt | 6 ++-- .../cwc/inline/InlineChatPopupPanel.kt | 29 +++++++++++++------ .../services/amazonq/TelemetryHelperTest.kt | 2 +- .../resources/AmazonQBundle.properties | 10 +++---- .../amazonq_inline_chat_cancel_icon.svg | 3 -- .../amazonq_inline_chat_confirm_icon.svg | 3 -- .../inlinechat/amazonq_inline_chat_reject.svg | 4 --- .../jetbrains-community/src/icons/AwsIcons.kt | 4 --- 10 files changed, 39 insertions(+), 39 deletions(-) delete mode 100644 plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_cancel_icon.svg delete mode 100644 plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_confirm_icon.svg delete mode 100644 plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_reject.svg diff --git a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml index 68d0b7737b6..f5185272538 100644 --- a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml +++ b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml @@ -30,8 +30,9 @@ - - + + + diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 7d77851905d..e428a2ceeea 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -243,13 +243,13 @@ class InlineChatController( private fun highlightCodeWithBackgroundColor(editor: Editor, startOffset: Int, endOffset: Int, isGreen: Boolean) { val greenBackgroundAttributes = TextAttributes().apply { - backgroundColor = JBColor(0xAADEAA, 0x447152) - effectColor = JBColor(0xAADEAA, 0x447152) + backgroundColor = JBColor(0xAADEAA, 0x294436) + effectColor = JBColor(0xAADEAA, 0x294436) } val redBackgroundAttributes = TextAttributes().apply { - backgroundColor = JBColor(0xFFC8BD, 0x8F5247) - effectColor = JBColor(0xFFC8BD, 0x8F5247) + backgroundColor = JBColor(0xFFC8BD, 0x45302B) + effectColor = JBColor(0xFFC8BD, 0x45302B) } val attributes = if (isGreen) greenBackgroundAttributes else redBackgroundAttributes rangeHighlighter = editor.markupModel.addRangeHighlighter( @@ -395,6 +395,7 @@ class InlineChatController( private fun finalComputation(selectedCode: String, finalMessage: String?) { if (finalMessage == null) { + canPopupAbort.set(true) throw Exception("No suggestions from Q; please try a different instruction.") } var numSuggestionAddChars = 0 @@ -437,6 +438,7 @@ class InlineChatController( metrics?.numSuggestionDelChars = numSuggestionDelChars metrics?.numSuggestionDelLines = numSuggestionDelLines if (isAllEqual) { + canPopupAbort.set(true) throw Exception("No suggestions from Q; please try a different instruction.") } } @@ -650,6 +652,8 @@ class InlineChatController( processNewCode(editor, selectedLineStart, unescape(event.message), prevMessage) } } catch (e: Exception) { + canPopupAbort.set(true) + undoChanges() logger.warn { "error streaming chat message to editor: ${e.stackTraceToString()}" } errorMessage = "Error processing request; please try again." } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index 762c60ee482..be3c8acdcc8 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -54,11 +54,11 @@ class InlineChatPopupFactory( } val submitListener: () -> Unit = { - submitButton.isEnabled = false - cancelButton.isEnabled = false - textField.isEnabled = false val prompt = textField.text if (prompt.isNotBlank()) { + submitButton.isEnabled = false + cancelButton.isEnabled = false + textField.isEnabled = false setLabel(message("amazonqInlineChat.popup.generating")) revalidate() diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index ee579c48e13..f518ca84a51 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -20,12 +20,13 @@ import java.awt.BorderLayout import java.awt.Dimension import java.awt.Font import javax.swing.BorderFactory -import javax.swing.Icon import javax.swing.JButton import javax.swing.JLabel import javax.swing.JPanel import javax.swing.JTextField import javax.swing.SwingConstants +import javax.swing.event.DocumentEvent +import javax.swing.event.DocumentListener class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { private var submitClickListener: (() -> Unit)? = null @@ -37,11 +38,23 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() private val popupInputHeight = 40 private val popupInputWidth = 500 - val textField = createTextField() + val textField = createTextField().apply { + document.addDocumentListener(object : DocumentListener { + fun updateButtonState() { + submitButton.isEnabled = text.isNotEmpty() + } + + override fun insertUpdate(e: DocumentEvent) = updateButtonState() + override fun removeUpdate(e: DocumentEvent) = updateButtonState() + override fun changedUpdate(e: DocumentEvent) = updateButtonState() + }) + } - val submitButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, message("amazonqInlineChat.popup.confirm")) + val submitButton = createButton(message("amazonqInlineChat.popup.confirm")).apply { + isEnabled = false + } - val cancelButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.REJECT, message("amazonqInlineChat.popup.cancel")).apply { + val cancelButton = createButton(message("amazonqInlineChat.popup.cancel")).apply { addActionListener { if (!Disposer.isDisposed(parentDisposable)) { Disposer.dispose(parentDisposable) @@ -55,8 +68,8 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() add(cancelButton, BorderLayout.EAST) } - private val acceptButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.CONFIRM, message("amazonqInlineChat.popup.accept")) - private val rejectButton = createButtonWithIcon(AwsIcons.Resources.InlineChat.REJECT, message("amazonqInlineChat.popup.reject")) + private val acceptButton = createButton(message("amazonqInlineChat.popup.accept")) + private val rejectButton = createButton(message("amazonqInlineChat.popup.reject")) private val textLabel = JLabel(message("amazonqInlineChat.popup.editCode"), AwsIcons.Logos.AWS_Q_GREY, SwingConstants.RIGHT).apply { font = font.deriveFont(popupButtonFontSize) } @@ -86,10 +99,8 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize) } - private fun createButtonWithIcon(icon: Icon, text: String): JButton = JButton(text).apply { - horizontalTextPosition = SwingConstants.LEFT + private fun createButton(text: String): JButton = JButton(text).apply { preferredSize = Dimension(popupButtonWidth, popupButtonHeight) - setIcon(icon) isOpaque = false isContentAreaFilled = false isBorderPainted = false diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/TelemetryHelperTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/TelemetryHelperTest.kt index 4bfc2466402..a0502367d7c 100644 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/TelemetryHelperTest.kt +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/TelemetryHelperTest.kt @@ -180,7 +180,7 @@ class TelemetryHelperTest { sessionStorage = mock { on { this.getSession(eq(tabId)) } doReturn ChatSessionInfo(session = mockSession, scope = mock(), history = mutableListOf()) } - sut = TelemetryHelper(appInitContext, sessionStorage) + sut = TelemetryHelper(appInitContext.project, sessionStorage) // set up client mockClientManager.create() diff --git a/plugins/amazonq/shared/jetbrains-community/resources/software/aws/toolkits/resources/AmazonQBundle.properties b/plugins/amazonq/shared/jetbrains-community/resources/software/aws/toolkits/resources/AmazonQBundle.properties index c0bd78f26bb..6e675823089 100644 --- a/plugins/amazonq/shared/jetbrains-community/resources/software/aws/toolkits/resources/AmazonQBundle.properties +++ b/plugins/amazonq/shared/jetbrains-community/resources/software/aws/toolkits/resources/AmazonQBundle.properties @@ -1,13 +1,11 @@ action.q.hello.description=Hello description -amazonqInlineChat.gutter.tooltip = Amazon Q amazonqInlineChat.hint.edit = Edit -amazonqInlineChat.hint.windows.shortCut = (Ctrl + I) -amazonqInlineChat.popup.accept=Accept -amazonqInlineChat.popup.cancel=Cancel -amazonqInlineChat.popup.confirm=Confirm +amazonqInlineChat.popup.accept=Accept \u23CE +amazonqInlineChat.popup.cancel=Cancel \u238B +amazonqInlineChat.popup.confirm=Confirm \u23CE amazonqInlineChat.popup.editCode = Edit Code amazonqInlineChat.popup.generating = Generating... -amazonqInlineChat.popup.reject=Reject +amazonqInlineChat.popup.reject=Reject \u238B amazonqInlineChat.popup.title=Enter Instructions for Q amazonq.refresh.panel=Refresh Chat Session q.hello=Hello diff --git a/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_cancel_icon.svg b/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_cancel_icon.svg deleted file mode 100644 index 3e3e1560291..00000000000 --- a/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_cancel_icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_confirm_icon.svg b/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_confirm_icon.svg deleted file mode 100644 index 40b16f05129..00000000000 --- a/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_confirm_icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_reject.svg b/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_reject.svg deleted file mode 100644 index 1f583d66e03..00000000000 --- a/plugins/core/jetbrains-community/resources/icons/resources/inlinechat/amazonq_inline_chat_reject.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/plugins/core/jetbrains-community/src/icons/AwsIcons.kt b/plugins/core/jetbrains-community/src/icons/AwsIcons.kt index 78488c95943..9a4703ce28b 100644 --- a/plugins/core/jetbrains-community/src/icons/AwsIcons.kt +++ b/plugins/core/jetbrains-community/src/icons/AwsIcons.kt @@ -127,10 +127,6 @@ object AwsIcons { } object InlineChat { - @JvmField val CONFIRM = load("/icons/resources/inlinechat/amazonq_inline_chat_confirm_icon.svg") - - @JvmField val REJECT = load("/icons/resources/inlinechat/amazonq_inline_chat_reject.svg") - @JvmField val AWS_Q_INLINECHAT_SHORTCUT = load("/icons/resources/inlinechat/amazonq_inline_chat_shortcut.svg") } } From 7068e2645ee0c97deddca4d9080907684630d893 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Sun, 27 Oct 2024 10:05:57 -0700 Subject: [PATCH 34/39] add to right click context menu --- .../resources/META-INF/plugin-chat.xml | 12 +++++++----- .../aws/toolkits/resources/MessagesBundle.properties | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml index f5185272538..3272dc35bfc 100644 --- a/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml +++ b/plugins/amazonq/chat/jetbrains-community/resources/META-INF/plugin-chat.xml @@ -29,11 +29,6 @@ - - - - - @@ -84,6 +79,13 @@ class="software.aws.toolkits.jetbrains.services.cwc.commands.SendToPromptAction"> + + + + + + diff --git a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties index 6bb9257b5ee..2d5696f7b8a 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -23,6 +23,7 @@ action.aws.toolkit.jetbrains.core.services.cwc.commands.FixCodeAction.descriptio action.aws.toolkit.jetbrains.core.services.cwc.commands.FixCodeAction.text = Fix Code action.aws.toolkit.jetbrains.core.services.cwc.commands.GenerateUnitTestsAction.description = Generates unit tests for the selected code action.aws.toolkit.jetbrains.core.services.cwc.commands.GenerateUnitTestsAction.text = Generate Tests (Beta) +action.aws.toolkit.jetbrains.core.services.cwc.inline.openChat.text = Inline Chat action.aws.toolkit.jetbrains.core.services.cwc.commands.OptimizeCodeAction.description = Optimizes the selected code action.aws.toolkit.jetbrains.core.services.cwc.commands.OptimizeCodeAction.text = Optimize Code action.aws.toolkit.jetbrains.core.services.cwc.commands.RefactorCodeAction.description = Refactors the selected code From 7f1e9f4549209d3e444a40ed3888e401c38e1c81 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Sun, 27 Oct 2024 11:21:28 -0700 Subject: [PATCH 35/39] fix error handling --- .../services/cwc/controller/ChatController.kt | 2 +- .../cwc/controller/ReferenceLogController.kt | 5 ++-- .../cwc/inline/InlineChatController.kt | 30 +++++++++++++++++-- .../cwc/inline/InlineChatPopupFactory.kt | 7 +++-- .../cwc/inline/InlineChatPopupPanel.kt | 22 ++++++++++++-- .../resources/MessagesBundle.properties | 2 +- 6 files changed, 57 insertions(+), 11 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt index 1aa9cb5e083..b2cb29d8de5 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt @@ -217,7 +217,7 @@ class ChatController private constructor( editor.document.insertString(offset, message.code) - ReferenceLogController.addReferenceLog(message.code, message.codeReference, editor, context.project) + ReferenceLogController.addReferenceLog(message.code, message.codeReference, editor, context.project, null) CodeWhispererUserModificationTracker.getInstance(context.project).enqueue( InsertedCodeModificationEntry( diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ReferenceLogController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ReferenceLogController.kt index b316ae1d032..13537f5b933 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ReferenceLogController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ReferenceLogController.kt @@ -9,11 +9,12 @@ import software.amazon.awssdk.services.codewhispererruntime.model.Reference import software.amazon.awssdk.services.codewhispererruntime.model.Span import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.CodeReferenceGenerated import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil +import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretPosition import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager import software.aws.toolkits.jetbrains.services.cwc.messages.CodeReference object ReferenceLogController { - fun addReferenceLog(originalCode: String, codeReferences: List?, editor: Editor, project: Project) { + fun addReferenceLog(originalCode: String, codeReferences: List?, editor: Editor, project: Project, inlineChatStartPosition: CaretPosition?) { codeReferences?.let { references -> val cwReferences = references.map { reference -> Reference.builder() @@ -36,7 +37,7 @@ object ReferenceLogController { originalCode, cwReferences, editor, - CodeWhispererEditorUtil.getCaretPosition(editor), + inlineChatStartPosition ?: CodeWhispererEditorUtil.getCaretPosition(editor), null, ) } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index e428a2ceeea..57943a90587 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -57,8 +57,10 @@ import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AMAZON_Q_WINDOW_ID import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator +import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretPosition import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.TriggerType +import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController import software.aws.toolkits.jetbrains.services.cwc.controller.chat.messenger.ChatPromptHandler import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.TelemetryHelper import software.aws.toolkits.jetbrains.services.cwc.controller.chat.userIntent.UserIntentRecognizer @@ -68,6 +70,7 @@ import software.aws.toolkits.jetbrains.services.cwc.inline.listeners.InlineChatF import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessage import software.aws.toolkits.jetbrains.services.cwc.messages.ChatMessageType import software.aws.toolkits.jetbrains.services.cwc.storage.ChatSessionStorage +import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.telemetry.FeatureId import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean @@ -285,9 +288,10 @@ class InlineChatController( .replace("'", "'") .replace("=>", "=>") - private fun processNewCode(editor: Editor, line: Int, code: String, prevMessage: String) { + private fun processNewCode(editor: Editor, line: Int, event: ChatMessage, prevMessage: String) { if (isAbandoned.get()) return runBlocking { + val code = event.message?.let { unescape(it) } ?: return@runBlocking logger.debug { "received inline chat recommendation with code: \n $code" } var insertLine = line var linesToAdd = emptyList() @@ -315,6 +319,12 @@ class InlineChatController( insertionLine.set(insertLine) acceptAction = { removeHighlighter(editor) + try { + val caretPosition = CaretPosition(offset = getLineStartOffset(editor.document, line), line = line) + ReferenceLogController.addReferenceLog(code, event.codeReference, editor, project, inlineChatStartPosition = caretPosition) + } catch (e: Exception) { + logger.warn { "error logging reference for inline chat: ${e.stackTraceToString()}" } + } } rejectAction = { val startOffset = getLineStartOffset(editor.document, line) @@ -469,9 +479,16 @@ class InlineChatController( }.join() acceptAction = { val startOffset = getLineStartOffset(editor.document, startLine) - val endOffset = getLineEndOffset(editor.document, endLine) + val endOffset = getLineEndOffset(editor.document, max((startLine + patchString.split("\n").size - 1), endLine)) replaceString(editor.document, startOffset, endOffset, recommendation) removeHighlighter(editor) + try { + val caretPosition = + CaretPosition(offset = selectionRange.startOffset, line = getLineNumber(editor.document, selectionRange.startOffset)) + ReferenceLogController.addReferenceLog(recommendation, event.codeReference, editor, project, inlineChatStartPosition = caretPosition) + } catch (e: Exception) { + logger.warn { "error logging reference for inline chat: ${e.stackTraceToString()}" } + } } rejectAction = { val startOffset = getLineStartOffset(editor.document, startLine) @@ -624,6 +641,7 @@ class InlineChatController( val sessionInfo = sessionStorage.getSession(tabId, project) val mutex = Mutex() + val isReferenceAllowed = CodeWhispererSettings.getInstance().isIncludeCodeWithReference() var errorMessage = "" var prevMessage = "" @@ -645,11 +663,17 @@ class InlineChatController( .onEach { event: ChatMessage -> if (event.message?.isNotEmpty() == true && prevMessage != event.message) { mutex.withLock { + if (event.codeReference?.isNotEmpty() == true && !isReferenceAllowed) { + canPopupAbort.set(true) + undoChanges() + errorMessage = "Suggestion had code reference; removed per setting." + return@withLock + } try { if (selectionRange != null) { processChatDiff(selectedCode, event, editor, selectionRange!!) } else { - processNewCode(editor, selectedLineStart, unescape(event.message), prevMessage) + processNewCode(editor, selectedLineStart, event, prevMessage) } } catch (e: Exception) { canPopupAbort.set(true) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt index be3c8acdcc8..b6f421a90ac 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupFactory.kt @@ -70,8 +70,10 @@ class InlineChatPopupFactory( errorMessage = submitHandler(prompt, selectedCode, selectedLineStart, editor) } if (errorMessage.isNotEmpty()) { - setLabel(errorMessage) - revalidate() + withContext(EDT) { + setErrorMessage(errorMessage) + revalidate() + } } else { val acceptAction = { acceptHandler.invoke() @@ -81,6 +83,7 @@ class InlineChatPopupFactory( } withContext(EDT) { addCodeActionsPanel(acceptAction, rejectAction) + revalidate() } } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index f518ca84a51..4021e2dc796 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -88,6 +88,10 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() add(buttonsPanel, BorderLayout.EAST) } + private val emptyTextField = createTextField().apply { + isEnabled = false + } + override fun getPreferredSize(): Dimension = Dimension(popupWidth, popupHeight) private fun createTextField(): JTextField = JTextField().apply { @@ -141,9 +145,7 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() fun addCodeActionsPanel(acceptAction: () -> Unit, rejectAction: () -> Unit) { textLabel.text = message("amazonqInlineChat.popup.editCode") // this is a workaround somehow the textField will interfere with the enter handler - val emptyTextField = createTextField() emptyTextField.text = textField.text - emptyTextField.isEnabled = false inputPanel.remove(textField) inputPanel.add(emptyTextField, BorderLayout.CENTER) @@ -162,6 +164,22 @@ class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() revalidate() } + fun setErrorMessage(message: String) { + setLabel(message) + inputPanel.remove(emptyTextField) + textField.text = "" + textField.isEnabled = true + inputPanel.add(textField, BorderLayout.CENTER) + + buttonsPanel.remove(acceptButton) + buttonsPanel.remove(rejectButton) + buttonsPanel.add(submitButton, BorderLayout.WEST) + buttonsPanel.add(cancelButton, BorderLayout.EAST) + submitButton.isEnabled = false + cancelButton.isEnabled = true + revalidate() + } + fun setLabel(text: String) { textLabel.text = text textField.isEnabled = false diff --git a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties index 2d5696f7b8a..3d8ea0a76d4 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -23,13 +23,13 @@ action.aws.toolkit.jetbrains.core.services.cwc.commands.FixCodeAction.descriptio action.aws.toolkit.jetbrains.core.services.cwc.commands.FixCodeAction.text = Fix Code action.aws.toolkit.jetbrains.core.services.cwc.commands.GenerateUnitTestsAction.description = Generates unit tests for the selected code action.aws.toolkit.jetbrains.core.services.cwc.commands.GenerateUnitTestsAction.text = Generate Tests (Beta) -action.aws.toolkit.jetbrains.core.services.cwc.inline.openChat.text = Inline Chat action.aws.toolkit.jetbrains.core.services.cwc.commands.OptimizeCodeAction.description = Optimizes the selected code action.aws.toolkit.jetbrains.core.services.cwc.commands.OptimizeCodeAction.text = Optimize Code action.aws.toolkit.jetbrains.core.services.cwc.commands.RefactorCodeAction.description = Refactors the selected code action.aws.toolkit.jetbrains.core.services.cwc.commands.RefactorCodeAction.text = Refactor Code action.aws.toolkit.jetbrains.core.services.cwc.commands.SendToPromptAction.description = Sends selected code to chat action.aws.toolkit.jetbrains.core.services.cwc.commands.SendToPromptAction.text = Send to Prompt +action.aws.toolkit.jetbrains.core.services.cwc.inline.openChat.text = Inline Chat action.aws.toolkit.open.arn.browser.text=Open ARN in AWS Console action.aws.toolkit.open.telemetry.viewer.text=View AWS Telemetry action.aws.toolkit.s3.open.bucket.viewer.prefixed.text=View Bucket with Prefix... From 1170e9f568f3a525a961082d16efdf17a18ec03f Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Mon, 28 Oct 2024 13:09:21 -0700 Subject: [PATCH 36/39] telemetry change --- .../controller/chat/telemetry/TelemetryHelper.kt | 5 ++--- .../services/cwc/inline/InlineChatController.kt | 13 ++++--------- .../credentials/CodeWhispererClientAdaptor.kt | 9 +++------ .../codewhispererruntime/service-2.json | 3 +-- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt index b4b973b22bc..635da478306 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt @@ -155,13 +155,12 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: numSuggestionAddLines: Int?, numSuggestionDelChars: Int?, numSuggestionDelLines: Int?, - charactersAdded: Int?, - charactersRemoved: Int?, + programmingLanguage: String?, ) { CodeWhispererClientAdaptor.getInstance(project).sendInlineChatTelemetry( requestId, inputLength, numSelectedLines, codeIntent, userDecision, responseStartLatency, responseEndLatency, numSuggestionAddChars, numSuggestionAddLines, numSuggestionDelChars, numSuggestionDelLines, - charactersAdded, charactersRemoved + programmingLanguage ) } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index 57943a90587..bc5805099dc 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -113,8 +113,7 @@ class InlineChatController( var numSuggestionAddLines: Int? = null, var numSuggestionDelChars: Int? = null, var numSuggestionDelLines: Int? = null, - var charactersAdded: Int? = null, - var charactersRemoved: Int? = null, + var programmingLanguage: String? = null, ) private val popupSubmitHandler: suspend (String, String, Int, Editor) -> String = { @@ -141,10 +140,6 @@ class InlineChatController( private fun recordInlineChatTelemetry(decision: InlineChatUserDecision) { if (metrics == null) return metrics?.userDecision = decision - if (decision == InlineChatUserDecision.ACCEPT) { - metrics?.charactersAdded = metrics?.numSuggestionAddChars - metrics?.charactersRemoved = metrics?.numSuggestionDelChars - } if (metrics?.requestId?.isNotEmpty() == true) { telemetryHelper.recordInlineChatTelemetry( metrics?.requestId!!, @@ -158,8 +153,7 @@ class InlineChatController( metrics?.numSuggestionAddLines, metrics?.numSuggestionDelChars, metrics?.numSuggestionDelLines, - metrics?.charactersAdded, - metrics?.charactersRemoved + metrics?.programmingLanguage ) } metrics = null @@ -698,7 +692,8 @@ class InlineChatController( requestId?.let { metrics = InlineChatMetrics( requestId = it, inputLength = message.length, numSelectedLines = selectedCode.split("\n").size, - codeIntent = true, responseStartLatency = firstResponseLatency, responseEndLatency = lastResponseLatency + codeIntent = true, responseStartLatency = firstResponseLatency, responseEndLatency = lastResponseLatency, + programmingLanguage = fileContext.fileContext?.fileLanguage ) } if (finalMessage != null) { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt index d6b7afc21d7..665895011f1 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt @@ -200,8 +200,7 @@ interface CodeWhispererClientAdaptor : Disposable { numSuggestionAddLines: Int?, numSuggestionDelChars: Int?, numSuggestionDelLines: Int?, - charactersAdded: Int?, - charactersRemoved: Int?, + programmingLanguage: String?, ): SendTelemetryEventResponse companion object { @@ -617,8 +616,7 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW numSuggestionAddLines: Int?, numSuggestionDelChars: Int?, numSuggestionDelLines: Int?, - charactersAdded: Int?, - charactersRemoved: Int?, + programmingLanguage: String?, ): SendTelemetryEventResponse = bearerClient().sendTelemetryEvent { requestBuilder -> requestBuilder.telemetryEvent { telemetryEventBuilder -> telemetryEventBuilder.inlineChatEvent { @@ -633,8 +631,7 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW it.numSuggestionAddLines(numSuggestionAddLines) it.numSuggestionDelChars(numSuggestionDelChars) it.numSuggestionDelLines(numSuggestionDelLines) - it.charactersRemoved(charactersRemoved) - it.charactersAdded(charactersAdded) + if (programmingLanguage != null) it.programmingLanguage { langBuilder -> langBuilder.languageName(programmingLanguage) } it.timestamp(Instant.now()) } } diff --git a/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json b/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json index dfc91e438e7..029f2d170a1 100644 --- a/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json +++ b/plugins/core/sdk-codegen/codegen-resources/codewhispererruntime/service-2.json @@ -1113,8 +1113,7 @@ "userDecision": { "shape": "InlineChatUserDecision" }, "responseStartLatency": { "shape": "Double" }, "responseEndLatency": { "shape": "Double" }, - "charactersRemoved": { "shape": "PrimitiveInteger" }, - "charactersAdded": { "shape": "PrimitiveInteger"} + "programmingLanguage": { "shape": "ProgrammingLanguage" } } }, "InlineChatUserDecision": { From cfdc0be4f22cc9ad4e947a0c64288bb75a72eff3 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Mon, 28 Oct 2024 13:59:34 -0700 Subject: [PATCH 37/39] add change log --- .../feature-d2858ed1-62fc-461b-93b3-6edf82df2855.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changes/next-release/feature-d2858ed1-62fc-461b-93b3-6edf82df2855.json diff --git a/.changes/next-release/feature-d2858ed1-62fc-461b-93b3-6edf82df2855.json b/.changes/next-release/feature-d2858ed1-62fc-461b-93b3-6edf82df2855.json new file mode 100644 index 00000000000..ac770600cf9 --- /dev/null +++ b/.changes/next-release/feature-d2858ed1-62fc-461b-93b3-6edf82df2855.json @@ -0,0 +1,4 @@ +{ + "type" : "feature", + "description" : "Added inline chat support. Select some code and hit ⌘+I on Mac or Ctrl+I on Windows to start" +} \ No newline at end of file From f7c7e2c4c79292f937a84d1d670a4fd579600fd1 Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Mon, 28 Oct 2024 14:19:25 -0700 Subject: [PATCH 38/39] pr feedback --- .../jetbrains/services/cwc/inline/InlineChatController.kt | 8 ++++---- .../jetbrains/services/cwc/inline/InlineChatPopupPanel.kt | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt index bc5805099dc..42d40ed1079 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt @@ -305,10 +305,10 @@ class InlineChatController( recordInlineChatTelemetry(InlineChatUserDecision.DISMISS) return@runBlocking } - launch(EDT) { + withContext(EDT) { insertNewLineIfNeeded(insertLine, editor) insertString(editor, getLineStartOffset(editor.document, insertLine), stringToAdd + "\n") - }.join() + } insertLine += linesToAdd.size insertionLine.set(insertLine) acceptAction = { @@ -466,11 +466,11 @@ class InlineChatController( recordInlineChatTelemetry(InlineChatUserDecision.DISMISS) return@runBlocking } - launch(EDT) { + withContext(EDT) { removeSelection(editor) applyChunk(patchString, editor, startLine, endLine) processHighlights(diff, startLine, editor) - }.join() + } acceptAction = { val startOffset = getLineStartOffset(editor.document, startLine) val endOffset = getLineEndOffset(editor.document, max((startLine + patchString.split("\n").size - 1), endLine)) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt index 4021e2dc796..f909115084f 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatPopupPanel.kt @@ -31,8 +31,8 @@ import javax.swing.event.DocumentListener class InlineChatPopupPanel(private val parentDisposable: Disposable) : JPanel() { private var submitClickListener: (() -> Unit)? = null private val popupButtonFontSize = 14f - val popupWidth = 600 - val popupHeight = 90 + private val popupWidth = 600 + private val popupHeight = 90 private val popupButtonHeight = 30 private val popupButtonWidth = 80 private val popupInputHeight = 40 From 3e9781be3c234621681f01f50fbbf9efb49a94cd Mon Sep 17 00:00:00 2001 From: Zoe Lin Date: Mon, 28 Oct 2024 14:47:39 -0700 Subject: [PATCH 39/39] add read action to language extractor --- .../cwc/editor/context/file/util/LanguageExtractor.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt index e292fa2b351..726edc0212d 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/util/LanguageExtractor.kt @@ -3,9 +3,13 @@ package software.aws.toolkits.jetbrains.services.cwc.editor.context.file.util +import com.intellij.openapi.application.runReadAction import com.intellij.openapi.editor.Editor import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage class LanguageExtractor { - fun extractLanguageNameFromCurrentFile(editor: Editor): String = editor.virtualFile.programmingLanguage().languageId + fun extractLanguageNameFromCurrentFile(editor: Editor): String = + runReadAction { + editor.virtualFile.programmingLanguage().languageId + } }