diff --git a/gradle.properties b/gradle.properties index ece811a1a9..02109e81d1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,4 +24,4 @@ kotlin.stdlib.default.dependency=false nodeBinaries.commit=8755ae4c05fd476cd23f2972049111ba436c86d4 nodeBinaries.version=v20.12.2 cody.autocomplete.enableFormatting=true -cody.commit=a7ba3c987888439299edb6c215bb8b3ed4765d1d +cody.commit=ec9d08e976153166c448a3df696488d882c4581e diff --git a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt index 251f31590d..d42f1f49e5 100644 --- a/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt +++ b/src/integrationTest/kotlin/com/sourcegraph/cody/edit/DocumentCodeTest.kt @@ -12,6 +12,7 @@ import com.sourcegraph.cody.edit.widget.LensSpinner import com.sourcegraph.cody.edit.widget.LensWidgetGroup import com.sourcegraph.cody.util.CodyIntegrationTextFixture import com.sourcegraph.cody.util.CustomJunitClassRunner +import kotlin.test.Ignore import org.hamcrest.Matchers.startsWith import org.junit.Assert.assertThat import org.junit.Test @@ -19,6 +20,7 @@ import org.junit.runner.RunWith @RunWith(CustomJunitClassRunner::class) class DocumentCodeTest : CodyIntegrationTextFixture() { + @Ignore @Test fun testGetsWorkingGroupLens() { val codeLensGroup = runAndWaitForLenses(DocumentCodeAction.ID, EditCancelAction.ID) diff --git a/src/main/java/com/sourcegraph/cody/PromptPanel.kt b/src/main/java/com/sourcegraph/cody/PromptPanel.kt deleted file mode 100644 index 254b000079..0000000000 --- a/src/main/java/com/sourcegraph/cody/PromptPanel.kt +++ /dev/null @@ -1,320 +0,0 @@ -package com.sourcegraph.cody - -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CustomShortcutSet -import com.intellij.openapi.actionSystem.KeyboardShortcut -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.project.Project -import com.intellij.ui.components.JBList -import com.intellij.ui.components.JBScrollPane -import com.intellij.util.concurrency.annotations.RequiresEdt -import com.intellij.util.ui.JBUI -import com.intellij.util.ui.UIUtil -import com.sourcegraph.cody.agent.WebviewMessage -import com.sourcegraph.cody.agent.protocol.ContextItem -import com.sourcegraph.cody.agent.protocol.ContextItemFile -import com.sourcegraph.cody.chat.ChatPromptHistory -import com.sourcegraph.cody.chat.ChatSession -import com.sourcegraph.cody.chat.ui.SendButton -import com.sourcegraph.cody.ui.AutoGrowingTextArea -import com.sourcegraph.cody.ui.TextAreaHistoryManager -import com.sourcegraph.cody.vscode.CancellationToken -import com.sourcegraph.common.CodyBundle -import com.sourcegraph.common.ui.SimpleDumbAwareEDTAction -import java.awt.Dimension -import java.awt.event.ComponentAdapter -import java.awt.event.ComponentEvent -import java.awt.event.KeyEvent -import java.awt.event.MouseAdapter -import java.awt.event.MouseEvent -import javax.swing.DefaultListModel -import javax.swing.JLayeredPane -import javax.swing.KeyStroke -import javax.swing.border.EmptyBorder -import javax.swing.event.AncestorEvent -import javax.swing.event.AncestorListener - -class PromptPanel(project: Project, private val chatSession: ChatSession) : JLayeredPane() { - - /** View components */ - private val autoGrowingTextArea = AutoGrowingTextArea(5, 9, this) - private val scrollPane = autoGrowingTextArea.scrollPane - private val textArea = autoGrowingTextArea.textArea - private val sendButton = SendButton() - private var contextFilesListViewModel = DefaultListModel() - private val contextFilesListView = JBList(contextFilesListViewModel) - private val contextFilesContainer = JBScrollPane(contextFilesListView) - - /** Externally updated state */ - private val selectedContextItems: ArrayList = ArrayList() - - /** Related components */ - private val promptMessageHistory = - ChatPromptHistory(project, chatSession, CHAT_MESSAGE_HISTORY_CAPACITY) - - private val historyManager = TextAreaHistoryManager(textArea, promptMessageHistory) - - init { - /** Initialize view */ - textArea.emptyText.text = CodyBundle.getString("PromptPanel.ask-cody.message") - scrollPane.border = EmptyBorder(JBUI.emptyInsets()) - scrollPane.background = UIUtil.getPanelBackground() - - // Set initial bounds for the scrollPane (100x100) to ensure proper initialization; - // later adjusted dynamically based on component resizing in the component listener. - scrollPane.setBounds(0, 0, 100, 100) - add(scrollPane, DEFAULT_LAYER) - scrollPane.setBounds(0, 0, width, scrollPane.preferredSize.height) - - contextFilesListView.disableEmptyText() - add(contextFilesContainer, PALETTE_LAYER, 0) - - add(sendButton, PALETTE_LAYER, 0) - - preferredSize = Dimension(scrollPane.width, scrollPane.height) - - /** Add listeners */ - addAncestorListener( - object : AncestorListener { - override fun ancestorAdded(event: AncestorEvent?) { - textArea.requestFocusInWindow() - textArea.caretPosition = textArea.document.length - } - - override fun ancestorRemoved(event: AncestorEvent?) {} - - override fun ancestorMoved(event: AncestorEvent?) {} - }) - addComponentListener( - object : ComponentAdapter() { - override fun componentResized(e: ComponentEvent?) { - revalidate() - } - }) - - // Add user action listeners - sendButton.addActionListener { _ -> didSubmitChatMessage() } - textArea.addCaretListener { - refreshSendButton() - didUserInputChange() - } - - contextFilesListView.addMouseListener( - object : MouseAdapter() { - override fun mouseClicked(e: MouseEvent) { - contextFilesListView.selectedIndex = contextFilesListView.locationToIndex(e.getPoint()) - didSelectContextFile() - textArea.requestFocusInWindow() - } - }) - for (shortcut in listOf(ENTER, UP, DOWN, TAB)) { // key listeners - SimpleDumbAwareEDTAction { didUseShortcut(it, shortcut) } - .registerCustomShortcutSet(shortcut, textArea) - } - - revalidate() - } - - fun focus() = textArea.requestFocusInWindow() - - private fun didUseShortcut(anActionEvent: AnActionEvent, shortcut: CustomShortcutSet) { - if (contextFilesListView.model.size > 0) { - when (shortcut) { - UP -> setSelectedContextFileIndex(-1) - DOWN -> setSelectedContextFileIndex(1) - ENTER, - TAB -> didSelectContextFile() - } - return - } - when (shortcut) { - ENTER -> - if (sendButton.isEnabled) { - didSubmitChatMessage() - } - else -> { - val inputEvent = anActionEvent.inputEvent as? KeyEvent - val listener = textArea.keyListeners.firstOrNull() - if (inputEvent != null && listener != null) { - listener.keyPressed(inputEvent) - } - } - } - } - - /** View handlers */ - private fun didSubmitChatMessage() { - val cf = findContextFiles(selectedContextItems, textArea.text) - val text = textArea.text - - // Reset text - historyManager.addPrompt(text) - textArea.text = "" - selectedContextItems.clear() - - chatSession.sendMessage(text, cf) - } - - private fun didSelectContextFile() { - if (contextFilesListView.selectedIndex == -1) return - - val selected = contextFilesListView.model.getElementAt(contextFilesListView.selectedIndex) - this.selectedContextItems.add(selected.contextItem) - val cfDisplayPath = selected.contextItem.displayPath() - val expr = - findAtExpressions(textArea.text).find { it.endIndex == textArea.caretPosition } ?: return - - textArea.replaceRange("@${cfDisplayPath} ", expr.startIndex, expr.endIndex) - - setContextFilesSelector(listOf()) - revalidate() - } - - private fun didUserInputChange() { - val exp = findAtExpressions(textArea.text).find { it.endIndex == textArea.caretPosition } - if (exp == null) { - setContextFilesSelector(listOf()) - revalidate() - return - } - val expTrimmed = exp.value.removePrefix("...") - this.chatSession.sendWebviewMessage( - WebviewMessage(command = "getUserContext", submitType = "user", query = expTrimmed)) - } - - /** State updaters */ - private fun setSelectedContextFileIndex(increment: Int) { - var newSelectedIndex = - (contextFilesListView.selectedIndex + increment) % contextFilesListView.model.size - if (newSelectedIndex < 0) { - newSelectedIndex += contextFilesListView.model.size - } - contextFilesListView.selectedIndex = newSelectedIndex - revalidate() - } - - override fun revalidate() { - super.revalidate() - - // get the height of the context files list based on font height and number of context files - val contextFilesContainerHeight = - if (contextFilesListViewModel.isEmpty) 0 else contextFilesListView.preferredSize.height + 2 - if (contextFilesContainerHeight == 0) { - contextFilesContainer.isVisible = false - } else if (findAtExpressions(textArea.text).find { it.endIndex == textArea.caretPosition } != - null) { - // Check if the caret position is at the end of an @-expression - // This ensures that the context files container is only shown when the user is actively - // typing an @-expression, and not when the response arrives asynchronously after the - // @-expression has been removed or modified. - contextFilesContainer.size = Dimension(scrollPane.width, contextFilesContainerHeight) - contextFilesContainer.isVisible = true - } - - preferredSize = Dimension(scrollPane.width, scrollPane.height + contextFilesContainerHeight) - - sendButton.setBounds( - preferredSize.width - sendButton.preferredSize.width, - preferredSize.height - sendButton.preferredSize.height, - sendButton.preferredSize.width, - sendButton.preferredSize.height) - - scrollPane.setBounds(0, contextFilesContainerHeight, width, scrollPane.preferredSize.height) - } - - @RequiresEdt - private fun refreshSendButton() { - sendButton.isEnabled = - textArea.getText().isNotEmpty() && chatSession.getCancellationToken().isDone - } - - /** External prop setters */ - fun registerCancellationToken(cancellationToken: CancellationToken) { - cancellationToken.onFinished { - ApplicationManager.getApplication().invokeLater { refreshSendButton() } - } - } - - @RequiresEdt - fun setContextFilesSelector(newUserContextItems: List) { - val changed = contextFilesListViewModel.elements().toList() != newUserContextItems - if (changed) { - val newModel = DefaultListModel() - newModel.addAll(newUserContextItems.map { f -> DisplayedContextFile(f) }) - contextFilesListView.model = newModel - contextFilesListViewModel = newModel - - if (newUserContextItems.isNotEmpty()) { - contextFilesListView.selectedIndex = 0 - } else { - contextFilesListView.selectedIndex = -1 - } - revalidate() - } - } - - fun updateEmptyTextAfterFirstMessage() { - textArea.emptyText.text = CodyBundle.getString("PromptPanel.ask-cody.follow-up-message") - } - - companion object { - private const val CHAT_MESSAGE_HISTORY_CAPACITY = 100 - private val KEY_ENTER = KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), null) - private val KEY_UP = KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), null) - private val KEY_DOWN = KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), null) - private val KEY_TAB = KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), null) - - val ENTER = CustomShortcutSet(KEY_ENTER) - val UP = CustomShortcutSet(KEY_UP) - val DOWN = CustomShortcutSet(KEY_DOWN) - val TAB = CustomShortcutSet(KEY_TAB) - - private val atExpressionPattern = """(@(?:\\\s|\S)*)(?:\s|$)""".toRegex() - - fun findAtExpressions(text: String): List { - val matches = atExpressionPattern.findAll(text) - val expressions = ArrayList() - for (match in matches) { - val mainMatch = match.groups[0] ?: continue - val prevIndex = mainMatch.range.first - 1 - // filter out things like email addresses - if (prevIndex >= 0 && !text[prevIndex].isWhitespace()) continue - - val subMatch = match.groups[1] - if (subMatch != null) { - val value = subMatch.value.substring(1).replace("\\ ", " ") - expressions.add( - AtExpression(subMatch.range.first, subMatch.range.last + 1, subMatch.value, value)) - } - } - return expressions - } - - private fun findContextFiles(contextItems: List, text: String): List { - val atExpressions = findAtExpressions(text) - return contextItems.filter { f -> atExpressions.any { it.value == f.displayPath() } } - } - } -} - -data class DisplayedContextFile(val contextItem: ContextItem) { - override fun toString(): String { - val contextItemFile = contextItem as? ContextItemFile - val isIgnored = contextItemFile?.isIgnored == true - val isTooLarge = contextItemFile?.title == "large-file" || contextItemFile?.isTooLarge == true - val warnIfNeeded = - when { - isIgnored -> " - ⚠ Ignored by an admin setting" - isTooLarge -> " - ⚠ File too large" - else -> "" - } - return "${contextItem.displayPath()}$warnIfNeeded" - } -} - -data class AtExpression( - val startIndex: Int, - val endIndex: Int, - val rawValue: String, - val value: String -) diff --git a/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java b/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java deleted file mode 100644 index 70da43798d..0000000000 --- a/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java +++ /dev/null @@ -1,275 +0,0 @@ -package com.sourcegraph.cody.agent; - -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.diagnostic.Logger; -import com.sourcegraph.cody.agent.protocol.*; -import com.sourcegraph.cody.agent.protocol_generated.DisplayCodeLensParams; -import com.sourcegraph.cody.agent.protocol_generated.EditTask; -import com.sourcegraph.cody.ui.NativeWebviewProvider; -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; -import java.util.function.Function; -import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; -import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Implementation of the client part of the Cody agent protocol. This class dispatches the requests - * and notifications sent by the agent. - */ -@SuppressWarnings("unused") -public class CodyAgentClient { - private static final Logger logger = Logger.getInstance(CodyAgentClient.class); - - @NotNull NativeWebviewProvider webview; - - CodyAgentClient(@NotNull NativeWebviewProvider webviewProvider) { - this.webview = webviewProvider; - } - - // TODO: Remove this once we stop sniffing postMessage. - // Callback that is invoked when the agent sends a "chat/updateMessageInProgress" notification. - @Nullable Consumer onNewMessage; - - // Callback that is invoked when the agent sends a "setConfigFeatures" message. - @Nullable ConfigFeaturesObserver onSetConfigFeatures; - - // Callback that is invoked on webview messages which aren't handled by onNewMessage or - // onSetConfigFeatures - @Nullable Consumer onReceivedWebviewMessageTODODeleteThis; - - // Callback for the "editTask/didUpdate" notification from the agent. - @Nullable Consumer onEditTaskDidUpdate; - - // Callback for the "editTask/didDelete" notification from the agent. - @Nullable Consumer onEditTaskDidDelete; - - // Callback for the "editTask/codeLensesDisplay" notification from the agent. - @Nullable Consumer onCodeLensesDisplay; - - // Callback for the "textDocument/edit" request from the agent. - @Nullable Function onTextDocumentEdit; - - // Callback for the "textDocument/show" request from the agent. - @Nullable Function onTextDocumentShow; - - // Callback for the "textDocument/openUntitledDocument" request from the agent. - @Nullable Function onOpenUntitledDocument; - - // Callback for the "workspace/edit" request from the agent. - @Nullable Function onWorkspaceEdit; - - @Nullable Consumer onDebugMessage; - - @JsonNotification("editTask/didUpdate") - public CompletableFuture editTaskDidUpdate(EditTask params) { - return acceptOnEventThread("editTask/didUpdate", onEditTaskDidUpdate, params); - } - - @JsonNotification("editTask/didDelete") - public CompletableFuture editTaskDidDelete(EditTask params) { - return acceptOnEventThread("editTask/didDelete", onEditTaskDidDelete, params); - } - - @JsonNotification("codeLenses/display") - public void codeLensesDisplay(DisplayCodeLensParams params) { - acceptOnEventThread("codeLenses/display", onCodeLensesDisplay, params); - } - - @Nullable Function onOpenExternal; - - @JsonRequest("env/openExternal") - public CompletableFuture ignoreTest(@NotNull OpenExternalParams params) { - return acceptOnEventThread("env/openExternal", onOpenExternal, params); - } - - @Nullable Consumer onRemoteRepoDidChange; - - @JsonNotification("remoteRepo/didChange") - public void remoteRepoDidChange() { - if (onRemoteRepoDidChange != null) { - onRemoteRepoDidChange.accept(null); - } - } - - @Nullable Consumer onRemoteRepoDidChangeState; - - @JsonNotification("remoteRepo/didChangeState") - public void remoteRepoDidChangeState(RemoteRepoFetchState state) { - if (onRemoteRepoDidChangeState != null) { - onRemoteRepoDidChangeState.accept(state); - } - } - - @Nullable Consumer onIgnoreDidChange; - - @JsonNotification("ignore/didChange") - public void ignoreDidChange() { - if (onIgnoreDidChange != null) { - onIgnoreDidChange.accept(null); - } - } - - @JsonRequest("textDocument/edit") - public CompletableFuture textDocumentEdit(TextDocumentEditParams params) { - return acceptOnEventThread("textDocument/edit", onTextDocumentEdit, params); - } - - @JsonRequest("textDocument/show") - public CompletableFuture textDocumentShow(TextDocumentShowParams params) { - return acceptOnEventThread("textDocument/show", onTextDocumentShow, params); - } - - @JsonRequest("textDocument/openUntitledDocument") - public CompletableFuture openUntitledDocument(UntitledTextDocument params) { - if (onOpenUntitledDocument == null) { - return CompletableFuture.failedFuture( - new Exception("No callback registered for textDocument/openUntitledDocument")); - } else { - return CompletableFuture.completedFuture(onOpenUntitledDocument.apply(params)); - } - } - - @JsonRequest("workspace/edit") - public CompletableFuture workspaceEdit(WorkspaceEditParams params) { - return acceptOnEventThread("workspace/edit", onWorkspaceEdit, params); - } - - /** - * Helper to run client request/notification handlers on the IntelliJ event thread. Use this - * helper for handlers that require access to the IntelliJ editor, for example to read the text - * contents of the open editor. - */ - private @NotNull CompletableFuture acceptOnEventThread( - String name, @Nullable Function callback, T params) { - CompletableFuture result = new CompletableFuture<>(); - ApplicationManager.getApplication() - .invokeLater( - () -> { - try { - if (callback != null) { - result.complete(callback.apply(params)); - } else { - result.completeExceptionally(new Exception("No callback registered for " + name)); - } - } catch (Exception e) { - result.completeExceptionally(e); - } - }); - return result; - } - - private @NotNull CompletableFuture acceptOnEventThread( - String name, @Nullable Consumer callback, T params) { - Function fun = - callback == null - ? null - : t -> { - callback.accept(params); - return null; - }; - return acceptOnEventThread(name, fun, params); - } - - // TODO: Delete this - // Webviews - @JsonRequest("webview/create") - public CompletableFuture webviewCreate(WebviewCreateParams params) { - logger.error("webview/create This request should not happen if you are using chat/new."); - return CompletableFuture.completedFuture(null); - } - - // ============= - // Notifications - // ============= - - @JsonNotification("debug/message") - public void debugMessage(@NotNull DebugMessage msg) { - logger.warn(String.format("%s: %s", msg.getChannel(), msg.getMessage())); - if (onDebugMessage != null) { - onDebugMessage.accept(msg); - } - } - - // ================================================ - // Webviews, forwarded to the NativeWebviewProvider - // ================================================ - - @JsonNotification("webview/createWebviewPanel") - public void webviewCreateWebviewPanel(@NotNull WebviewCreateWebviewPanelParams params) { - this.webview.createPanel(params); - } - - @JsonNotification("webview/postMessageStringEncoded") - public void webviewPostMessageStringEncoded( - @NotNull WebviewPostMessageStringEncodedParams params) { - this.webview.receivedPostMessage(params); - } - - @JsonNotification("webview/registerWebviewViewProvider") - public void webviewRegisterWebviewViewProvider( - @NotNull WebviewRegisterWebviewViewProviderParams params) { - this.webview.registerViewProvider(params); - } - - @JsonNotification("webview/setHtml") - public void webviewTitle(@NotNull WebviewSetHtmlParams params) { - this.webview.setHtml(params); - } - - @JsonNotification("webview/setIconPath") - public void webviewSetIconPath(@NotNull WebviewSetIconPathParams params) { - // TODO: Implement this. - System.out.println("TODO, implement webview/setIconPath"); - } - - @JsonNotification("webview/setOptions") - public void webviewTitle(@NotNull WebviewSetOptionsParams params) { - this.webview.setOptions(params); - } - - @JsonNotification("webview/setTitle") - public void webviewTitle(@NotNull WebviewSetTitleParams params) { - this.webview.setTitle(params); - } - - @JsonNotification("webview/reveal") - public void webviewReveal(@NotNull WebviewRevealParams params) { - // TODO: Implement this. - System.out.println("TODO, implement webview/reveal"); - } - - @JsonNotification("webview/dispose") - public void webviewDispose(@NotNull WebviewDisposeParams params) { - // TODO: Implement this. - System.out.println("TODO, implement webview/dispose"); - } - - // TODO: Remove this - @JsonNotification("webview/postMessage") - public void webviewPostMessage(@NotNull WebviewPostMessageParams params) { - ExtensionMessage extensionMessage = params.getMessage(); - - if (onNewMessage != null - && extensionMessage.getType().equals(ExtensionMessage.Type.TRANSCRIPT)) { - ApplicationManager.getApplication().invokeLater(() -> onNewMessage.accept(params)); - return; - } - - if (onSetConfigFeatures != null - && extensionMessage.getType().equals(ExtensionMessage.Type.SET_CONFIG_FEATURES)) { - ApplicationManager.getApplication() - .invokeLater(() -> onSetConfigFeatures.update(extensionMessage.getConfigFeatures())); - return; - } - - if (onReceivedWebviewMessageTODODeleteThis != null) { - ApplicationManager.getApplication() - .invokeLater(() -> onReceivedWebviewMessageTODODeleteThis.accept(params)); - return; - } - - logger.debug(String.format("webview/postMessage %s: %s", params.getId(), params.getMessage())); - } -} diff --git a/src/main/java/com/sourcegraph/cody/attribution/AttributionListener.kt b/src/main/java/com/sourcegraph/cody/attribution/AttributionListener.kt deleted file mode 100644 index 128fc9fbd0..0000000000 --- a/src/main/java/com/sourcegraph/cody/attribution/AttributionListener.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.sourcegraph.cody.attribution - -import com.intellij.openapi.application.ApplicationManager -import com.sourcegraph.cody.agent.protocol.AttributionSearchResponse - -/** - * [AttributionListener] responds to attribution search state changes. - * - * The interface does not convey any contract about execution thread. The caller and callee should - * make sure of proper execution. - */ -interface AttributionListener { - /** Notifies the listener that attribution search has started. */ - fun onAttributionSearchStart() - - /** Notifies the listener of the result of attribution search. */ - fun updateAttribution(attribution: AttributionSearchResponse) - - /** - * Wraps given [AttributionListener] so that all notifications are delivered asynchronously on UI - * thread. - */ - class UiThreadDecorator(private val delegate: AttributionListener) : AttributionListener { - override fun onAttributionSearchStart() { - ApplicationManager.getApplication().invokeLater { delegate.onAttributionSearchStart() } - } - - override fun updateAttribution(attribution: AttributionSearchResponse) { - ApplicationManager.getApplication().invokeLater { delegate.updateAttribution(attribution) } - } - } -} diff --git a/src/main/java/com/sourcegraph/cody/attribution/AttributionSearchCommand.kt b/src/main/java/com/sourcegraph/cody/attribution/AttributionSearchCommand.kt deleted file mode 100644 index 6d9f978730..0000000000 --- a/src/main/java/com/sourcegraph/cody/attribution/AttributionSearchCommand.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.sourcegraph.cody.attribution - -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.project.Project -import com.sourcegraph.cody.agent.CodyAgentService -import com.sourcegraph.cody.agent.CurrentConfigFeatures -import com.sourcegraph.cody.agent.protocol.AttributionSearchParams -import com.sourcegraph.cody.agent.protocol.AttributionSearchResponse -import com.sourcegraph.cody.chat.ConnectionId -import com.sourcegraph.cody.chat.ui.CodeEditorPart -import java.util.* -import java.util.function.BiFunction - -/** - * [AttributionSearchCommand] performs attribution search on a code snippet, and then notifies of - * the result. - */ -class AttributionSearchCommand(private val project: Project) { - - /** - * [onSnippetFinished] invoked when assistant finished writing a code snippet in a chat message, - * and triggers attribution search (if enabled). Once attribution returns, the - * [CodeEditorPart.attributionListener] is updated. - */ - fun onSnippetFinished( - snippet: String, - connectionId: ConnectionId, - listener: AttributionListener - ) { - if (attributionEnabled()) { - CodyAgentService.withAgent(project) { agent -> - ApplicationManager.getApplication().invokeLater { listener.onAttributionSearchStart() } - val params = AttributionSearchParams(id = connectionId, snippet = snippet) - agent.server.attributionSearch(params).handle(updateEditor(listener)) - } - } - } - - /** - * [updateEditor] returns a future handler for attribution search operation, which notifies the - * listener. - */ - private fun updateEditor(listener: AttributionListener) = - BiFunction { response, throwable -> - listener.updateAttribution( - response - ?: AttributionSearchResponse( - error = throwable?.message ?: "Error searching for attribution.", - repoNames = listOf(), - limitHit = false, - )) - } - - private fun attributionEnabled(): Boolean = - project.getService(CurrentConfigFeatures::class.java).get().attribution -} diff --git a/src/main/java/com/sourcegraph/cody/chat/CodeEditorFactory.java b/src/main/java/com/sourcegraph/cody/chat/CodeEditorFactory.java index c645dcaa4c..d1bb82b8f3 100644 --- a/src/main/java/com/sourcegraph/cody/chat/CodeEditorFactory.java +++ b/src/main/java/com/sourcegraph/cody/chat/CodeEditorFactory.java @@ -1,243 +1,8 @@ package com.sourcegraph.cody.chat; -import com.intellij.openapi.command.WriteCommandAction; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.*; -import com.intellij.openapi.editor.event.*; -import com.intellij.openapi.editor.ex.EditorEx; -import com.intellij.openapi.fileEditor.FileEditorManager; -import com.intellij.openapi.ide.CopyPasteManager; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.wm.IdeFocusManager; -import com.intellij.util.ui.JBInsets; -import com.sourcegraph.cody.chat.ui.CodeEditorButtons; -import com.sourcegraph.cody.chat.ui.CodeEditorPart; -import com.sourcegraph.cody.telemetry.TelemetryV2; -import com.sourcegraph.cody.ui.AttributionButtonController; -import com.sourcegraph.cody.ui.TransparentButton; -import com.sourcegraph.telemetry.GraphQlLogger; -import java.awt.*; -import java.awt.datatransfer.Clipboard; -import java.awt.datatransfer.DataFlavor; -import java.awt.datatransfer.StringSelection; -import java.awt.event.ActionListener; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; -import java.time.Duration; -import java.util.Objects; -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - public class CodeEditorFactory { - private static final Logger logger = Logger.getInstance(CodeEditorFactory.class); - - private final @NotNull Project project; - private final @NotNull JPanel parentPanel; - private final int rightMargin; public static final int spaceBetweenButtons = 5; public static volatile String lastCopiedText = null; - - public CodeEditorFactory(@NotNull Project project, @NotNull JPanel parentPanel, int rightMargin) { - this.project = project; - this.parentPanel = parentPanel; - this.rightMargin = rightMargin; - } - - public CodeEditorPart createCodeEditor(@NotNull String code, @Nullable String language) { - Document codeDocument = EditorFactory.getInstance().createDocument(code); - EditorEx editor = (EditorEx) EditorFactory.getInstance().createViewer(codeDocument); - fillEditorSettings(editor.getSettings()); - editor.setVerticalScrollbarVisible(false); - editor.getGutterComponentEx().setPaintBackground(false); - JComponent editorComponent = editor.getComponent(); - Dimension editorPreferredSize = editorComponent.getPreferredSize(); - - TransparentButton copyButton = new TransparentButton("Copy"); - copyButton.setToolTipText("Copy text"); - copyButton.addActionListener(copyCodeListener(editor, copyButton)); - - TransparentButton insertAtCursorButton = new TransparentButton("Insert at Cursor"); - insertAtCursorButton.setToolTipText("Insert text at current cursor position"); - insertAtCursorButton.addActionListener(insertAtCursorActionListener(editor)); - - AttributionButtonController attributionButtonController = - AttributionButtonController.Companion.setup(project); - - Dimension copyButtonPreferredSize = copyButton.getPreferredSize(); - int halfOfButtonHeight = copyButtonPreferredSize.height / 2; - JLayeredPane layeredEditorPane = new JLayeredPane(); - layeredEditorPane.setOpaque(false); - // add right margin to show gradient from the parent on the right side - layeredEditorPane.setBorder(new EmptyBorder(JBInsets.create(new Insets(0, rightMargin, 0, 0)))); - // layered pane should have width of the editor + gradient width and height of the editor + half - // of the button height - // to show button on the top border of the editor - layeredEditorPane.setPreferredSize( - new Dimension( - editorPreferredSize.width + rightMargin, - editorPreferredSize.height + halfOfButtonHeight)); - // add empty space to editor to show button on top border - editorComponent.setBorder( - BorderFactory.createEmptyBorder(halfOfButtonHeight, rightMargin, 0, 0)); - editorComponent.setOpaque(false); - // place the editor in the layered pane - editorComponent.setBounds( - rightMargin, - 0, - parentPanel.getSize().width - rightMargin, - editorPreferredSize.height + halfOfButtonHeight); - layeredEditorPane.add(editorComponent, JLayeredPane.DEFAULT_LAYER); - - // Rendering order of buttons is right-to-left: - JButton[] buttons = - new JButton[] {attributionButtonController.getButton(), copyButton, insertAtCursorButton}; - CodeEditorButtons codeEditorButtons = new CodeEditorButtons(buttons); - codeEditorButtons.addButtons(layeredEditorPane, editorComponent.getWidth()); - attributionButtonController.onUpdate( - () -> { - // Resize buttons on text update. - codeEditorButtons.updateBounds(editorComponent.getWidth()); - }); - - // resize the editor and move the copy button when the parent panel is resized - layeredEditorPane.addComponentListener( - new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent e) { - Dimension editorPreferredSize = editorComponent.getPreferredSize(); - editorComponent.setBounds( - rightMargin, - 0, - parentPanel.getSize().width - rightMargin, - editorPreferredSize.height + halfOfButtonHeight); - codeEditorButtons.updateBounds(editorComponent.getWidth()); - } - }); - editor - .getDocument() - .addDocumentListener( - new DocumentListener() { - @Override - public void documentChanged(@NotNull DocumentEvent event) { - Dimension editorPreferredSize = editorComponent.getPreferredSize(); - layeredEditorPane.setPreferredSize( - new Dimension( - editorPreferredSize.width + rightMargin, - editorPreferredSize.height + halfOfButtonHeight)); - editorComponent.setBounds( - rightMargin, - 0, - parentPanel.getSize().width - rightMargin, - editorPreferredSize.height + halfOfButtonHeight); - } - }); - - EditorMouseMotionListener editorMouseMotionListener = - new EditorMouseMotionListener() { - @Override - public void mouseMoved(@NotNull EditorMouseEvent e) { - codeEditorButtons.setVisible(true); - } - }; - - EditorMouseListener editorMouseListener = - new EditorMouseListener() { - @Override - public void mouseExited(@NotNull EditorMouseEvent event) { - codeEditorButtons.setVisible(false); - } - }; - - CopyPasteManager.getInstance() - .addContentChangedListener( - (oldTransferable, newTransferable) -> { - try { - String copiedString = - newTransferable.getTransferData(DataFlavor.stringFlavor).toString(); - String selectedText = editor.getSelectionModel().getSelectedText(); - // Unfortunately there is no easy way to check if origin of the copied text, so we - // are checking if copied text is matching a text selected in the component - if (Objects.equals(selectedText, copiedString)) { - lastCopiedText = selectedText; - TelemetryV2.Companion.sendCodeGenerationEvent( - project, "keyDown.Copy", "clicked", lastCopiedText); - } - } catch (Exception e) { - logger.warn("Unable to process copied text", e); - } - }, - project); - - editor.addEditorMouseMotionListener(editorMouseMotionListener); - editor.addEditorMouseListener(editorMouseListener); - CodeEditorPart codeEditorPart = - new CodeEditorPart(layeredEditorPane, editor, attributionButtonController); - codeEditorPart.recognizeLanguage(language); - return codeEditorPart; - } - - @NotNull - private ActionListener copyCodeListener(EditorEx editor, JButton copyButton) { - return e -> { - String text = editor.getDocument().getText(); - StringSelection stringSelection = new StringSelection(text); - Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - clipboard.setContents(stringSelection, null); - copyButton.setText("Copied"); - Timer timer = - new Timer((int) Duration.ofSeconds(2).toMillis(), it -> copyButton.setText("Copy")); - timer.setRepeats(false); - timer.start(); - - lastCopiedText = text; - GraphQlLogger.logCodeGenerationEvent(project, "copyButton", "clicked", text); - TelemetryV2.Companion.sendCodeGenerationEvent(project, "copyButton", "clicked", text); - }; - } - - @NotNull - private ActionListener insertAtCursorActionListener(EditorEx editor) { - return e -> { - FileEditorManager fileEditorManager = FileEditorManager.getInstance(project); - Editor mainEditor = fileEditorManager.getSelectedTextEditor(); - if (mainEditor != null) { - CaretModel caretModel = mainEditor.getCaretModel(); - int caretPos = caretModel.getOffset(); - // Paste the text at the caret position - Document document = mainEditor.getDocument(); - String text = editor.getDocument().getText(); - WriteCommandAction.runWriteCommandAction( - project, - () -> { - try { - document.insertString(caretPos, text); - IdeFocusManager.getInstance(project) - .requestFocus(mainEditor.getContentComponent(), true) - .doWhenDone(() -> caretModel.moveToOffset(caretPos + text.length())); - } catch (Exception ex) { - logger.warn("Failed to insert text at cursor", ex); - } - }); - - GraphQlLogger.logCodeGenerationEvent(project, "insertButton", "clicked", text); - TelemetryV2.Companion.sendCodeGenerationEvent(project, "insertButton", "clicked", text); - } - }; - } - - private static void fillEditorSettings(@NotNull EditorSettings editorSettings) { - editorSettings.setAdditionalColumnsCount(0); - editorSettings.setAdditionalLinesCount(0); - editorSettings.setGutterIconsShown(false); - editorSettings.setWhitespacesShown(false); - editorSettings.setLineMarkerAreaShown(false); - editorSettings.setIndentGuidesShown(false); - editorSettings.setLineNumbersShown(false); - editorSettings.setUseSoftWraps(false); - editorSettings.setCaretRowShown(false); - } } diff --git a/src/main/java/com/sourcegraph/cody/chat/MessageContentCreatorFromMarkdownNodes.java b/src/main/java/com/sourcegraph/cody/chat/MessageContentCreatorFromMarkdownNodes.java deleted file mode 100644 index 69a4a9455c..0000000000 --- a/src/main/java/com/sourcegraph/cody/chat/MessageContentCreatorFromMarkdownNodes.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.sourcegraph.cody.chat; - -import com.intellij.util.concurrency.annotations.RequiresEdt; -import com.sourcegraph.cody.chat.ui.SingleMessagePanel; -import org.commonmark.node.*; -import org.commonmark.node.Image; -import org.commonmark.renderer.html.HtmlRenderer; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * This class is to be used with a Markdown document like this: Node document = - * parser.parse(message.getDisplayText()); document.accept(messageContentCreator); It converts a - * single chat message to a JPanel and other Swing components inside it. - */ -public class MessageContentCreatorFromMarkdownNodes extends AbstractVisitor { - private final HtmlRenderer htmlRenderer; - private final SingleMessagePanel messagePanel; - private StringBuilder htmlContent = new StringBuilder(); - - public MessageContentCreatorFromMarkdownNodes( - @NotNull SingleMessagePanel messagePanel, @NotNull HtmlRenderer htmlRenderer) { - this.messagePanel = messagePanel; - this.htmlRenderer = htmlRenderer; - } - - @Override - public void visit(@NotNull Paragraph paragraph) { - addContentOfNodeAsHtml(htmlRenderer.render(paragraph)); - } - - @Override - public void visit(@NotNull Code code) { - addContentOfNodeAsHtml(htmlRenderer.render(code)); - super.visit(code); - } - - @Override - public void visit(@NotNull IndentedCodeBlock indentedCodeBlock) { - insertCodeEditor(indentedCodeBlock.getLiteral(), ""); - super.visit(indentedCodeBlock); - } - - @Override - public void visit(@NotNull Text text) { - addContentOfNodeAsHtml(htmlRenderer.render(text)); - super.visit(text); - } - - @Override - public void visit(@NotNull BlockQuote blockQuote) { - addContentOfNodeAsHtml(htmlRenderer.render(blockQuote)); - super.visit(blockQuote); - } - - @Override - public void visit(@NotNull BulletList bulletList) { - addContentOfNodeAsHtml(htmlRenderer.render(bulletList)); - } - - @Override - public void visit(@NotNull OrderedList orderedList) { - addContentOfNodeAsHtml(htmlRenderer.render(orderedList)); - } - - @Override - public void visit(@NotNull Emphasis emphasis) { - addContentOfNodeAsHtml(htmlRenderer.render(emphasis)); - super.visit(emphasis); - } - - @Override - public void visit(@NotNull FencedCodeBlock fencedCodeBlock) { - insertCodeEditor(fencedCodeBlock.getLiteral(), fencedCodeBlock.getInfo()); - super.visit(fencedCodeBlock); - } - - @RequiresEdt - private void insertCodeEditor(@NotNull String codeContent, @Nullable String languageName) { - messagePanel.addOrUpdateCode(codeContent, languageName); - htmlContent = new StringBuilder(); - } - - @Override - public void visit(@NotNull HardLineBreak hardLineBreak) { - addContentOfNodeAsHtml(htmlRenderer.render(hardLineBreak)); - super.visit(hardLineBreak); - } - - @Override - public void visit(@NotNull Heading heading) { - addContentOfNodeAsHtml(htmlRenderer.render(heading)); - super.visit(heading); - } - - @Override - public void visit(@NotNull ThematicBreak thematicBreak) { - addContentOfNodeAsHtml(htmlRenderer.render(thematicBreak)); - super.visit(thematicBreak); - } - - @Override - public void visit(@NotNull HtmlInline htmlInline) { - addContentOfNodeAsHtml(htmlRenderer.render(htmlInline)); - super.visit(htmlInline); - } - - @Override - public void visit(@NotNull HtmlBlock htmlBlock) { - addContentOfNodeAsHtml(htmlRenderer.render(htmlBlock)); - super.visit(htmlBlock); - } - - @Override - public void visit(@NotNull Image image) { - addContentOfNodeAsHtml(htmlRenderer.render(image)); - super.visit(image); - } - - @Override - public void visit(@NotNull Link link) { - addContentOfNodeAsHtml(htmlRenderer.render(link)); - } - - @Override - public void visit(@NotNull ListItem listItem) { - addContentOfNodeAsHtml(htmlRenderer.render(listItem)); - super.visit(listItem); - } - - @Override - public void visit(@NotNull SoftLineBreak softLineBreak) { - addContentOfNodeAsHtml(htmlRenderer.render(softLineBreak)); - super.visit(softLineBreak); - } - - @Override - public void visit(@NotNull StrongEmphasis strongEmphasis) { - addContentOfNodeAsHtml(htmlRenderer.render(strongEmphasis)); - super.visit(strongEmphasis); - } - - @Override - public void visit(@NotNull LinkReferenceDefinition linkReferenceDefinition) { - addContentOfNodeAsHtml(htmlRenderer.render(linkReferenceDefinition)); - super.visit(linkReferenceDefinition); - } - - @Override - public void visit(@NotNull CustomBlock customBlock) { - addContentOfNodeAsHtml(htmlRenderer.render(customBlock)); - } - - private void addContentOfNodeAsHtml(@Nullable String renderedHtml) { - htmlContent.append(renderedHtml); - messagePanel.addOrUpdateText(htmlContent.toString()); - } -} diff --git a/src/main/java/com/sourcegraph/cody/ui/ComponentWithButton.java b/src/main/java/com/sourcegraph/cody/ui/ComponentWithButton.java deleted file mode 100644 index e5d4e804ff..0000000000 --- a/src/main/java/com/sourcegraph/cody/ui/ComponentWithButton.java +++ /dev/null @@ -1,193 +0,0 @@ -package com.sourcegraph.cody.ui; - -import static com.intellij.openapi.actionSystem.PlatformDataKeys.UI_DISPOSABLE; - -import com.intellij.ide.DataManager; -import com.intellij.openapi.Disposable; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.ui.FixedSizeButton; -import com.intellij.openapi.util.Disposer; -import com.intellij.openapi.util.IconLoader; -import com.intellij.openapi.util.SystemInfo; -import com.intellij.openapi.wm.IdeFocusManager; -import com.intellij.util.ui.StartupUiUtil; -import com.intellij.util.ui.update.Activatable; -import com.intellij.util.ui.update.UiNotifyConnector; -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.event.ActionListener; -import java.awt.event.KeyEvent; -import java.lang.ref.WeakReference; -import javax.swing.Icon; -import javax.swing.JComponent; -import javax.swing.JPanel; -import javax.swing.KeyStroke; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * This is a modification of a component from the IntelliJ Platform. We have removed some - * unnecessary code that couldn't be overridden - * - * @see com.intellij.openapi.ui.ComponentWithBrowseButton - */ -public class ComponentWithButton extends JPanel implements Disposable { - @NotNull private final Comp component; - @Nullable private final FixedSizeButton button; - protected boolean componentDisabledOverride = false; - - public ComponentWithButton(@NotNull Comp component) { - // Mac and Darcula have no horizontal gap, while other themes have a 2px gap. - super(new BorderLayout(SystemInfo.isMac || StartupUiUtil.isUnderDarcula() ? 0 : 2, 0)); - - // Add the component to the panel. - this.component = component; - // Required! Otherwise, the JPanel will occasionally gain focus instead of the component. - setFocusable(false); - add(this.component, BorderLayout.CENTER); - - // Create a button with a fixed size and add it to the panel. - button = new FixedSizeButton(this.component); - if (isBackgroundSet()) { - button.setBackground(getBackground()); - } - add(button, BorderLayout.EAST); - - new LazyDisposable(this); - } - - public void setIconTooltip(@NotNull String tooltip) { - if (button != null) { - button.setToolTipText(tooltip); - } - } - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - if (button != null) { - button.setEnabled(enabled); - } - component.setEnabled(enabled && !componentDisabledOverride); - } - - public void setComponentDisabledOverride(boolean disabled) { - componentDisabledOverride = disabled; - if (button != null) { - component.setEnabled(button.isEnabled() && !disabled); - } - } - - public void setButtonIcon(@NotNull Icon icon) { - if (button != null) { - button.setIcon(icon); - button.setDisabledIcon(IconLoader.getDisabledIcon(icon)); - } - } - - @Override - public void setBackground(Color color) { - super.setBackground(color); - if (button != null) { - button.setBackground(color); - } - } - - /** Adds specified {@code listener} to the button. */ - public void addButtonActionListener(ActionListener listener) { - if (button != null) { - button.addActionListener(listener); - } - } - - @Override - public void dispose() { - if (button != null) { - ActionListener[] listeners = button.getActionListeners(); - for (ActionListener listener : listeners) { - button.removeActionListener(listener); - } - } - } - - @Override - public final void requestFocus() { - IdeFocusManager.getGlobalInstance() - .doWhenFocusSettlesDown( - () -> IdeFocusManager.getGlobalInstance().requestFocus(component, true)); - } - - @SuppressWarnings("deprecation") - @Override - public final void setNextFocusableComponent(Component aComponent) { - super.setNextFocusableComponent(aComponent); - component.setNextFocusableComponent(aComponent); - } - - private KeyEvent currentEvent = null; - - /** - * This method is overridden to dispatch the event to the component. This is necessary because - * otherwise the event is dispatched to the parent component, which is the panel, and the event is - * not dispatched to the component. - * - * @param ks The KeyStroke queried - * @param e The KeyEvent forwarded to the focused component - * @param condition one of the following values: - *
    - *
  • JComponent.WHEN_FOCUSED - *
  • JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT - *
  • JComponent.WHEN_IN_FOCUSED_WINDOW - *
- * - * @param pressed true if the key is pressed - * @return true if there was a binding to the event, false otherwise. - */ - @Override - protected final boolean processKeyBinding( - KeyStroke ks, KeyEvent e, int condition, boolean pressed) { - if (condition == WHEN_FOCUSED && currentEvent != e) { - try { - currentEvent = e; - component.dispatchEvent(e); - } finally { - currentEvent = null; - } - } - if (e.isConsumed()) { - return true; - } - return super.processKeyBinding(ks, e, condition, pressed); - } - - /** - * We need to register this component in the parent disposable. But we can't do it in the - * constructor because the parent disposable is not yet available. So we do it lazily when the - * component is shown. - */ - private static final class LazyDisposable implements Activatable { - private final WeakReference> reference; - - private LazyDisposable(ComponentWithButton component) { - reference = new WeakReference<>(component); - new UiNotifyConnector.Once(component, this); - } - - @Override - public void showNotify() { - ComponentWithButton component = reference.get(); - if (component == null) { - return; // component is collected - } - Disposable disposable = - ApplicationManager.getApplication() == null - ? null - : UI_DISPOSABLE.getData(DataManager.getInstance().getDataContext(component)); - if (disposable == null) { - return; // parent disposable not found - } - Disposer.register(disposable, component); - } - } -} diff --git a/src/main/java/com/sourcegraph/cody/ui/PasswordFieldWithShowHideButton.java b/src/main/java/com/sourcegraph/cody/ui/PasswordFieldWithShowHideButton.java deleted file mode 100644 index 11eece7d7d..0000000000 --- a/src/main/java/com/sourcegraph/cody/ui/PasswordFieldWithShowHideButton.java +++ /dev/null @@ -1,166 +0,0 @@ -package com.sourcegraph.cody.ui; - -import com.intellij.icons.AllIcons; -import com.intellij.ui.components.JBPasswordField; -import com.sourcegraph.cody.Icons; -import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.util.Optional; -import java.util.function.Supplier; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.text.Document; -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class PasswordFieldWithShowHideButton extends ComponentWithButton { - private final String placeholder; - private final char echoChar; - private boolean passwordLoaded = false; - private boolean passwordVisible = false; - private boolean passwordChanged = false; - private boolean errorState = false; - @NotNull private final JBPasswordField passwordField; - - // A function that retrieves the current password from storage. - private final Supplier passwordLoader; - - public PasswordFieldWithShowHideButton( - @NotNull JBPasswordField passwordField, Supplier passwordLoader) { - this(passwordField, passwordLoader, 40); - } - - private PasswordFieldWithShowHideButton( - @NotNull JBPasswordField passwordField, - Supplier passwordLoader, - int placeholderLength) { - super(passwordField); - this.passwordField = passwordField; - this.echoChar = passwordField.getEchoChar(); - this.passwordLoader = passwordLoader; - this.placeholder = StringUtils.repeat("x", placeholderLength); - - // Disable the password field by default so that the user can't type into it. - setComponentDisabledOverride(true); - - ActionListener buttonActionListener = - e -> { - // Toggle password visibility - passwordVisible = !passwordVisible; - update(); - }; - addButtonActionListener(buttonActionListener); - - // Mark the password as changed whenever the user types into the field. - DocumentListener documentListener = createDocumentListener(); - passwordField.getDocument().addDocumentListener(documentListener); - - // Handle click events on passwordField - passwordField.addMouseListener( - new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (!passwordVisible && !componentDisabledOverride) { - buttonActionListener.actionPerformed(null); - } - } - }); - - // Update UI - update(); - } - - @NotNull - private DocumentListener createDocumentListener() { - return new DocumentListener() { - @Override - public void insertUpdate(DocumentEvent e) { - passwordChanged = true; - } - - @Override - public void removeUpdate(DocumentEvent e) { - passwordChanged = true; - } - - @Override - public void changedUpdate(DocumentEvent e) { - passwordChanged = true; - } - }; - } - - public void resetUI() { - passwordChanged = false; - passwordVisible = false; - passwordLoaded = false; - setErrorState(false); - update(); - } - - @Override - public void setEnabled(boolean enabled) { - if (!errorState) { - super.setEnabled(enabled); - } - } - - private void setErrorState(boolean errorState) { - this.errorState = errorState; - if (errorState) { - passwordField.setText("Access was denied to secure storage."); - setEnabled(false); - } - } - - /** This should be called "updateUI" but that was already taken. :shrug: */ - private void update() { - setComponentDisabledOverride(!passwordVisible); - passwordField.setEchoChar(passwordVisible ? (char) 0 : echoChar); - setButtonIcon(passwordVisible ? AllIcons.Actions.Show : Icons.Actions.Hide); - if (!passwordLoaded) { - if (passwordVisible) { - String storedPassword = passwordLoader.get(); - if (storedPassword != null) { - passwordField.setText(storedPassword); - passwordLoaded = true; - } else { - setErrorState(true); - } - } else { - passwordField.setText(placeholder); - } - } - setIconTooltip(passwordVisible ? "Hide" : "Show"); - } - - public void setEmptyText(@NotNull String emptyText) { - passwordField.getEmptyText().setText(emptyText); - } - - /** - * @return Null means we don't know the token because it wasn't loaded from the secure storage. An - * empty value means the user has explicitly set it to empty. - */ - @Nullable - public String getPassword() { - String password = - Optional.ofNullable(passwordField.getPassword()).map(String::copyValueOf).orElse(""); - // Known edge case: if the user's password is exactly the placeholder, we will think there's no - // password. - // We won't fix it because we currently only use the component for access tokens where this is - // not a problem. - return password.equals(placeholder) || errorState ? null : password; - } - - @NotNull - public Document getDocument() { - return passwordField.getDocument(); - } - - public boolean hasPasswordChanged() { - return passwordChanged; - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index 8efad232d2..7ce06b9e6d 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -2,7 +2,6 @@ package com.sourcegraph.cody.agent import com.intellij.ide.plugins.PluginManagerCore import com.intellij.openapi.application.ApplicationInfo -import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.extensions.PluginId import com.intellij.openapi.project.Project @@ -103,8 +102,7 @@ private constructor( fun create(project: Project): CompletableFuture { try { val conn = startAgentProcess() - val client = CodyAgentClient(WebUIServiceWebviewProvider(project)) - client.onSetConfigFeatures = project.service() + val client = CodyAgentClient(project, WebUIServiceWebviewProvider(project)) val launcher = startAgentLauncher(conn, client) val server = launcher.remoteProxy val listeningToJsonRpc = launcher.startListening() diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentClient.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentClient.kt new file mode 100644 index 0000000000..13a0cc3792 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentClient.kt @@ -0,0 +1,190 @@ +package com.sourcegraph.cody.agent + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import com.sourcegraph.cody.agent.protocol.ProtocolTextDocument +import com.sourcegraph.cody.agent.protocol.WebviewCreateWebviewPanelParams +import com.sourcegraph.cody.agent.protocol_generated.DebugMessage +import com.sourcegraph.cody.agent.protocol_generated.DisplayCodeLensParams +import com.sourcegraph.cody.agent.protocol_generated.Env_OpenExternalParams +import com.sourcegraph.cody.agent.protocol_generated.Null +import com.sourcegraph.cody.agent.protocol_generated.TextDocumentEditParams +import com.sourcegraph.cody.agent.protocol_generated.TextDocument_ShowParams +import com.sourcegraph.cody.agent.protocol_generated.UntitledTextDocument +import com.sourcegraph.cody.agent.protocol_generated.WorkspaceEditParams +import com.sourcegraph.cody.edit.EditService +import com.sourcegraph.cody.edit.LensesService +import com.sourcegraph.cody.error.CodyConsole +import com.sourcegraph.cody.ignore.IgnoreOracle +import com.sourcegraph.cody.ui.NativeWebviewProvider +import com.sourcegraph.common.BrowserOpener +import com.sourcegraph.utils.CodyEditorUtil +import java.util.concurrent.CompletableFuture +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest + +/** + * Implementation of the client part of the Cody agent protocol. This class dispatches the requests + * and notifications sent by the agent. + */ +@Suppress("unused", "FunctionName") +class CodyAgentClient(private val project: Project, private val webview: NativeWebviewProvider) { + companion object { + private val logger = Logger.getInstance(CodyAgentClient::class.java) + } + + /** + * Helper to run client request/notification handlers on the IntelliJ event thread. Use this + * helper for handlers that require access to the IntelliJ editor, for example to read the text + * contents of the open editor. + */ + private fun acceptOnEventThreadAndGet( + callback: (() -> R), + ): CompletableFuture { + val result = CompletableFuture() + ApplicationManager.getApplication().invokeLater { + try { + result.complete(callback.invoke()) + } catch (e: Exception) { + result.completeExceptionally(e) + } + } + return result + } + + // ============= + // Requests + // ============= + + @JsonRequest("env/openExternal") + fun env_openExternal(params: Env_OpenExternalParams): CompletableFuture { + return acceptOnEventThreadAndGet { + BrowserOpener.openInBrowser(project, params.uri) + true + } + } + + @JsonRequest("workspace/edit") + fun workspace_edit(params: WorkspaceEditParams): CompletableFuture { + return acceptOnEventThreadAndGet { + try { + EditService.getInstance(project).performWorkspaceEdit(params) + } catch (e: RuntimeException) { + logger.error(e) + false + } + } + } + + @JsonRequest("textDocument/edit") + fun textDocument_edit(params: TextDocumentEditParams): CompletableFuture { + return acceptOnEventThreadAndGet { + try { + EditService.getInstance(project).performTextEdits(params.uri, params.edits) + } catch (e: RuntimeException) { + logger.error(e) + false + } + } + } + + @JsonRequest("textDocument/show") + fun textDocument_show(params: TextDocument_ShowParams): CompletableFuture { + return acceptOnEventThreadAndGet { + val selection = params.options?.selection + val preserveFocus = params.options?.preserveFocus + val vf = CodyEditorUtil.findFileOrScratch(project, params.uri) + if (vf != null) { + CodyEditorUtil.showDocument(project, vf, selection, preserveFocus) + true + } else { + false + } + } + } + + @JsonRequest("textDocument/openUntitledDocument") + fun textDocument_openUntitledDocument( + params: UntitledTextDocument + ): CompletableFuture { + return acceptOnEventThreadAndGet { + val vf = CodyEditorUtil.createFileOrScratchFromUntitled(project, params.uri, params.content) + vf?.let { ProtocolTextDocument.fromVirtualFile(it) } + } + } + + // ============= + // Notifications + // ============= + + @JsonNotification("codeLenses/display") + fun codeLenses_display(params: DisplayCodeLensParams) { + runInEdt { LensesService.getInstance(project).updateLenses(params.uri, params.codeLenses) } + } + + @JsonNotification("ignore/didChange") + fun ignore_didChange(params: Null?) { + IgnoreOracle.getInstance(project).onIgnoreDidChange() + } + + @JsonNotification("debug/message") + fun debug_message(params: DebugMessage) { + if (!project.isDisposed) { + CodyConsole.getInstance(project).addMessage(params) + } + } + + // ================================================ + // Webviews, forwarded to the NativeWebviewProvider + // ================================================ + + @JsonNotification("webview/createWebviewPanel") + fun webviewCreateWebviewPanel(params: WebviewCreateWebviewPanelParams) { + webview.createPanel(params) + } + + @JsonNotification("webview/postMessageStringEncoded") + fun webviewPostMessageStringEncoded(params: WebviewPostMessageStringEncodedParams) { + webview.receivedPostMessage(params) + } + + @JsonNotification("webview/registerWebviewViewProvider") + fun webviewRegisterWebviewViewProvider(params: WebviewRegisterWebviewViewProviderParams) { + webview.registerViewProvider(params) + } + + @JsonNotification("webview/setHtml") + fun webviewSetHtml(params: WebviewSetHtmlParams) { + webview.setHtml(params) + } + + @JsonNotification("webview/setIconPath") + fun webviewSetIconPath(params: WebviewSetIconPathParams) { + // TODO: Implement this. + println("TODO, implement webview/setIconPath") + } + + @JsonNotification("webview/setOptions") + fun webviewSetOptions(params: WebviewSetOptionsParams) { + webview.setOptions(params) + } + + @JsonNotification("webview/setTitle") + fun webviewSetTitle(params: WebviewSetTitleParams) { + webview.setTitle(params) + } + + @JsonNotification("webview/reveal") + fun webviewReveal(params: WebviewRevealParams) { + // TODO: Implement this. + println("TODO, implement webview/reveal") + } + + @JsonNotification("webview/dispose") + fun webviewDispose(params: WebviewDisposeParams) { + // TODO: Implement this. + println("TODO, implement webview/dispose") + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt index 3e9d792f7c..4aaa86ff2f 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt @@ -9,7 +9,6 @@ import com.sourcegraph.cody.agent.protocol.AutocompleteResult import com.sourcegraph.cody.agent.protocol.ChatHistoryResponse import com.sourcegraph.cody.agent.protocol.ChatModelsParams import com.sourcegraph.cody.agent.protocol.ChatModelsResponse -import com.sourcegraph.cody.agent.protocol.ChatSubmitMessageParams import com.sourcegraph.cody.agent.protocol.CompletionItemParams import com.sourcegraph.cody.agent.protocol.CurrentUserCodySubscription import com.sourcegraph.cody.agent.protocol.Event @@ -41,7 +40,6 @@ import com.sourcegraph.cody.agent.protocol_generated.Graphql_GetRepoIdsParams import com.sourcegraph.cody.agent.protocol_generated.Graphql_GetRepoIdsResult import com.sourcegraph.cody.agent.protocol_generated.Null import com.sourcegraph.cody.agent.protocol_generated.ServerInfo -import com.sourcegraph.cody.chat.ConnectionId import java.util.concurrent.CompletableFuture import org.eclipse.lsp4j.jsonrpc.services.JsonNotification import org.eclipse.lsp4j.jsonrpc.services.JsonRequest @@ -161,10 +159,6 @@ interface _LegacyAgentServer { @JsonRequest("command/execute") fun commandExecute(params: CommandExecuteParams): CompletableFuture - @JsonRequest("commands/explain") fun legacyCommandsExplain(): CompletableFuture - - @JsonRequest("commands/smell") fun legacyCommandsSmell(): CompletableFuture - @JsonRequest("editCommands/document") fun commandsDocument(): CompletableFuture @JsonRequest("editCommands/code") @@ -172,8 +166,6 @@ interface _LegacyAgentServer { @JsonRequest("editCommands/test") fun commandsTest(): CompletableFuture - @JsonRequest("chat/new") fun chatNewTODODeleteMe(): CompletableFuture - @JsonRequest("chat/web/new") fun chatNew(): CompletableFuture @JsonRequest("webview/receiveMessageStringEncoded") @@ -187,9 +179,6 @@ interface _LegacyAgentServer { @JsonRequest("webview/resolveWebviewView") fun webviewResolveWebviewView(params: WebviewResolveWebviewViewParams): CompletableFuture - @JsonRequest("chat/submitMessage") - fun chatSubmitMessage(params: ChatSubmitMessageParams): CompletableFuture - @JsonRequest("chat/models") fun chatModels(params: ChatModelsParams): CompletableFuture diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt index 0105375fea..02557e1837 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentService.kt @@ -10,18 +10,10 @@ import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.project.Project import com.intellij.util.net.HttpConfigurable -import com.sourcegraph.cody.agent.protocol.ProtocolTextDocument import com.sourcegraph.cody.config.CodyApplicationSettings -import com.sourcegraph.cody.context.RemoteRepoSearcher -import com.sourcegraph.cody.edit.EditService -import com.sourcegraph.cody.edit.LensesService -import com.sourcegraph.cody.error.CodyConsole -import com.sourcegraph.cody.ignore.IgnoreOracle import com.sourcegraph.cody.listeners.CodyFileEditorListener import com.sourcegraph.cody.statusbar.CodyStatusService -import com.sourcegraph.common.BrowserOpener import com.sourcegraph.common.CodyBundle -import com.sourcegraph.utils.CodyEditorUtil import java.util.Timer import java.util.TimerTask import java.util.concurrent.CompletableFuture @@ -29,7 +21,6 @@ import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.concurrent.atomic.AtomicReference import java.util.function.Consumer -import java.util.function.Function @Service(Service.Level.PROJECT) class CodyAgentService(private val project: Project) : Disposable { @@ -57,69 +48,6 @@ class CodyAgentService(private val project: Project) : Disposable { 0, 5000) // Check every 5 seconds onStartup { agent -> - agent.client.onOpenExternal = Function { params -> - BrowserOpener.openInBrowser(project, params.uri) - true - } - - agent.client.onWorkspaceEdit = Function { params -> - try { - EditService.getInstance(project).performWorkspaceEdit(params) - } catch (e: RuntimeException) { - logger.error(e) - false - } - } - - agent.client.onCodeLensesDisplay = Consumer { params -> - LensesService.getInstance(project).updateLenses(params.uri, params.codeLenses) - } - - agent.client.onTextDocumentEdit = Function { params -> - try { - EditService.getInstance(project).performTextEdits(params.uri, params.edits) - } catch (e: RuntimeException) { - logger.error(e) - false - } - } - - agent.client.onTextDocumentShow = Function { params -> - val selection = params.options?.selection - val preserveFocus = params.options?.preserveFocus - val vf = CodyEditorUtil.findFileOrScratch(project, params.uri) ?: return@Function false - CodyEditorUtil.showDocument(project, vf, selection, preserveFocus) - true - } - - agent.client.onOpenUntitledDocument = Function { params -> - val result = CompletableFuture() - ApplicationManager.getApplication().invokeAndWait { - val vf = - CodyEditorUtil.createFileOrScratchFromUntitled(project, params.uri, params.content) - result.complete(if (vf == null) null else ProtocolTextDocument.fromVirtualFile(vf)) - } - result.get() - } - - agent.client.onRemoteRepoDidChange = Consumer { - RemoteRepoSearcher.getInstance(project).remoteRepoDidChange() - } - - agent.client.onRemoteRepoDidChangeState = Consumer { state -> - RemoteRepoSearcher.getInstance(project).remoteRepoDidChangeState(state) - } - - agent.client.onIgnoreDidChange = Consumer { - IgnoreOracle.getInstance(project).onIgnoreDidChange() - } - - agent.client.onDebugMessage = Consumer { message -> - if (!project.isDisposed) { - CodyConsole.getInstance(project).addMessage(message) - } - } - if (!project.isDisposed) { CodyFileEditorListener.registerAllOpenedFiles(project, agent) } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ChatNewResponse.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ChatNewResponse.kt deleted file mode 100644 index b6992695ea..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ChatNewResponse.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.sourcegraph.cody.agent.protocol - -data class ChatNewResponse(val panelId: String, val chatId: String) {} diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ChatSubmitMessageParams.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ChatSubmitMessageParams.kt deleted file mode 100644 index ba748983ab..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ChatSubmitMessageParams.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.sourcegraph.cody.agent.protocol - -import com.sourcegraph.cody.agent.WebviewMessage - -data class ChatSubmitMessageParams(val id: String, val message: WebviewMessage) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/DebugMessage.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/DebugMessage.kt deleted file mode 100644 index 1ba1b029ed..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/DebugMessage.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.sourcegraph.cody.agent.protocol - -data class DebugMessage(var channel: String, var message: String) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextDocumentEditParams.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextDocumentEditParams.kt deleted file mode 100644 index ee57c1d103..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextDocumentEditParams.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.sourcegraph.cody.agent.protocol - -data class TextDocumentEditParams( - val uri: String, - val edits: List, - val options: TextDocumentEditOptions? = null -) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextEdit.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextEdit.kt deleted file mode 100644 index e01906db9f..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextEdit.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.sourcegraph.cody.agent.protocol - -import com.sourcegraph.cody.agent.protocol_generated.Position -import com.sourcegraph.cody.agent.protocol_generated.Range - -data class TextEdit( - // This tag will be 'replace', 'insert', or 'delete'. - val type: String, - - // Valid for replace & delete. - val range: Range? = null, - - // Valid for insert. - val position: Position? = null, - - // Valid for replace & insert. - val value: String? = null -) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/UntitledTextDocument.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/UntitledTextDocument.kt deleted file mode 100644 index 8c52bd0188..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/UntitledTextDocument.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.sourcegraph.cody.agent.protocol - -data class UntitledTextDocument( - val uri: String, - val content: String?, - val language: String?, -) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/WorkspaceEditMetadata.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/WorkspaceEditMetadata.kt deleted file mode 100644 index 6a0007b02a..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/WorkspaceEditMetadata.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.sourcegraph.cody.agent.protocol - -data class WorkspaceEditMetadata(val isRefactoring: Boolean = false) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/WorkspaceEditOperation.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/WorkspaceEditOperation.kt deleted file mode 100644 index bc082f10dc..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/WorkspaceEditOperation.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.sourcegraph.cody.agent.protocol - -data class WorkspaceEditParamsOptions( - val overwrite: Boolean = false, - val ignoreIfNotExists: Boolean = false, - val recursive: Boolean = false -) - -data class WorkspaceEditOperation( - val type: String, // all - val uri: String? = null, // created, delete, edit - val oldUri: String? = null, // rename - val newUri: String? = null, // rename - val textContents: String? = null, // create-file - val options: WorkspaceEditParamsOptions? = null, // all - val metadata: WorkspaceEditMetadata? = null, // all - val edits: List? = null -) // edit diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/WorkspaceEditParams.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/WorkspaceEditParams.kt deleted file mode 100644 index 41675d7dba..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/WorkspaceEditParams.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.sourcegraph.cody.agent.protocol - -data class WorkspaceEditParams( - val operations: List, - val metadata: WorkspaceEditMetadata? = null -) diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ChatPromptHistory.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ChatPromptHistory.kt deleted file mode 100644 index 5fcc7dca8c..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ChatPromptHistory.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.sourcegraph.cody.chat - -import com.intellij.openapi.project.Project -import com.sourcegraph.cody.history.HistoryService -import com.sourcegraph.cody.history.state.MessageState - -class ChatPromptHistory(project: Project, chatSession: ChatSession, capacity: Int) : - PromptHistory(capacity) { - - init { - preloadHistoricalMessages(project, chatSession) - } - - private fun preloadHistoricalMessages(project: Project, chatSession: ChatSession) { - HistoryService.getInstance(project) - .findActiveAccountChat(chatSession.getInternalId()) - ?.messages - ?.filter { it.speaker == MessageState.SpeakerState.HUMAN } - ?.mapNotNull { it.text } - ?.forEach { add(it) } - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ChatSession.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ChatSession.kt deleted file mode 100644 index cc3ae8d259..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ChatSession.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.sourcegraph.cody.chat - -import com.intellij.util.concurrency.annotations.RequiresEdt -import com.sourcegraph.cody.agent.ExtensionMessage -import com.sourcegraph.cody.agent.WebviewMessage -import com.sourcegraph.cody.agent.protocol.ContextItem -import com.sourcegraph.cody.vscode.CancellationToken - -typealias ConnectionId = String - -interface ChatSession { - - fun getConnectionId(): ConnectionId? - - fun sendWebviewMessage(message: WebviewMessage) - - @RequiresEdt fun sendMessage(text: String, contextItems: List) - - fun receiveMessage(extensionMessage: ExtensionMessage) - - fun getCancellationToken(): CancellationToken - - fun getInternalId(): String -} diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/MarkdownExtensions.kt b/src/main/kotlin/com/sourcegraph/cody/chat/MarkdownExtensions.kt deleted file mode 100644 index 39f0579ce1..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/chat/MarkdownExtensions.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.sourcegraph.cody.chat - -import org.commonmark.node.Document -import org.commonmark.node.FencedCodeBlock -import org.commonmark.node.IndentedCodeBlock -import org.commonmark.node.Node - -fun Node.isCodeBlock(): Boolean { - return this is FencedCodeBlock || this is IndentedCodeBlock -} - -fun Node.findNodeAfterLastCodeBlock(): Node { - val lastNodeAfterCode = - generateSequence(lastChild) { it.previous }.takeWhile { !it.isCodeBlock() }.lastOrNull() - return lastNodeAfterCode.buildNewDocumentFrom() -} - -fun Node?.buildNewDocumentFrom(): Document { - var nodeAfterCode = this - val document = Document() - while (nodeAfterCode != null) { - val nextNode = nodeAfterCode.next - document.appendChild(nodeAfterCode) - nodeAfterCode = nextNode - } - return document -} - -fun Node.extractCodeAndLanguage() = - when (this) { - is FencedCodeBlock -> Pair(literal, info) - is IndentedCodeBlock -> Pair(literal, "") - else -> Pair("", "") - } diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ui/BlinkingCursorComponent.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ui/BlinkingCursorComponent.kt deleted file mode 100644 index b398724a1a..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ui/BlinkingCursorComponent.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.sourcegraph.cody.chat.ui - -import com.intellij.util.ui.UIUtil -import java.awt.Dimension -import java.awt.Font -import java.awt.Graphics -import javax.swing.JPanel -import javax.swing.Timer - -class BlinkingCursorComponent private constructor() : JPanel() { - private var showCursor = true - - private val timer: Timer = - Timer(500) { - showCursor = !showCursor - repaint() - } - - init { - timer.start() - } - - override fun paintComponent(g: Graphics) { - super.paintComponent(g) - if (showCursor) { - g.font = Font("Monospaced", Font.PLAIN, 12) - g.drawString("█", 10, 20) - g.color = UIUtil.getActiveTextColor() - background = UIUtil.getPanelBackground() - } - } - - override fun getPreferredSize(): Dimension { - return Dimension(30, 30) - } - - companion object { - var instance = BlinkingCursorComponent() - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ui/ChatPanel.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ui/ChatPanel.kt deleted file mode 100644 index 5b97c4598d..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ui/ChatPanel.kt +++ /dev/null @@ -1,121 +0,0 @@ -package com.sourcegraph.cody.chat.ui - -import com.intellij.icons.AllIcons -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.VerticalFlowLayout -import com.intellij.util.IconUtil -import com.intellij.util.concurrency.annotations.RequiresEdt -import com.sourcegraph.cody.PromptPanel -import com.sourcegraph.cody.agent.WebviewMessage -import com.sourcegraph.cody.agent.protocol.ChatMessage -import com.sourcegraph.cody.agent.protocol.ChatModelsResponse -import com.sourcegraph.cody.agent.protocol.ModelUsage -import com.sourcegraph.cody.chat.ChatSession -import com.sourcegraph.cody.config.CodyAuthenticationManager -import com.sourcegraph.cody.context.ui.EnhancedContextPanel -import com.sourcegraph.cody.history.HistoryService -import com.sourcegraph.cody.history.state.LLMState -import com.sourcegraph.cody.ui.ChatScrollPane -import com.sourcegraph.cody.vscode.CancellationToken -import java.awt.BorderLayout -import java.awt.Dimension -import java.awt.FlowLayout -import javax.swing.BorderFactory -import javax.swing.JButton -import javax.swing.JPanel - -class ChatPanel( - val project: Project, - val chatSession: ChatSession, - chatModelProviderFromState: ChatModelsResponse.ChatModelProvider? -) : JPanel(VerticalFlowLayout(VerticalFlowLayout.CENTER, 0, 0, true, false)) { - - val promptPanel: PromptPanel = PromptPanel(project, chatSession) - private val llmDropdown = - LlmDropdown( - ModelUsage.CHAT, - project, - onSetSelectedItem = ::setLlmForAgentSession, - parentDialog = null, - chatModelProviderFromState) - private val messagesPanel = MessagesPanel(project, chatSession) - private val chatPanel = ChatScrollPane(messagesPanel) - - internal val contextView: EnhancedContextPanel = EnhancedContextPanel.create(project, chatSession) - - private val stopGeneratingButton = - object : JButton("Stop generating", IconUtil.desaturate(AllIcons.Actions.Suspend)) { - init { - isVisible = false - layout = FlowLayout(FlowLayout.CENTER) - minimumSize = Dimension(Short.MAX_VALUE.toInt(), 0) - isOpaque = false - } - } - - init { - layout = BorderLayout() - border = BorderFactory.createEmptyBorder(0, 0, 0, 10) - add(chatPanel, BorderLayout.CENTER) - - val lowerPanel = JPanel(VerticalFlowLayout(VerticalFlowLayout.BOTTOM, 10, 10, true, false)) - lowerPanel.add(stopGeneratingButton) - lowerPanel.add(promptPanel) - lowerPanel.add(contextView) - - val wrapper = JPanel() - wrapper.add(llmDropdown) - wrapper.layout = VerticalFlowLayout(VerticalFlowLayout.TOP, 12, 12, true, false) - - add(lowerPanel, BorderLayout.SOUTH) - add(wrapper, BorderLayout.NORTH) - } - - fun setAsActive() { - contextView.setContextFromThisChatAsDefault() - promptPanel.focus() - } - - fun isEnhancedContextEnabled(): Boolean = contextView.isEnhancedContextEnabled - - @RequiresEdt - fun addOrUpdateMessage(message: ChatMessage, index: Int) { - val numberOfMessagesBeforeAddOrUpdate = messagesPanel.componentCount - if (numberOfMessagesBeforeAddOrUpdate == 1) { - llmDropdown.updateAfterFirstMessage() - promptPanel.updateEmptyTextAfterFirstMessage() - } - messagesPanel.addOrUpdateMessage(message, index) - if (numberOfMessagesBeforeAddOrUpdate < messagesPanel.componentCount) { - chatPanel.touchingBottom = true - } - } - - @RequiresEdt - fun registerCancellationToken(cancellationToken: CancellationToken) { - messagesPanel.registerCancellationToken(cancellationToken) - promptPanel.registerCancellationToken(cancellationToken) - - cancellationToken.onFinished { stopGeneratingButton.isVisible = false } - - stopGeneratingButton.isVisible = true - for (listener in stopGeneratingButton.actionListeners) { - stopGeneratingButton.removeActionListener(listener) - } - stopGeneratingButton.addActionListener { cancellationToken.abort() } - } - - private fun setLlmForAgentSession(chatModelProvider: ChatModelsResponse.ChatModelProvider) { - val activeAccountType = CodyAuthenticationManager.getInstance(project).account - if (activeAccountType?.isEnterpriseAccount() == true) { - // no need to send the webview message since the chat model is set by default - } else { - chatSession.sendWebviewMessage( - WebviewMessage(command = "chatModel", model = chatModelProvider.model)) - } - - HistoryService.getInstance(project) - .updateChatLlmProvider( - chatSession.getInternalId(), LLMState.fromChatModel(chatModelProvider)) - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ui/CodeEditorButtons.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ui/CodeEditorButtons.kt deleted file mode 100644 index 33d570ee39..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ui/CodeEditorButtons.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.sourcegraph.cody.chat.ui - -import com.sourcegraph.cody.chat.CodeEditorFactory -import java.awt.event.MouseAdapter -import java.awt.event.MouseEvent -import java.awt.event.MouseMotionAdapter -import javax.swing.JButton -import javax.swing.JLayeredPane - -class CodeEditorButtons(val buttons: Array) { - init { - for (button in buttons) { - button.addMouseMotionListener( - object : MouseMotionAdapter() { - override fun mouseMoved(e: MouseEvent) { - setVisible(true) - } - }) - button.addMouseListener( - object : MouseAdapter() { - override fun mouseExited(e: MouseEvent) { - setVisible(false) - } - }) - } - } - - fun addButtons(layeredEditorPane: JLayeredPane, editorWidth: Int) { - updateBounds(editorWidth) - for (jButton in buttons) { - layeredEditorPane.add(jButton, JLayeredPane.PALETTE_LAYER, 0) - } - } - - fun updateBounds(editorWidth: Int) { - var shift = 0 - for (jButton in buttons) { - val jButtonPreferredSize = jButton.preferredSize - jButton.setBounds( - editorWidth - jButtonPreferredSize.width - shift, - 0, - jButtonPreferredSize.width, - jButtonPreferredSize.height) - if (jButtonPreferredSize.width > 0) { // Do not add space for collapsed button. - shift += jButtonPreferredSize.width + CodeEditorFactory.spaceBetweenButtons - } - } - } - - fun setVisible(visible: Boolean) { - for (button in buttons) { - button.isVisible = visible - } - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ui/ContextFileActionLink.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ui/ContextFileActionLink.kt deleted file mode 100644 index 9283cef818..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ui/ContextFileActionLink.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.sourcegraph.cody.chat.ui - -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.project.Project -import com.intellij.ui.JBColor -import com.intellij.ui.components.AnActionLink -import com.sourcegraph.cody.agent.protocol.ContextItemFile -import java.awt.Color -import java.awt.Font -import java.awt.Graphics -import java.awt.font.TextAttribute - -class ContextFileActionLink( - project: Project, - contextItemFile: ContextItemFile, - anAction: AnAction -) : AnActionLink("", anAction) { - private val localFileBackground = JBColor(Color(182, 210, 242), Color(56, 85, 112)) - private val isReferringToLocalFile = contextItemFile.isLocal() - - init { - text = contextItemFile.getLinkActionText(project.basePath) - font = - when { - contextItemFile.isIgnored == true || contextItemFile.isTooLarge == true -> - Font( - super.getFont().attributes + - (TextAttribute.STRIKETHROUGH to TextAttribute.STRIKETHROUGH_ON)) - else -> super.getFont() - } - toolTipText = - when { - contextItemFile.isIgnored == true -> "File ignored by an admin setting" - contextItemFile.isTooLarge == true -> "Excluded due to context window limit" - else -> contextItemFile.uri.path - } - } - - override fun paintComponent(g: Graphics) { - if (isReferringToLocalFile) { - g.color = localFileBackground - - val fm = g.fontMetrics - val rect = fm.getStringBounds(text, g) - val textWidth = rect.width.toInt() - g.fillRect(0, 0, textWidth, height) - } - super.paintComponent(g) - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ui/ContextFilesPanel.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ui/ContextFilesPanel.kt deleted file mode 100644 index eb64ff3df6..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ui/ContextFilesPanel.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.sourcegraph.cody.chat.ui - -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.fileEditor.OpenFileDescriptor -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.LocalFileSystem -import com.intellij.util.concurrency.annotations.RequiresEdt -import com.intellij.util.ui.JBInsets -import com.intellij.util.ui.JBUI -import com.sourcegraph.cody.agent.protocol.ChatMessage -import com.sourcegraph.cody.agent.protocol.ContextItem -import com.sourcegraph.cody.agent.protocol.ContextItemFile -import com.sourcegraph.cody.agent.protocol.Speaker -import com.sourcegraph.cody.agent.protocol_extensions.* -import com.sourcegraph.cody.chat.ChatUIConstants.ASSISTANT_MESSAGE_GRADIENT_WIDTH -import com.sourcegraph.cody.chat.ChatUIConstants.TEXT_MARGIN -import com.sourcegraph.cody.ui.AccordionSection -import com.sourcegraph.common.BrowserOpener.openInBrowser -import com.sourcegraph.common.ui.SimpleDumbAwareEDTAction -import java.awt.BorderLayout -import java.awt.Insets -import javax.swing.JPanel -import javax.swing.border.EmptyBorder - -class ContextFilesPanel( - val project: Project, - chatMessage: ChatMessage, -) : PanelWithGradientBorder(ASSISTANT_MESSAGE_GRADIENT_WIDTH, Speaker.ASSISTANT) { - init { - this.layout = BorderLayout() - isVisible = false - - updateContentWith(chatMessage.contextFiles) - } - - fun updateContentWith(contextItems: List?) { - val contextItemFiles = contextItems?.mapNotNull { it as? ContextItemFile } - - if (contextItemFiles.isNullOrEmpty()) { - return - } - - val title = deriveAccordionTitle(contextItemFiles) - val margin = JBInsets.create(Insets(TEXT_MARGIN, TEXT_MARGIN, TEXT_MARGIN, TEXT_MARGIN)) - val accordionSection = AccordionSection(title) - accordionSection.isOpaque = false - accordionSection.border = EmptyBorder(margin) - contextItemFiles.forEachIndexed { index, contextFile: ContextItemFile -> - val filePanel = createFileWithLinkPanel(contextFile) - accordionSection.contentPanel.add(filePanel, index) - } - - this.removeAll() - this.isVisible = true - add(accordionSection, BorderLayout.CENTER) - } - - private fun deriveAccordionTitle(contextItemFiles: List): String { - val (excludedFiles, includedFiles) = - contextItemFiles.partition { it.isTooLarge == true || it.isIgnored == true } - - val lineCount = includedFiles.sumOf { it.range?.length()?.toInt() ?: 0 } - val lines = "$lineCount ${"line".pluralize(lineCount)}" - - val excludedFileCount = excludedFiles.distinctBy { it.uri }.size - val includedFileCount = includedFiles.distinctBy { it.uri }.size - val excludedFilesMessage = - when { - excludedFileCount > 0 -> - " — $excludedFileCount ${"file".pluralize(excludedFileCount)} excluded" - else -> "" - } - val files = "$includedFileCount ${"file".pluralize(includedFileCount)}${excludedFilesMessage}" - - val title = - when { - lineCount > 0 -> "$lines from $files" - else -> files - } - - val prefix = "✨ Context: " - return "$prefix $title" - } - - @RequiresEdt - private fun createFileWithLinkPanel(contextItemFile: ContextItemFile): JPanel { - val anAction = SimpleDumbAwareEDTAction { - if (contextItemFile.isLocal()) { - openInEditor(contextItemFile) - } else { - openInBrowser(project, contextItemFile.uri) - } - } - - val goToFile = ContextFileActionLink(project, contextItemFile, anAction) - val panel = JPanel(BorderLayout()) - panel.isOpaque = false - panel.border = JBUI.Borders.empty(3, 3, 0, 0) - panel.add(goToFile, BorderLayout.PAGE_START) - return panel - } - - private fun openInEditor(contextItemFile: ContextItemFile) { - val logicalLine = contextItemFile.range?.start?.line ?: 0 - val contextFilePath = contextItemFile.getPath() ?: return - ApplicationManager.getApplication().executeOnPooledThread { - val findFileByNioFile = LocalFileSystem.getInstance().findFileByNioFile(contextFilePath) - if (findFileByNioFile != null) { - ApplicationManager.getApplication().invokeLater { - OpenFileDescriptor( - project, findFileByNioFile, logicalLine.toInt(), /* logicalColumn= */ 0) - .navigate(/* requestFocus= */ true) - } - } - } - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ui/MessagePart.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ui/MessagePart.kt deleted file mode 100644 index 18d453e26b..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ui/MessagePart.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.sourcegraph.cody.chat.ui - -import com.intellij.ide.highlighter.HighlighterFactory -import com.intellij.lang.Language -import com.intellij.openapi.command.WriteCommandAction -import com.intellij.openapi.editor.colors.EditorColorsManager -import com.intellij.openapi.editor.ex.EditorEx -import com.intellij.openapi.fileTypes.FileTypeManager -import com.intellij.openapi.fileTypes.PlainTextFileType -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Computable -import com.intellij.util.ui.SwingHelper -import com.sourcegraph.cody.ui.AttributionButtonController -import java.util.concurrent.atomic.AtomicReference -import javax.swing.JComponent -import javax.swing.JEditorPane - -sealed interface MessagePart - -class TextPart(val component: JEditorPane) : MessagePart { - fun updateText(text: String) { - SwingHelper.setHtml(component, text, null) - } -} - -class CodeEditorPart( - val component: JComponent, - private val editor: EditorEx, - val attribution: AttributionButtonController -) : MessagePart { - - private var recognizedLanguage: Language? = null - private val _text = AtomicReference("") - var text: String - set(value) { - _text.set(value) - } - get() = _text.get() - - fun updateCode(project: Project, code: String, language: String?) { - recognizeLanguage(language) - updateText(project, code) - } - - fun recognizeLanguage(languageName: String?) { - if (recognizedLanguage != null) return - val language = - Language.getRegisteredLanguages() - .filter { it != Language.ANY } - .firstOrNull { it.displayName.equals(languageName, ignoreCase = true) } - if (language != null) { - val fileType = - FileTypeManager.getInstance().findFileTypeByLanguage(language) - ?: PlainTextFileType.INSTANCE - val settings = EditorColorsManager.getInstance().schemeForCurrentUITheme - val editorHighlighter = HighlighterFactory.createHighlighter(fileType, settings, null) - editor.highlighter = editorHighlighter - recognizedLanguage = language - } - } - - private fun updateText(project: Project, text: String) { - this.text = text - WriteCommandAction.runWriteCommandAction( - project, Computable { editor.document.replaceText(text, System.currentTimeMillis()) }) - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ui/MessagesPanel.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ui/MessagesPanel.kt deleted file mode 100644 index 769e7fe8ce..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ui/MessagesPanel.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.sourcegraph.cody.chat.ui - -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.keymap.KeymapUtil -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.VerticalFlowLayout -import com.intellij.util.concurrency.annotations.RequiresEdt -import com.sourcegraph.cody.agent.protocol.ChatMessage -import com.sourcegraph.cody.agent.protocol.Speaker -import com.sourcegraph.cody.chat.ChatSession -import com.sourcegraph.cody.chat.ChatUIConstants -import com.sourcegraph.cody.vscode.CancellationToken -import com.sourcegraph.common.CodyBundle -import javax.swing.JPanel - -class MessagesPanel(private val project: Project, private val chatSession: ChatSession) : - JPanel(VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 0, true, true)) { - init { - val welcomeText = CodyBundle.getString("messages-panel.welcome-text") - val regex = Regex("\\{action\\.([^}]+)}") - val matches = regex.findAll(welcomeText) - val shortcutKeys = matches.map { it.groups[1]?.value ?: "" }.toList() - val finalWelcomeText = - shortcutKeys.fold(welcomeText) { acc, shortcutKey -> - acc.replace("{action.$shortcutKey}", KeymapUtil.getShortcutText(shortcutKey)) - } - addChatMessageAsComponent(ChatMessage(Speaker.ASSISTANT, finalWelcomeText)) - } - - @RequiresEdt - fun addOrUpdateMessage(message: ChatMessage, index: Int) { - val indexAfterHelloMessage = index + 1 - val messageToUpdate = - components.getOrNull(indexAfterHelloMessage).let { it as? ChatMessageWrapper } - - if (message.speaker == Speaker.ASSISTANT) { - removeBlinkingCursor() - } - - if (messageToUpdate != null) { - messageToUpdate.singleMessagePanel.updateContentWith(message.actualMessage()) - messageToUpdate.contextFilesPanel.updateContentWith(message.contextFiles) - } else { - addChatMessageAsComponent(message) - } - - if (message.speaker == Speaker.ASSISTANT && message.actualMessage().isBlank()) { - add(BlinkingCursorComponent.instance) - } - - revalidate() - repaint() - } - - @RequiresEdt - fun removeBlinkingCursor() { - components.find { it is BlinkingCursorComponent }?.let { remove(it) } - } - - fun registerCancellationToken(cancellationToken: CancellationToken) { - cancellationToken.onFinished { - ApplicationManager.getApplication().invokeLater { - removeBlinkingCursor() - getLastMessage()?.onPartFinished() - } - } - } - - @RequiresEdt - fun addChatMessageAsComponent(message: ChatMessage) { - val singleMessagePanel = - SingleMessagePanel( - message, project, this, ChatUIConstants.ASSISTANT_MESSAGE_GRADIENT_WIDTH, chatSession) - val contextFilesPanel = ContextFilesPanel(project, message) - add(ChatMessageWrapper(singleMessagePanel, contextFilesPanel)) - } - - private class ChatMessageWrapper( - val singleMessagePanel: SingleMessagePanel, - val contextFilesPanel: ContextFilesPanel - ) : JPanel() { - init { - add(singleMessagePanel) - add(contextFilesPanel) - layout = VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 0, true, false) - } - } - - private fun getLastMessage(): SingleMessagePanel? { - val lastPanel = components.last() as? JPanel - return lastPanel?.getComponent(0) as? SingleMessagePanel - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ui/PanelWithGradientBorder.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ui/PanelWithGradientBorder.kt deleted file mode 100644 index f1b56a4620..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ui/PanelWithGradientBorder.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.sourcegraph.cody.chat.ui - -import com.intellij.ide.ui.LafManagerListener -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.ui.VerticalFlowLayout -import com.intellij.ui.ColorUtil -import com.intellij.util.ui.JBUI -import com.intellij.util.ui.UIUtil -import com.sourcegraph.cody.agent.protocol.Speaker -import com.sourcegraph.cody.ui.Colors -import java.awt.GradientPaint -import java.awt.Graphics -import java.awt.Graphics2D -import javax.swing.BorderFactory -import javax.swing.JPanel -import javax.swing.border.Border - -open class PanelWithGradientBorder(private val gradientWidth: Int, speaker: Speaker) : JPanel() { - - private val isHuman: Boolean = speaker == Speaker.HUMAN - - init { - computeLayout() - - ApplicationManager.getApplication() - .messageBus - .connect() - .subscribe(LafManagerListener.TOPIC, LafManagerListener { computeLayout() }) - } - - private fun computeLayout() { - val panelBackground = UIUtil.getPanelBackground() - val separatorForeground = JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground() - val topBorder: Border = BorderFactory.createMatteBorder(1, 0, 0, 0, separatorForeground) - val bottomBorder: Border = - BorderFactory.createMatteBorder(0, 0, 1, 0, ColorUtil.brighter(separatorForeground, 1)) - val topAndBottomBorder: Border = BorderFactory.createCompoundBorder(topBorder, bottomBorder) - val emptyBorder = BorderFactory.createEmptyBorder(0, 0, 0, 0) - - this.border = if (isHuman) emptyBorder else topAndBottomBorder - this.layout = VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 0, true, false) - this.background = if (isHuman) ColorUtil.darker(panelBackground, 2) else panelBackground - } - - override fun paintComponent(g: Graphics) { - super.paintComponent(g) - paintLeftBorderGradient(g) - } - - private fun paintLeftBorderGradient(g: Graphics) { - if (isHuman) return - val halfOfHeight = height / 2 - val firstPartGradient = - GradientPaint(0f, 0f, Colors.PURPLE, 0f, halfOfHeight.toFloat(), Colors.ORANGE) - val secondPartGradient = - GradientPaint(0f, halfOfHeight.toFloat(), Colors.ORANGE, 0f, height.toFloat(), Colors.CYAN) - val g2d = g as Graphics2D - g2d.paint = firstPartGradient - g2d.fillRect(0, 0, gradientWidth, halfOfHeight) - g2d.paint = secondPartGradient - g2d.fillRect(0, halfOfHeight, gradientWidth, height) - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ui/SendButton.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ui/SendButton.kt deleted file mode 100644 index ab150dd860..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ui/SendButton.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.sourcegraph.cody.chat.ui - -import com.sourcegraph.cody.Icons -import java.awt.Dimension -import javax.swing.JButton - -class SendButton : JButton(Icons.Actions.Send) { - - init { - isContentAreaFilled = false - isBorderPainted = false - isEnabled = false - preferredSize = Dimension(32, 32) - toolTipText = "Send message" - disabledIcon = Icons.Actions.DisabledSend - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ui/SingleMessagePanel.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ui/SingleMessagePanel.kt deleted file mode 100644 index 2d4228a211..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ui/SingleMessagePanel.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.sourcegraph.cody.chat.ui - -import com.intellij.openapi.project.Project -import com.intellij.util.ui.SwingHelper -import com.sourcegraph.cody.agent.protocol.ChatMessage -import com.sourcegraph.cody.attribution.AttributionListener -import com.sourcegraph.cody.attribution.AttributionSearchCommand -import com.sourcegraph.cody.chat.ChatSession -import com.sourcegraph.cody.chat.CodeEditorFactory -import com.sourcegraph.cody.chat.MessageContentCreatorFromMarkdownNodes -import com.sourcegraph.cody.chat.extractCodeAndLanguage -import com.sourcegraph.cody.chat.findNodeAfterLastCodeBlock -import com.sourcegraph.cody.chat.isCodeBlock -import com.sourcegraph.cody.ui.HtmlViewer.createHtmlViewer -import com.sourcegraph.telemetry.GraphQlLogger -import javax.swing.JEditorPane -import javax.swing.JPanel -import org.commonmark.ext.gfm.tables.TablesExtension -import org.commonmark.node.Node -import org.commonmark.parser.Parser -import org.commonmark.renderer.html.HtmlRenderer - -class SingleMessagePanel( - private val chatMessage: ChatMessage, - private val project: Project, - private val parentPanel: JPanel, - private val gradientWidth: Int, - private val chatSession: ChatSession, -) : PanelWithGradientBorder(gradientWidth, chatMessage.speaker) { - private var lastMessagePart: MessagePart? = null - private var lastTrimmedText = "" - - init { - val markdownNodes: Node = markdownParser.parse(chatMessage.actualMessage()) - markdownNodes.accept(MessageContentCreatorFromMarkdownNodes(this, htmlRenderer)) - } - - fun updateContentWith(text: String) { - val trimmedText = text.trimEnd { c -> c == '`' || c.isWhitespace() } - val isGrowing = - trimmedText.contains(lastTrimmedText) && trimmedText.length > lastTrimmedText.length - if (isGrowing) { - lastTrimmedText = trimmedText - val markdownNodes = markdownParser.parse(text) - val lastMarkdownNode = markdownNodes.lastChild - if (lastMarkdownNode != null && lastMarkdownNode.isCodeBlock()) { - val (code, language) = lastMarkdownNode.extractCodeAndLanguage() - addOrUpdateCode(code, language) - } else { - val nodesAfterLastCodeBlock = markdownNodes.findNodeAfterLastCodeBlock() - val renderedHtml = htmlRenderer.render(nodesAfterLastCodeBlock) - addOrUpdateText(renderedHtml) - } - } - } - - fun addOrUpdateCode(code: String, language: String?) { - val lastPart = lastMessagePart - if (lastPart is CodeEditorPart) { - lastPart.updateCode(project, code, language) - } else { - // For completeness of [onPartFinished] semantics. - // At this point the implementation only considers - // lastMessagePart if it is CodeEditorPart, so this - // is always no-op. - onPartFinished() - addAsNewCodeComponent(code, language) - } - } - - private fun addAsNewCodeComponent(code: String, info: String?) { - val codeEditorComponent = - CodeEditorFactory(project, parentPanel, gradientWidth).createCodeEditor(code, info) - this.lastMessagePart = codeEditorComponent - add(codeEditorComponent.component) - } - - fun addOrUpdateText(text: String) { - val lastPart = lastMessagePart - if (lastPart is TextPart) { - lastPart.updateText(text) - } else { - onPartFinished() - addAsNewTextComponent(text) - } - } - - private fun addAsNewTextComponent(renderedHtml: String) { - val textPane: JEditorPane = createHtmlViewer(project) - SwingHelper.setHtml(textPane, renderedHtml, null) - val textEditorComponent = TextPart(textPane) - this.lastMessagePart = textEditorComponent - add(textEditorComponent.component) - } - - /** - * Trigger attribution search if the part that finished is a code snippet. - * - * Call sites should include: - * - including new text component after writing a code snippet (triggers attribution search - * mid-chat message). - * - including new code component after writing a text snippet (no-op because the implementation - * only considers [CodeEditorPart] [lastMessagePart], but added for completeness of - * [onPartFinished] semantics. - * - in a cancellation token callback in [MessagesPanel] (triggering attribution search if code - * snippet is the final part as well as if Cody's typing is cancelled. - */ - fun onPartFinished() { - val lastPart = lastMessagePart - if (lastPart is CodeEditorPart) { - chatSession.getConnectionId()?.let { connectionId -> - val listener = AttributionListener.UiThreadDecorator(lastPart.attribution) - AttributionSearchCommand(project).onSnippetFinished(lastPart.text, connectionId, listener) - } - - GraphQlLogger.logCodeGenerationEvent(project, "chatResponse", "hasCode", lastPart.text) - } - } - - companion object { - private val extensions = listOf(TablesExtension.create()) - - private val markdownParser = Parser.builder().extensions(extensions).build() - private val htmlRenderer = - HtmlRenderer.builder().softbreak("
").extensions(extensions).build() - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/commands/CommandId.kt b/src/main/kotlin/com/sourcegraph/cody/commands/CommandId.kt index 06d712f28c..cc02376ce7 100644 --- a/src/main/kotlin/com/sourcegraph/cody/commands/CommandId.kt +++ b/src/main/kotlin/com/sourcegraph/cody/commands/CommandId.kt @@ -1,8 +1,6 @@ package com.sourcegraph.cody.commands -import java.awt.event.KeyEvent - -enum class CommandId(val id: String, val displayName: String, val mnemonic: Int) { - Explain("cody.command.Explain", "Explain Code", KeyEvent.VK_E), - Smell("cody.command.Smell", "Smell Code", KeyEvent.VK_S), +enum class CommandId(val id: String, val displayName: String) { + Explain("cody.command.Explain", "Explain Code"), + Smell("cody.command.Smell", "Smell Code"), } diff --git a/src/main/kotlin/com/sourcegraph/cody/context/EnterpriseEnhancedContextStateController.kt b/src/main/kotlin/com/sourcegraph/cody/context/EnterpriseEnhancedContextStateController.kt deleted file mode 100644 index 53148c51a8..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/context/EnterpriseEnhancedContextStateController.kt +++ /dev/null @@ -1,345 +0,0 @@ -package com.sourcegraph.cody.context - -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.project.Project -import com.sourcegraph.cody.agent.EnhancedContextContextT -import com.sourcegraph.cody.agent.protocol.Repo -import com.sourcegraph.cody.context.RemoteRepoUtils.getRepositories -import com.sourcegraph.cody.context.ui.MAX_REMOTE_REPOSITORY_COUNT -import com.sourcegraph.cody.history.state.EnhancedContextState -import com.sourcegraph.cody.history.state.RemoteRepositoryState -import com.sourcegraph.vcs.CodebaseName -import java.util.concurrent.TimeUnit - -// The ephemeral, in-memory model of enterprise enhanced context state. -private class EnterpriseEnhancedContextModel { - // What the user actually wrote - @Volatile var rawSpec: String = "" - - // `rawSpec` after parsing and de-duping. This defines the order in which to display repositories. - var specified: Set = emptySet() - - // The names of repositories that have been manually deselected. - var manuallyDeselected: Set = emptySet() - - // What the Agent told us it is using for context. - var configured: List = emptyList() - - // Any repository we ever resolved. Used when re-selecting a de-selected repository without - // re-resolving. - val resolvedCache: MutableMap = mutableMapOf() -} - -/** - * Provides the [EnterpriseEnhancedContextStateController] access to chat's representation of - * enhanced context state. There are THREE representations: - * - JetBrains Cody has a bespoke representation saved in its chat history. This is divorced from - * the TypeScript extension's saved chat history :shrug: - * - The agent has a set of repositories that are actually used for enhanced context. This set can - * be read and written, however the agent may add a repository it has picked up and included - * automatically by examining the project. - * - The chat sidebar UI presents a view of enhanced context to the user. (Including a text field in - * a popup, however that is only *read* by the controller so does not appear here--see - * [EnterpriseEnhancedContextStateController.updateRawSpec].) - */ -interface ChatEnhancedContextStateProvider { - /** Updates JetBrains Cody's "chat history" copy of enhanced context state. */ - fun updateSavedState(updater: (EnhancedContextState) -> Unit) - - /** Updates the Agent-side state for the chat. */ - fun updateAgentState(repos: List) - - /** Pushes a UI update to the chat side panel. */ - fun updateUI(repos: List) - - /** Displays a message that remote repository resolution failed. */ - fun notifyRemoteRepoResolutionFailed() - - /** Displays a message that the user has reached the maximum number of remote repositories. */ - fun notifyRemoteRepoLimit() -} - -/** - * Reconciles the multiple, asynchronously updated copies of enhanced context state. - * - * Changes follow this flow: - * 1. A chat is restored ([loadFromChatState]) which synthesizes a [rawSpec] and a - * `model.manuallyDeselected` set. - * 2. When the raw spec is updated, we parse it and produce a "speculative set of repos" - * ([updateSpeculativeRepos]) These have not been resolved by the backend and may be totally - * bogus. - * 3. When the speculative repos are resolved ([onResolvedRepos]) we can filter the - * `model.manuallyDeselected` ones and request the Agent to focus on a set of repositories - * (`chat.updateAgentState`). - * 4. When the Agent has updated its state ([onAgentStateUpdated]) we finally learn which - * repositories are actually used, whether a repository is implicitly included by the Agent based - * on the project, and whether a repository is filtered by Context Filters. - * 5. Finally, we can [updateUI]. - * - * When the user updates the raw spec, the same process happens from step 2 to step 4, however we - * also `chat.updateSavedState` to save the changes to the JetBrains-side copy of chat history. - * - * When the user checks and unchecks repositories, we already have all the resolved repository - * details. We just update the JetBrains-side copy of chat history (`chat.updateSavedState`) and do - * the `chat.updateAgentState` -> [onAgentStateUpdated] flow. - */ -class EnterpriseEnhancedContextStateController( - val project: Project, - val chat: ChatEnhancedContextStateProvider -) { - private val logger = Logger.getInstance(EnterpriseEnhancedContextStateController::class.java) - private val model_ = EnterpriseEnhancedContextModel() - private var epoch = 0 - - val rawSpec: String - get(): String = model_.rawSpec - - private fun withModel(f: (EnterpriseEnhancedContextModel) -> T): T { - assert(!ApplicationManager.getApplication().isDispatchThread) { - "Must not use model from EDT, it may block" - } - synchronized(model_) { - return f(model_) - } - } - - /** - * Loads the set of repositories from the JetBrains-side copy of chat history and starts the - * process of resolving the mentioned repositories, configuring Agent to use them, and eventually - * updating the UI. - */ - fun loadFromChatState(remoteRepositories: List?) { - val cleanedRepos = - remoteRepositories?.filter { it.codebaseName != null }?.toSet()?.toList() ?: emptyList() - - // Start trying to resolve these cached repos. Note, we try to resolve everything, even - // deselected repos. - ApplicationManager.getApplication().executeOnPooledThread { - // Remember which repositories have been manually deselected. - withModel { model -> - model.rawSpec = cleanedRepos.map { it.codebaseName }.joinToString("\n") - model.manuallyDeselected = - cleanedRepos.filter { !it.isEnabled }.mapNotNull { it.codebaseName }.toSet() - } - - updateSpeculativeRepos(cleanedRepos.mapNotNull { it.codebaseName }) - } - } - - /** - * Updates the text spec of the repository list when it is edited by the user. This does not reset - * the manually deselected set because the user may have edited an unrelated part of the spec. - * However, if a repository is removed from the spec, we remove it from the manually deselected - * set for it to be selected by default if it is re-added later. This saves the updated repository - * list to the JetBrains-side copy of chat history. - */ - fun updateRawSpec(newSpec: String) { - val speculative = withModel { model -> - model.rawSpec = newSpec - val speculative = newSpec.split(Regex("""\s+""")).filter { it != "" }.toSet().toList() - - // If a repository name has been removed from the list of speculative repos, then forget that - // it was manually deselected in order for it to be default selected if it is added back. - - // TODO: Improve the accuracy of removals when there's an Agent API that maps specified name - // -> - // resolved name. - // Today we only have names go in and a set of repositories come out, in different - // (alphabetical) order. - model.manuallyDeselected = - model.manuallyDeselected.filter { speculative.contains(it) }.toSet() - speculative - } - updateSpeculativeRepos(speculative) - } - - // Builds the initial list of repositories and kicks off the process of resolving them. - private fun updateSpeculativeRepos(repos: List) { - assert(!ApplicationManager.getApplication().isDispatchThread) { - "updateSpeculativeRepos should not be used on EDT, it may block" - } - - var thisEpoch = - synchronized(this) { - withModel { model -> model.specified = repos.toSet() } - ++epoch - } - - // Consult the repo resolution cache. - val resolved = mutableSetOf() - val toResolve = mutableSetOf() - withModel { model -> - for (repo in repos) { - val cached = model.resolvedCache[repo] - when { - cached == null -> toResolve.add(repo) - else -> resolved.add(cached) - } - } - } - - // Remotely resolve the repositories that we couldn't resolve locally. - if (toResolve.size > 0) { - val newlyResolvedRepos = - getRepositories(project, toResolve.map { CodebaseName(it) }.toList()) - .completeOnTimeout(emptyList(), 15, TimeUnit.SECONDS) - .get() - - // Update the cache of resolved repositories. - withModel { model -> model.resolvedCache.putAll(newlyResolvedRepos.associateBy { it.name }) } - - resolved.addAll(newlyResolvedRepos) - } - - synchronized(this) { - if (epoch != thisEpoch) { - // We've kicked off another update in the meantime, so run with that one. - return - } - if (repos.isNotEmpty() && resolved.isEmpty()) { - chat.notifyRemoteRepoResolutionFailed() - return - } - updateSavedState() - onResolvedRepos(resolved.toList()) - } - } - - private fun onResolvedRepos(repos: List) { - var resolvedRepos = repos.associateBy { repo -> repo.name } - - // Update the Agent state. This eventually produces `updateFromAgent` which triggers the tree - // view update. - val reposToSendToAgent = withModel { model -> - model.specified - .mapNotNull { repoSpecName -> resolvedRepos[repoSpecName] } - .filter { !model.manuallyDeselected.contains(it.name) } - .take(MAX_REMOTE_REPOSITORY_COUNT) - } - chat.updateAgentState(reposToSendToAgent) - } - - fun updateFromAgent(enhancedContextStatus: EnhancedContextContextT) { - // Collect the configured repositories from the Agent reported state. - val repos = mutableListOf() - - for (group in enhancedContextStatus.groups) { - val provider = group.providers.firstOrNull() ?: continue - val name = group.displayName - val id = provider.id ?: continue - val enablement = - when { - provider.state == "ready" -> RepoSelectionStatus.SELECTED - else -> RepoSelectionStatus.DESELECTED - } - val ignored = provider.isIgnored == true - val inclusion = - when (provider.inclusion) { - "auto" -> RepoInclusion.AUTO - "manual" -> RepoInclusion.MANUAL - else -> RepoInclusion.MANUAL - } - repos.add(RemoteRepo(name, id, enablement, isIgnored = ignored, inclusion)) - } - - withModel { model -> model.configured = repos } - updateUI() - } - - private fun updateUI() { - val usedRepos: MutableMap = mutableMapOf() - val repos = mutableListOf() - - withModel { model -> - // Compute the merged representation of repositories. - usedRepos.putAll(model.configured.associateBy { it.name }) - - // Visit the repositories in the order specified by the user. - repos.addAll( - model.specified.map { - usedRepos.getOrDefault( - it, - // If the repo was manually deselected, then we show it as de-selected. - // The repo was not manually deselected, yet isn't in the configured repos, hence it - // is not found. - // TODO: We could speculatively consult Cody Ignore to see if the deselected repo - // *would* have been ignored. - RemoteRepo( - it, - null, - if (model.manuallyDeselected.contains(it)) { - RepoSelectionStatus.DESELECTED - } else { - RepoSelectionStatus.NOT_FOUND - }, - isIgnored = false, - RepoInclusion.MANUAL)) - }) - } - - // Finally, if there are any remaining repos configured by the agent which are not used, - // represent them now. - repos.addAll(usedRepos.values.filter { !repos.contains(it) }) - - // ...and push the list to the UI. - chat.updateUI(repos) - } - - fun setRepoEnabledInContextState(repoName: String, enabled: Boolean) { - withModel { model -> - val atLimit = model.configured.count { it.isEnabled } >= MAX_REMOTE_REPOSITORY_COUNT - val repos = model.configured.map { Repo(it.name, it.id!!) }.toMutableList() - - if (enabled) { - if (atLimit) { - chat.notifyRemoteRepoLimit() - return@withModel - } - model.manuallyDeselected = model.manuallyDeselected.filter { it != repoName }.toSet() - val repoToAdd = synchronized(model.resolvedCache) { model.resolvedCache[repoName] } - if (repoToAdd == null) { - logger.warn("failed to find repo $repoName in the resolved cache; will not enable it") - return@withModel - } - repos.add(repoToAdd) - } else { - model.manuallyDeselected = model.manuallyDeselected.plus(repoName) - repos.removeIf { it.name == repoName } - } - updateSavedState() - - // Update the Agent state. This eventually produces `updateFromAgent` which triggers the tree - // view update. - chat.updateAgentState(repos) - } - } - - // Pushes a state update to the JetBrains chat history copy of the enhanced context state. This - // simply takes - // whatever the user specified (`model.specified`) and saves it, along with which repos were - // deselected - // (`model.manuallyDeselected`). - private fun updateSavedState() { - val reposToWriteToState = withModel { model -> - model.specified.map { repoSpecName -> - RemoteRepositoryState().apply { - codebaseName = repoSpecName - // Note, we don't limit to MAX_REMOTE_REPOSITORY_COUNT here. We may raise or lower - // that limit in future versions anyway, so we just record what is manually deselected - // and apply the limit when updating Agent-side state. - isEnabled = !model.manuallyDeselected.contains(repoSpecName) - } - } - } - - chat.updateSavedState { state -> - state.remoteRepositories.clear() - state.remoteRepositories.addAll(reposToWriteToState) - } - } - - fun requestUIUpdate() { - ApplicationManager.getApplication().executeOnPooledThread(this::updateUI) - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/context/RemoteRepoInsight.kt b/src/main/kotlin/com/sourcegraph/cody/context/RemoteRepoInsight.kt deleted file mode 100644 index f409cbd5fd..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/context/RemoteRepoInsight.kt +++ /dev/null @@ -1,435 +0,0 @@ -package com.sourcegraph.cody.context - -import com.intellij.codeInsight.completion.CompletionContributor -import com.intellij.codeInsight.completion.CompletionParameters -import com.intellij.codeInsight.completion.CompletionProvider -import com.intellij.codeInsight.completion.CompletionResultSet -import com.intellij.codeInsight.completion.CompletionType -import com.intellij.codeInsight.lookup.LookupElementBuilder -import com.intellij.extapi.psi.ASTWrapperPsiElement -import com.intellij.extapi.psi.PsiFileBase -import com.intellij.lang.ASTNode -import com.intellij.lang.Language -import com.intellij.lang.ParserDefinition -import com.intellij.lang.PsiParser -import com.intellij.lang.annotation.AnnotationHolder -import com.intellij.lang.annotation.Annotator -import com.intellij.lang.annotation.HighlightSeverity -import com.intellij.lexer.Lexer -import com.intellij.lexer.LexerPosition -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.fileTypes.FileType -import com.intellij.openapi.fileTypes.LanguageFileType -import com.intellij.openapi.progress.blockingContext -import com.intellij.openapi.progress.runBlockingCancellable -import com.intellij.openapi.project.DumbAware -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.TextRange -import com.intellij.patterns.PlatformPatterns -import com.intellij.psi.FileViewProvider -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.tree.IElementType -import com.intellij.psi.tree.IFileElementType -import com.intellij.psi.tree.TokenSet -import com.intellij.psi.util.elementType -import com.intellij.refactoring.suggested.endOffset -import com.intellij.refactoring.suggested.startOffset -import com.intellij.util.ProcessingContext -import com.jetbrains.rd.util.CancellationException -import com.jetbrains.rd.util.getThrowableText -import com.sourcegraph.Icons -import com.sourcegraph.cody.context.ui.MAX_REMOTE_REPOSITORY_COUNT -import com.sourcegraph.common.CodyBundle -import com.sourcegraph.common.CodyBundle.fmt -import javax.swing.Icon -import org.jetbrains.annotations.NonNls - -enum class RepoInclusion { - AUTO, - MANUAL, -} - -enum class RepoSelectionStatus { - /** The user manually deselected the repository. */ - DESELECTED, - - /** Remote repo search did not find the repo (so it is disabled.) */ - NOT_FOUND, - - /** The repo has been found and is selected. */ - SELECTED, -} - -data class RemoteRepo( - val name: String, - /** - * Null in the case of "not found" repos, or manually deselected repos we did not try to find. - */ - val id: String?, - val selectionStatus: RepoSelectionStatus, - val isIgnored: Boolean, - val inclusion: RepoInclusion, -) { - val isEnabled: Boolean - get() = selectionStatus == RepoSelectionStatus.SELECTED && !isIgnored - - val displayName: String - get() = name.substring(name.indexOf('/') + 1) // Note, works for names without / => full name. - - val icon: Icon - get() = - when { - isIgnored -> Icons.RepoIgnored - else -> iconForName(name) - } - - companion object { - fun iconForName(name: String): Icon { - return when { - name.startsWith("github.com/") -> Icons.RepoHostGitHub - name.startsWith("gitlab.com/") -> Icons.RepoHostGitlab - name.startsWith("bitbucket.org/") -> Icons.RepoHostBitbucket - else -> Icons.RepoHostGeneric - } - } - } -} - -val RemoteRepoLanguage = object : Language("SourcegraphRemoteRepoList") {} - -class RemoteRepoFileType : LanguageFileType(RemoteRepoLanguage) { - companion object { - @JvmStatic val INSTANCE = RemoteRepoFileType() - } - - override fun getName(): String { - return "SourcegraphRemoteRepoListFile" - } - - override fun getDescription(): String { - return "A list of Sourcegraph repository indexes" - } - - override fun getDefaultExtension(): String { - return "" - } - - override fun getIcon(): Icon? { - return null - } -} - -class RemoteRepoTokenType(debugName: @NonNls String) : IElementType(debugName, RemoteRepoLanguage) { - override fun toString(): String { - return "RemoteRepoTokenType." + super.toString() - } - - companion object { - val REPO = RemoteRepoTokenType("REPO") - val SEPARATOR = RemoteRepoTokenType("SEPARATOR") - val EOF = RemoteRepoTokenType("EOF") - } -} - -class RemoteRepoFile(viewProvider: FileViewProvider) : - PsiFileBase(viewProvider, RemoteRepoLanguage) { - override fun getFileType(): FileType { - return RemoteRepoFileType.INSTANCE - } - - override fun toString(): String { - return "Sourcegraph Remote Repo File" - } -} - -private enum class LexerState(val value: Int) { - IN_REPO(1), - IN_SEPARATOR(2), - EOF(3); - - companion object { - fun fromInt(value: Int): LexerState? = values().find { it.value == value } - } -} - -internal class RemoteRepoListParserDefinition : ParserDefinition { - override fun createLexer(project: Project): Lexer { - return object : Lexer() { - var buffer: CharSequence = "" - var startOffset: Int = 0 - var endOffset: Int = 0 - var state: LexerState = LexerState.EOF - var offset: Int = 0 - - override fun start( - buffer: CharSequence, - startOffset: Int, - endOffset: Int, - initialState: Int - ) { - this.buffer = buffer - this.startOffset = startOffset - this.endOffset = endOffset - offset = startOffset - state = LexerState.fromInt(initialState) ?: stateAtOffset() - } - - override fun getState(): Int { - return this.state.value - } - - override fun getTokenType(): IElementType? { - return when (state) { - LexerState.IN_REPO -> RemoteRepoTokenType.REPO - LexerState.IN_SEPARATOR -> RemoteRepoTokenType.SEPARATOR - LexerState.EOF -> null - } - } - - override fun getTokenStart(): Int { - return this.offset - } - - override fun getTokenEnd(): Int { - return when (tokenType) { - RemoteRepoTokenType.REPO, - RemoteRepoTokenType.SEPARATOR -> { - val index = - buffer.subSequence(offset, buffer.length).indexOfFirst { ch -> - if (tokenType == RemoteRepoTokenType.REPO) { - ch.isWhitespace() - } else { - !ch.isWhitespace() - } - } - if (index == -1) { - buffer.length - } else { - offset + index - } - } - RemoteRepoTokenType.EOF -> return buffer.length - else -> throw RuntimeException("unexpected token type $tokenType lexing repo list") - } - } - - override fun advance() { - this.offset = this.tokenEnd - this.state = stateAtOffset() - } - - fun stateAtOffset(): LexerState { - val ch = peekChar() - return when { - ch == null -> LexerState.EOF - ch.isWhitespace() -> LexerState.IN_SEPARATOR - else -> LexerState.IN_REPO - } - } - - fun peekChar(): Char? { - return if (offset == buffer.length) { - null - } else { - buffer[offset] - } - } - - override fun getCurrentPosition(): LexerPosition { - val snapState = this.state.value - val snapOffset = this.offset - - return object : LexerPosition { - override fun getOffset(): Int { - return snapOffset - } - - override fun getState(): Int { - return snapState - } - } - } - - override fun restore(position: LexerPosition) { - this.offset = position.offset - this.state = LexerState.fromInt(position.state) ?: stateAtOffset() - } - - override fun getBufferSequence(): CharSequence { - return buffer - } - - override fun getBufferEnd(): Int { - return endOffset - } - } - } - - override fun getCommentTokens(): TokenSet { - return TokenSet.EMPTY - } - - override fun getStringLiteralElements(): TokenSet { - return TokenSet.EMPTY - } - - override fun createParser(project: Project): PsiParser { - return PsiParser { root, builder -> - val repoList = builder.mark() - while (!builder.eof()) { - val tokenType = builder.tokenType - when (builder.tokenType) { - RemoteRepoTokenType.REPO -> { - val mark = builder.mark() - builder.advanceLexer() - mark.done(RemoteRepoTokenType.REPO) - } - RemoteRepoTokenType.SEPARATOR -> { - builder.advanceLexer() - } - else -> { - builder.error("Unexpected token type: $tokenType") - builder.advanceLexer() - } - } - } - repoList.done(root) - builder.treeBuilt - } - } - - override fun getFileNodeType(): IFileElementType { - return FILE - } - - override fun createFile(viewProvider: FileViewProvider): PsiFile { - return RemoteRepoFile(viewProvider) - } - - override fun createElement(node: ASTNode): PsiElement { - return ASTWrapperPsiElement(node) - } - - companion object { - val FILE: IFileElementType = IFileElementType(RemoteRepoLanguage) - } -} - -class RemoteRepoAnnotator : Annotator, DumbAware { - override fun annotate(element: PsiElement, holder: AnnotationHolder) { - // TODO: Messages/tooltips are not appearing on hover, but they *do* appear if the editor/popup - // is not focused. - // Debug how popups interact with tooltips and re-enable tooltips. - when (element.elementType) { - RemoteRepoTokenType.REPO -> { - val name = element.text - val service = RemoteRepoSearcher.getInstance(element.project) - runBlockingCancellable { - if (!service.cancellableHas(name)) { - blockingContext { - holder - .newAnnotation( - HighlightSeverity.ERROR, - CodyBundle.getString("context-panel.remote-repo.error-not-found")) - .tooltip(CodyBundle.getString("context-panel.remote-repo.error-not-found")) - .range(element) - .create() - } - } - } - } - RemoteRepoListParserDefinition.FILE -> { - val seen = mutableSetOf() - var firstTruncatedElement: PsiElement? = null - element.children - .filter { it.elementType == RemoteRepoTokenType.REPO } - .forEach { repo -> - val name = repo.text - if (seen.contains(name)) { - holder - .newAnnotation( - HighlightSeverity.WEAK_WARNING, - CodyBundle.getString( - "context-panel.remote-repo.error-duplicate-repository")) - .tooltip( - CodyBundle.getString( - "context-panel.remote-repo.error-duplicate-repository")) - .range(repo) - .create() - } else if (seen.size == MAX_REMOTE_REPOSITORY_COUNT) { - firstTruncatedElement = firstTruncatedElement ?: repo - } - seen.add(name) - } - if (firstTruncatedElement != null) { - holder - .newAnnotation( - HighlightSeverity.WARNING, - CodyBundle.getString("context-panel.remote-repo.error-too-many-repositories") - .fmt(MAX_REMOTE_REPOSITORY_COUNT.toString())) - .tooltip( - CodyBundle.getString( - "context-panel.remote-repo.error-too-many-repositories.tooltip")) - .range(TextRange(firstTruncatedElement!!.startOffset, element.endOffset)) - .create() - } - } - } - } -} - -class RemoteRepoCompletionContributor : CompletionContributor(), DumbAware { - init { - extend( - CompletionType.BASIC, - PlatformPatterns.psiElement(), - object : CompletionProvider() { - override fun addCompletions( - parameters: CompletionParameters, - context: ProcessingContext, - result: CompletionResultSet - ) { - val searcher = RemoteRepoSearcher.getInstance(parameters.position.project) - // We use original position, if present, because it does not have the "helpful" dummy - // text "IntellijIdeaRulezzz". Because we do a fuzzy match, we use the whole element - // as the query. - val element = parameters.originalPosition - val query = - if (element?.elementType == RemoteRepoTokenType.REPO) { - element.text - } else { - null // Return all repos - } - // Update the prefix to the whole query to get accurate highlighting. - val prefixedResult = - if (query != null) { - result.withPrefixMatcher(query) - } else { - result - } - prefixedResult.restartCompletionOnAnyPrefixChange() - try { - // TODO: Extend repo search to consult Cody Ignore and denote repositories that are - // ignored. - for (repo in searcher.cancellableSearch(query)) { - prefixedResult - .caseInsensitive() - .addElement( - LookupElementBuilder.create(repo).withIcon(RemoteRepo.iconForName(repo))) - } - } catch (e: Exception) { - if (e is CancellationException) { - throw e - } else { - prefixedResult.addLookupAdvertisement(e.getThrowableText()) - } - } - } - }) - } - - override fun handleEmptyLookup(parameters: CompletionParameters, editor: Editor?): String { - return CodyBundle.getString("context-panel.remote-repo.contact-admin-advertisement") - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/context/RemoteRepoSearcher.kt b/src/main/kotlin/com/sourcegraph/cody/context/RemoteRepoSearcher.kt deleted file mode 100644 index e924fbdf6e..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/context/RemoteRepoSearcher.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.sourcegraph.cody.context - -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.project.Project -import com.sourcegraph.cody.agent.CodyAgentException -import com.sourcegraph.cody.agent.CodyAgentService -import com.sourcegraph.cody.agent.protocol.* -import java.util.concurrent.CompletableFuture -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException - -@Service(Service.Level.PROJECT) -class RemoteRepoSearcher(private val project: Project) { - companion object { - fun getInstance(project: Project): RemoteRepoSearcher { - return project.service() - } - } - - private val logger = Logger.getInstance(RemoteRepoSearcher::class.java) - - fun cancellableHas(repoName: String): Boolean { - val result = has(repoName) - while (true) { - ProgressManager.checkCanceled() - try { - return result.get(10, TimeUnit.MILLISECONDS) - } catch (e: TimeoutException) { - // ignore - } - } - } - - /** Gets whether `repoName` is a known remote repo. */ - fun has(repoName: String): CompletableFuture { - val result = CompletableFuture() - CodyAgentService.withAgent(project) { agent -> - agent.server.remoteRepoHas(RemoteRepoHasParams(repoName)).thenApply { - result.complete(it.result) - } - } - return result - } - - fun cancellableSearch(query: String?): List { - val result = search(query) - while (true) { - ProgressManager.checkCanceled() - try { - return result.get(10, TimeUnit.MILLISECONDS) - } catch (e: TimeoutException) { - // ignore - } - } - } - - fun search(query: String?): CompletableFuture> { - val result = CompletableFuture>() - val repos = mutableListOf() - CodyAgentService.withAgent(project) { agent -> - do { - val stepDone = CompletableFuture() - agent.server - .remoteRepoList( - RemoteRepoListParams( - query = query, - first = 500, - after = repos.lastOrNull()?.id, - )) - .thenApply { partialResult -> - if (partialResult.state.error != null) { - logger.warn( - "remote repository search had error: ${partialResult.state.error.title}") - if (partialResult.repos.isEmpty() && repos.isEmpty()) { - result.completeExceptionally(CodyAgentException(partialResult.state.error.title)) - stepDone.complete(false) - return@thenApply - } - } - logger.debug( - "remote repo search $query adding ${partialResult.repos.size} results (${partialResult.state.state})") - repos.addAll(partialResult.repos) - if (partialResult.state.state != "fetching") { - result.complete(repos.map { it.name }) - stepDone.complete(false) - return@thenApply - } - stepDone.complete(true) - } - } while (stepDone.get()) - } - return result - } - - private fun fetchDone(state: RemoteRepoFetchState): Boolean { - return state.state == "complete" || state.state == "errored" - } - - // Callbacks for CodyAgentService - fun remoteRepoDidChange() { - // Ignore this. `search` uses the earliest available result. - } - - fun remoteRepoDidChangeState(state: RemoteRepoFetchState) { - // No-op. - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/context/RemoteRepoUtils.kt b/src/main/kotlin/com/sourcegraph/cody/context/RemoteRepoUtils.kt deleted file mode 100644 index 7aaeb2f2da..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/context/RemoteRepoUtils.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.sourcegraph.cody.context - -import com.intellij.openapi.project.Project -import com.sourcegraph.cody.agent.CodyAgentService -import com.sourcegraph.cody.agent.protocol.Repo -import com.sourcegraph.cody.agent.protocol_generated.Graphql_GetRepoIdsParams -import com.sourcegraph.vcs.CodebaseName -import java.util.concurrent.CompletableFuture - -object RemoteRepoUtils { - /** - * Gets any repository IDs which match `codebaseNames`. If `codebaseNames` is empty, completes - * with an empty list. - */ - fun getRepositories( - project: Project, - codebaseNames: List - ): CompletableFuture> { - val result = CompletableFuture>() - if (codebaseNames.isEmpty()) { - result.complete(emptyList()) - return result - } - CodyAgentService.withAgent(project) { agent -> - try { - val param = - Graphql_GetRepoIdsParams(codebaseNames.map { it.value }, codebaseNames.size.toLong()) - val repos = agent.server.graphql_getRepoIds(param).get() - result.complete( - repos?.repos?.map { reposParams -> Repo(name = reposParams.name, id = reposParams.id) } - ?: emptyList()) - } catch (e: Exception) { - result.complete(emptyList()) - } - } - return result - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextNotifications.kt b/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextNotifications.kt deleted file mode 100644 index b8f4b2b524..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextNotifications.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.sourcegraph.cody.context.ui - -import com.intellij.ide.util.PropertiesComponent -import com.intellij.notification.Notification -import com.intellij.notification.NotificationAction -import com.intellij.notification.NotificationType -import com.intellij.notification.impl.NotificationFullContent -import com.intellij.openapi.actionSystem.AnActionEvent -import com.sourcegraph.Icons -import com.sourcegraph.common.CodyBundle -import com.sourcegraph.common.CodyBundle.fmt -import com.sourcegraph.common.NotificationGroups - -class RemoteRepoResolutionFailedNotification : - Notification( - NotificationGroups.SOURCEGRAPH_ERRORS, - CodyBundle.getString("context-panel.remote-repo.error-resolution-failed.title"), - CodyBundle.getString("context-panel.remote-repo.error-resolution-failed.detail"), - NotificationType.WARNING), - NotificationFullContent { - - init { - icon = Icons.RepoHostGeneric - - addAction( - object : - NotificationAction( - CodyBundle.getString( - "context-panel.remote-repo.error-resolution-failed.do-not-show-again")) { - override fun actionPerformed(event: AnActionEvent, notification: Notification) { - PropertiesComponent.getInstance().setValue(ignore, true) - notification.expire() - } - }) - } - - companion object { - val ignore = CodyBundle.getString("context-panel.remote-repo.error-resolution-failed.ignore") - } -} - -class RemoteRepoLimitNotification : - Notification( - NotificationGroups.SOURCEGRAPH_ERRORS, - CodyBundle.getString("context-panel.remote-repo.error-too-many-repositories.tooltip"), - CodyBundle.getString("context-panel.remote-repo.error-too-many-repositories") - .fmt(MAX_REMOTE_REPOSITORY_COUNT.toString()), - NotificationType.WARNING), - NotificationFullContent { - - init { - icon = Icons.RepoHostGeneric - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextRepositoriesCheckboxRenderer.kt b/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextRepositoriesCheckboxRenderer.kt deleted file mode 100644 index 180277e9ea..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextRepositoriesCheckboxRenderer.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.sourcegraph.cody.context.ui - -import com.intellij.openapi.application.ApplicationInfo -import com.intellij.ui.CheckboxTree -import com.intellij.ui.CheckedTreeNode -import com.intellij.ui.SimpleTextAttributes -import com.intellij.util.ui.ThreeStateCheckBox -import com.sourcegraph.cody.Icons -import com.sourcegraph.cody.chat.ui.pluralize -import com.sourcegraph.cody.context.RepoInclusion -import com.sourcegraph.cody.context.RepoSelectionStatus -import com.sourcegraph.common.CodyBundle -import com.sourcegraph.common.CodyBundle.fmt -import java.util.concurrent.atomic.AtomicBoolean -import javax.swing.JTree - -class ContextRepositoriesCheckboxRenderer(private val enhancedContextEnabled: AtomicBoolean) : - CheckboxTree.CheckboxTreeCellRenderer() { - - override fun customizeRenderer( - tree: JTree?, - node: Any?, - selected: Boolean, - expanded: Boolean, - leaf: Boolean, - row: Int, - hasFocus: Boolean - ) { - val style = - if (ApplicationInfo.getInstance().build.baselineVersion > 233) "style='color:#808080'" - else "" - - when (node) { - // Consumer context node renderers - is ContextTreeLocalRepoNode -> { - val projectPath = node.project.basePath?.replace(System.getProperty("user.home"), "~") - textRenderer.appendHTML( - "${node.project.name} ${projectPath}", - SimpleTextAttributes.REGULAR_ATTRIBUTES) - } - - // Enterprise context node renderers - - is ContextTreeEditReposNode -> { - toolTipText = "" - myCheckbox.isVisible = false - textRenderer.appendHTML( - CodyBundle.getString( - when { - node.hasRemovableRepos -> "context-panel.tree.node-edit-repos.label-edit" - else -> "context-panel.tree.node-edit-repos.label-add" - }) - .fmt(style), - SimpleTextAttributes.REGULAR_ATTRIBUTES) - textRenderer.icon = - when { - node.hasRemovableRepos -> Icons.Actions.Edit - else -> Icons.Actions.Add - } - } - is ContextTreeEnterpriseRootNode -> { - textRenderer.appendHTML( - CodyBundle.getString("context-panel.tree.node-chat-context.detailed") - .fmt( - style, - node.numActiveRepos.toString(), - "repository".pluralize(node.numActiveRepos)), - SimpleTextAttributes.REGULAR_ATTRIBUTES) - // The root element controls enhanced context which includes editor selection, etc. Do not - // display unchecked/bar even if the child repos are unchecked. - myCheckbox.state = - if (node.isChecked) { - ThreeStateCheckBox.State.SELECTED - } else { - ThreeStateCheckBox.State.NOT_SELECTED - } - toolTipText = "" - myCheckbox.toolTipText = "" - } - is ContextTreeRemoteRepoNode -> { - val isEnhancedContextEnabled = enhancedContextEnabled.get() - - textRenderer.appendHTML( - CodyBundle.getString("context-panel.tree.node-remote-repo.label") - .fmt( - style, - node.repo.name, - when { - // TODO: Handle missing remote repos with a "not found" string - node.repo.inclusion == RepoInclusion.AUTO && node.repo.isIgnored -> - CodyBundle.getString("context-panel.tree.node-remote-repo.auto-ignored") - node.repo.inclusion == RepoInclusion.AUTO -> - CodyBundle.getString("context-panel.tree.node-remote-repo.auto") - node.repo.isIgnored -> - CodyBundle.getString("context-panel.tree.node-remote-repo.ignored") - node.repo.selectionStatus == RepoSelectionStatus.NOT_FOUND -> - CodyBundle.getString("context-panel.tree.node-remote-repo.not-found") - else -> "" - }), - SimpleTextAttributes.REGULAR_ATTRIBUTES) - - textRenderer.icon = node.repo.icon - - toolTipText = - when { - node.repo.isIgnored -> CodyBundle.getString("context-panel.tree.node-ignored.tooltip") - node.repo.inclusion == RepoInclusion.AUTO -> - CodyBundle.getString("context-panel.tree.node-auto.tooltip") - else -> node.repo.name - } - myCheckbox.state = - when { - isEnhancedContextEnabled && node.repo.isEnabled && !node.repo.isIgnored -> - ThreeStateCheckBox.State.SELECTED - node.repo.isEnabled -> ThreeStateCheckBox.State.DONT_CARE - else -> ThreeStateCheckBox.State.NOT_SELECTED - } - myCheckbox.isEnabled = - isEnhancedContextEnabled && - node.repo.inclusion != RepoInclusion.AUTO && - node.repo.selectionStatus != RepoSelectionStatus.NOT_FOUND - myCheckbox.toolTipText = - when { - node.repo.inclusion == RepoInclusion.AUTO -> - CodyBundle.getString("context-panel.tree.node-auto.tooltip") - node.repo.selectionStatus == RepoSelectionStatus.NOT_FOUND -> - CodyBundle.getString("context-panel.tree.node-remote-repo.not-found") - else -> CodyBundle.getString("context-panel.tree.node.checkbox.remove-tooltip") - } - } - - // Fallback - is CheckedTreeNode -> { - textRenderer.appendHTML( - "${node.userObject}", SimpleTextAttributes.REGULAR_ATTRIBUTES) - } - } - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextToolbarButton.kt b/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextToolbarButton.kt deleted file mode 100644 index f35343178a..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextToolbarButton.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.sourcegraph.cody.context.ui - -import com.intellij.openapi.actionSystem.ActionUpdateThread -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.ui.DumbAwareActionButton -import javax.swing.Icon - -open class ContextToolbarButton( - name: String, - icon: Icon, - private val buttonAction: () -> Unit = {} -) : DumbAwareActionButton(name, icon) { - - fun getActionUpdateThread(): ActionUpdateThread { - return ActionUpdateThread.EDT - } - - override fun isDumbAware(): Boolean = true - - override fun actionPerformed(p0: AnActionEvent) { - buttonAction() - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextTreeNode.kt b/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextTreeNode.kt deleted file mode 100644 index 6853b293a2..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextTreeNode.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.sourcegraph.cody.context.ui - -import com.intellij.openapi.project.Project -import com.intellij.ui.CheckedTreeNode -import com.sourcegraph.cody.context.RemoteRepo -import java.util.concurrent.atomic.AtomicBoolean - -open class ContextTreeNode(value: T, private val onSetChecked: (Boolean) -> Unit = {}) : - CheckedTreeNode(value) { - override fun setChecked(checked: Boolean) { - super.setChecked(checked) - onSetChecked(checked) - } -} - -class ContextTreeRootNode(val text: String, onSetChecked: (Boolean) -> Unit) : - ContextTreeNode(text, onSetChecked) - -open class ContextTreeLocalNode(value: T, private val isEnhancedContextEnabled: AtomicBoolean) : - ContextTreeNode(value) { - init { - this.isEnabled = false - } - - override fun isChecked(): Boolean = isEnhancedContextEnabled.get() -} - -class ContextTreeLocalRootNode(val text: String, isEnhancedContextEnabled: AtomicBoolean) : - ContextTreeLocalNode(text, isEnhancedContextEnabled) - -class ContextTreeLocalRepoNode(val project: Project, isEnhancedContextEnabled: AtomicBoolean) : - ContextTreeLocalNode(project, isEnhancedContextEnabled) - -/** Enterprise context selector tree, root node. */ -open class ContextTreeEnterpriseRootNode(var numActiveRepos: Int, onSetChecked: (Boolean) -> Unit) : - ContextTreeNode( - Object(), onSetChecked) // TreePaths depend on user objects; Object() ensures uniqueness. - -// TODO: Can we remove onActivate if we remove the toolbar? -/** Enterprise context selector tree, a node to trigger editing the repository list. */ -class ContextTreeEditReposNode(var hasRemovableRepos: Boolean, val onActivate: () -> Unit) : - ContextTreeNode(Object()) - -/** Enterprise context selector tree, a specific remote repository. */ -class ContextTreeRemoteRepoNode(val repo: RemoteRepo, onSetChecked: (Boolean) -> Unit) : - ContextTreeNode( - Object(), onSetChecked) // TreePaths depend on user objects; Object() ensures uniqueness. diff --git a/src/main/kotlin/com/sourcegraph/cody/context/ui/EnhancedContextPanel.kt b/src/main/kotlin/com/sourcegraph/cody/context/ui/EnhancedContextPanel.kt deleted file mode 100644 index 3421cfc474..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/context/ui/EnhancedContextPanel.kt +++ /dev/null @@ -1,463 +0,0 @@ -package com.sourcegraph.cody.context.ui - -import com.intellij.ide.BrowserUtil -import com.intellij.ide.HelpTooltip -import com.intellij.openapi.actionSystem.ActionToolbarPosition -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.runInEdt -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.VerticalFlowLayout -import com.intellij.openapi.ui.getTreePath -import com.intellij.openapi.ui.popup.JBPopup -import com.intellij.ui.CheckboxTree -import com.intellij.ui.CheckboxTreeBase -import com.intellij.ui.CheckedTreeNode -import com.intellij.ui.TitledSeparator -import com.intellij.ui.ToolbarDecorator -import com.intellij.ui.ToolbarDecorator.createDecorator -import com.intellij.ui.awt.RelativePoint -import com.intellij.util.concurrency.annotations.RequiresEdt -import com.sourcegraph.cody.agent.EnhancedContextContextT -import com.sourcegraph.cody.agent.WebviewMessage -import com.sourcegraph.cody.agent.protocol.Repo -import com.sourcegraph.cody.chat.ChatSession -import com.sourcegraph.cody.config.CodyAuthenticationManager -import com.sourcegraph.cody.context.ChatEnhancedContextStateProvider -import com.sourcegraph.cody.context.EnterpriseEnhancedContextStateController -import com.sourcegraph.cody.context.RemoteRepo -import com.sourcegraph.cody.context.RepoInclusion -import com.sourcegraph.cody.history.HistoryService -import com.sourcegraph.cody.history.state.EnhancedContextState -import com.sourcegraph.common.CodyBundle -import com.sourcegraph.common.CodyBundle.fmt -import java.awt.BorderLayout -import java.awt.Dimension -import java.awt.Point -import java.awt.event.ActionEvent -import java.awt.event.KeyEvent -import java.awt.event.MouseAdapter -import java.awt.event.MouseEvent -import java.util.concurrent.atomic.AtomicBoolean -import javax.swing.AbstractAction -import javax.swing.BorderFactory -import javax.swing.JComponent -import javax.swing.JPanel -import javax.swing.KeyStroke -import javax.swing.event.TreeExpansionEvent -import javax.swing.event.TreeExpansionListener -import javax.swing.tree.DefaultTreeModel -import javax.swing.tree.TreeSelectionModel -import kotlin.math.max - -/** - * A panel for configuring context in chats. Consumer and Enterprise context panels are designed - * around a tree whose layout grows and shrinks as the tree view nodes are expanded and collapsed. - */ -abstract class EnhancedContextPanel -@RequiresEdt -constructor(protected val project: Project, protected val chatSession: ChatSession) : JPanel() { - init { - // TODO: When Kotlin @RequiresEdt annotations are instrumented, remove this manual assertion. - ApplicationManager.getApplication().assertIsDispatchThread() - } - - companion object { - /** Creates an EnhancedContextPanel for `chatSession`. */ - fun create(project: Project, chatSession: ChatSession): EnhancedContextPanel { - val isDotcomAccount = - CodyAuthenticationManager.getInstance(project).account?.isDotcomAccount() ?: false - return if (isDotcomAccount) { - ConsumerEnhancedContextPanel(project, chatSession) - } else { - EnterpriseEnhancedContextPanel(project, chatSession) - } - } - } - - /** Gets whether enhanced context is enabled. */ - val isEnhancedContextEnabled: Boolean - get() = enhancedContextEnabled.get() - - /** - * Whether enhanced context is enabled. Set this when enhance context is toggled in the panel UI. - * This is read on background threads by `isEnhancedContextEnabled`. - */ - protected val enhancedContextEnabled = AtomicBoolean(true) - - /** - * Sets this EnhancedContextPanel's configuration as the project's default enhanced context state. - */ - fun setContextFromThisChatAsDefault() { - ApplicationManager.getApplication().executeOnPooledThread { - getContextState()?.let { HistoryService.getInstance(project).updateDefaultContextState(it) } - } - } - - /** Gets the chat session's enhanced context state. */ - protected fun getContextState(): EnhancedContextState? { - if (CodyAuthenticationManager.getInstance(project).hasNoActiveAccount()) { - // There is no active account, so there is no enhanced context either - return null - } - val historyService = HistoryService.getInstance(project) - return historyService.getContextReadOnly(chatSession.getInternalId()) - ?: historyService.getDefaultContextReadOnly() - } - - /** Reads, modifies, and writes back the chat's enhanced context state. */ - protected fun updateContextState(modifyContext: (EnhancedContextState) -> Unit) { - val contextState = getContextState() ?: EnhancedContextState() - modifyContext(contextState) - HistoryService.getInstance(project) - .updateContextState(chatSession.getInternalId(), contextState) - HistoryService.getInstance(project).updateDefaultContextState(contextState) - } - - /** - * The root node of the tree view. This node is not visible. Add entries to the enhanced context - * treeview as roots of this node. - */ - protected val treeRoot = CheckedTreeNode(CodyBundle.getString("context-panel.tree.root")) - - /** - * The mutable model of tree nodes. Call `treeModel.reload()`, etc. when the tree model changes. - */ - protected val treeModel = DefaultTreeModel(treeRoot) - - /** The tree component. */ - protected val tree = run { - val checkPolicy = createCheckboxPolicy() - object : - CheckboxTree( - ContextRepositoriesCheckboxRenderer(enhancedContextEnabled), - treeRoot, - checkPolicy) { - // When collapsed, the horizontal scrollbar obscures the Chat Context summary & checkbox. - // Prefer to clip. Users can resize the sidebar if desired. - override fun getScrollableTracksViewportWidth(): Boolean = true - } - .apply { selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION } - } - - protected abstract fun createCheckboxPolicy(): CheckboxTreeBase.CheckPolicy - - init { - layout = VerticalFlowLayout(VerticalFlowLayout.BOTTOM, 0, 0, true, false) - tree.model = treeModel - } - - /** Creates the component with the enhanced context panel UI. */ - protected abstract fun createPanel(): JComponent - - val panel = createPanel() - - init { - // TODO: Resizing synchronously causes the element *now* under the pointer to get a click on - // mouse up, which can - // check/uncheck a checkbox you were not aiming at. - tree.addTreeExpansionListener( - object : TreeExpansionListener { - override fun treeExpanded(event: TreeExpansionEvent) { - if (event.path.pathCount == 2) { - // The top-level node was expanded, so expand the entire tree. - expandAllNodes() - } - resize() - } - - override fun treeCollapsed(event: TreeExpansionEvent) { - resize() - } - }) - - add(panel) - } - - /** - * Adjusts the layout to accommodate the expanded rows in the treeview, and revalidates layout. - */ - @RequiresEdt abstract fun resize() - - @RequiresEdt - private fun expandAllNodes(rowCount: Int = tree.rowCount) { - for (i in 0 until tree.rowCount) { - tree.expandRow(i) - } - - if (tree.getRowCount() != rowCount) { - expandAllNodes(tree.rowCount) - } - } - - abstract fun updateFromAgent(enhancedContextStatus: EnhancedContextContextT) - - abstract fun updateFromSavedState(state: EnhancedContextState) -} - -class EnterpriseEnhancedContextPanel(project: Project, chatSession: ChatSession) : - EnhancedContextPanel(project, chatSession) { - companion object { - fun JBPopup.showAbove(component: JComponent) { - val northWest = RelativePoint(component, Point(0, -this.size.height)) - show(northWest) - } - - private const val ENTER_MAP_KEY = "enter" - } - - private var controller = - EnterpriseEnhancedContextStateController( - project, - object : ChatEnhancedContextStateProvider { - override fun updateSavedState(modifyContext: (EnhancedContextState) -> Unit) { - runInEdt { updateContextState(modifyContext) } - } - - override fun updateAgentState(repos: List) { - chatSession.sendWebviewMessage( - WebviewMessage( - command = "context/choose-remote-search-repo", explicitRepos = repos)) - } - - override fun updateUI(repos: List) { - runInEdt { updateTree(repos) } - } - - override fun notifyRemoteRepoResolutionFailed() = runInEdt { - RemoteRepoResolutionFailedNotification().notify(project) - } - - override fun notifyRemoteRepoLimit() = runInEdt { - RemoteRepoLimitNotification().notify(project) - } - }) - - private var endpointName: String = "" - - private val repoPopupController = - RemoteRepoPopupController(project).apply { - onAccept = { spec -> - ApplicationManager.getApplication().executeOnPooledThread { - controller.updateRawSpec(spec) - } - } - } - - init { - tree.inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), ENTER_MAP_KEY) - tree.actionMap.put( - ENTER_MAP_KEY, - object : AbstractAction() { - override fun actionPerformed(e: ActionEvent) { - repoPopupController - .createPopup(tree.width, endpointName, controller.rawSpec) - .showAbove(tree) - } - }) - - tree.addMouseListener( - object : MouseAdapter() { - fun targetForEvent(e: MouseEvent): Any? = - tree.getClosestPathForLocation(e.x, e.y)?.lastPathComponent - - override fun mousePressed(e: MouseEvent) { - super.mousePressed(e) - if (targetForEvent(e) is ContextTreeEditReposNode && - (e.button == MouseEvent.BUTTON1 || e.isPopupTrigger)) { - repoPopupController - .createPopup(tree.width, endpointName, controller.rawSpec) - .showAbove(tree) - } - } - }) - - controller.requestUIUpdate() - } - - @RequiresEdt - override fun createPanel(): JComponent { - val separator = TitledSeparator(CodyBundle.getString("chat.enhanced_context.title"), tree) - HelpTooltip() - .setTitle(CodyBundle.getString("context-panel.tree.help-tooltip.title")) - .setDescription( - CodyBundle.getString("context-panel.tree.help-tooltip.description") - .fmt(MAX_REMOTE_REPOSITORY_COUNT.toString())) - .setLink(CodyBundle.getString("context-panel.tree.help-tooltip.link.text")) { - BrowserUtil.open(CodyBundle.getString("context-panel.tree.help-tooltip.link.href")) - } - .setLocation(HelpTooltip.Alignment.LEFT) - .setInitialDelay( - 1500) // Tooltip can interfere with the treeview, so cool off on showing it. - .installOn(separator) - - val panel = JPanel() - panel.layout = BorderLayout() - panel.add(separator, BorderLayout.NORTH) - panel.add(tree, BorderLayout.CENTER) - return panel - } - - override fun resize() { - val padding = 5 - tree.preferredSize = Dimension(0, padding + tree.rowCount * tree.rowHeight) - panel.parent?.revalidate() - } - - override fun createCheckboxPolicy(): CheckboxTreeBase.CheckPolicy = - CheckboxTreeBase.CheckPolicy( - /* checkChildrenWithCheckedParent = */ false, - /* uncheckChildrenWithUncheckedParent = */ false, - /* checkParentWithCheckedChild = */ false, - /* uncheckParentWithUncheckedChild = */ false) - - override fun updateFromAgent(enhancedContextStatus: EnhancedContextContextT) { - ApplicationManager.getApplication().executeOnPooledThread { - controller.updateFromAgent(enhancedContextStatus) - } - } - - override fun updateFromSavedState(state: EnhancedContextState) { - controller.loadFromChatState(state.remoteRepositories) - } - - private val contextRoot = - object : - ContextTreeEnterpriseRootNode(0, { checked -> enhancedContextEnabled.set(checked) }) { - override fun isChecked(): Boolean { - return enhancedContextEnabled.get() - } - } - - private val editReposNode = - ContextTreeEditReposNode(false) { - val popup = repoPopupController.createPopup(tree.width, endpointName, controller.rawSpec) - popup.showAbove(tree) - } - - init { - controller.loadFromChatState(getContextState()?.remoteRepositories) - endpointName = - CodyAuthenticationManager.getInstance(project).account?.server?.displayName - ?: CodyBundle.getString("context-panel.remote-repo.generic-endpoint-name") - - treeRoot.add(contextRoot) - treeModel.reload() - resize() - } - - @RequiresEdt - private fun updateTree(repos: List) { - // TODO: When Kotlin @RequiresEdt annotations are instrumented, remove this manual assertion. - ApplicationManager.getApplication().assertIsDispatchThread() - - val remotesPath = treeModel.getTreePath(contextRoot.userObject) - val wasExpanded = remotesPath != null && tree.isExpanded(remotesPath) - contextRoot.removeAllChildren() - repos - .map { repo -> - ContextTreeRemoteRepoNode(repo) { - ApplicationManager.getApplication().executeOnPooledThread { - controller.setRepoEnabledInContextState(repo.name, !repo.isEnabled) - } - } - } - .forEach { contextRoot.add(it) } - - // Add the node to add/edit the repository list. - editReposNode.hasRemovableRepos = repos.count { it.inclusion == RepoInclusion.MANUAL } > 0 - contextRoot.add(editReposNode) - - contextRoot.numActiveRepos = repos.count { it.isEnabled } - treeModel.reload(contextRoot) - if (wasExpanded) { - tree.expandPath(remotesPath) - } - - resize() - } -} - -class ConsumerEnhancedContextPanel(project: Project, chatSession: ChatSession) : - EnhancedContextPanel(project, chatSession) { - private val enhancedContextNode = - ContextTreeRootNode(CodyBundle.getString("context-panel.tree.node-chat-context")) { isChecked - -> - enhancedContextEnabled.set(isChecked) - updateContextState { it.isEnabled = isChecked } - } - - private val localContextNode = - ContextTreeLocalRootNode( - CodyBundle.getString("context-panel.tree.node-local-project"), enhancedContextEnabled) - private val localProjectNode = ContextTreeLocalRepoNode(project, enhancedContextEnabled) - - private fun prepareTree() { - treeRoot.add(enhancedContextNode) - localContextNode.add(localProjectNode) - enhancedContextNode.add(localContextNode) - - val contextState = getContextState() - updateFromSavedState(contextState ?: EnhancedContextState()) - - treeModel.reload() - resize() - } - - private var toolbar: ToolbarDecorator? = null - - @RequiresEdt - override fun createPanel(): JComponent { - val toolbar = - createDecorator(tree) - .disableUpDownActions() - .setToolbarPosition(ActionToolbarPosition.RIGHT) - .setVisibleRowCount(1) - .setScrollPaneBorder(BorderFactory.createEmptyBorder()) - .setToolbarBorder(BorderFactory.createEmptyBorder()) - .addExtraAction(ReindexButton(project)) - .addExtraAction(HelpButton()) - this.toolbar = toolbar - return toolbar.createPanel() - } - - override fun createCheckboxPolicy(): CheckboxTreeBase.CheckPolicy = - CheckboxTreeBase.CheckPolicy( - /* checkChildrenWithCheckedParent = */ true, - /* uncheckChildrenWithUncheckedParent = */ true, - /* checkParentWithCheckedChild = */ true, - /* uncheckParentWithUncheckedChild = */ false) - - override fun resize() { - val padding = 5 - // Set the minimum size to accommodate at least one toolbar button and an overflow ellipsis. - // Because the buttons - // are approximately square, use the toolbar width as a proxy for the button height. - val toolbarButtonHeight = toolbar?.actionsPanel?.preferredSize?.width ?: 0 - val preferredSizeNumVisibleButtons = 1 - panel.preferredSize = - Dimension( - 0, - padding + - max( - tree.rowCount * tree.rowHeight, - preferredSizeNumVisibleButtons * toolbarButtonHeight)) - panel.parent?.revalidate() - } - - override fun updateFromAgent(enhancedContextStatus: EnhancedContextContextT) { - // No-op. The consumer panel relies solely on JetBrains-side state. - } - - override fun updateFromSavedState(state: EnhancedContextState) { - ApplicationManager.getApplication().invokeLater { - if (project.isDisposed) { - return@invokeLater - } - enhancedContextNode.isChecked = state.isEnabled - } - } - - init { - prepareTree() - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/context/ui/HelpButton.kt b/src/main/kotlin/com/sourcegraph/cody/context/ui/HelpButton.kt deleted file mode 100644 index e376628b1a..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/context/ui/HelpButton.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.sourcegraph.cody.context.ui - -import com.intellij.icons.AllIcons -import com.intellij.ide.BrowserUtil -import com.sourcegraph.common.CodyBundle - -class HelpButton : - ContextToolbarButton( - CodyBundle.getString("context-panel.button.help"), - AllIcons.Actions.Help, - { BrowserUtil.open("https://sourcegraph.com/docs/cody/core-concepts/context") }) diff --git a/src/main/kotlin/com/sourcegraph/cody/context/ui/ReindexButton.kt b/src/main/kotlin/com/sourcegraph/cody/context/ui/ReindexButton.kt deleted file mode 100644 index b20199b156..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/context/ui/ReindexButton.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.sourcegraph.cody.context.ui - -import com.intellij.icons.AllIcons -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.Task -import com.intellij.openapi.project.Project -import com.sourcegraph.cody.agent.CodyAgentService -import com.sourcegraph.cody.agent.CommandExecuteParams -import com.sourcegraph.common.CodyBundle -import java.util.concurrent.atomic.AtomicBoolean - -class ReindexButton(private val project: Project) : - ContextToolbarButton( - CodyBundle.getString("context-panel.button.reindex"), AllIcons.Actions.Refresh) { - - private val isReindexingInProgress = AtomicBoolean(false) - - override fun isEnabled() = !isReindexingInProgress.get() - - override fun actionPerformed(p0: AnActionEvent) { - CodyAgentService.withAgentRestartIfNeeded(project) { agent -> - ProgressManager.getInstance() - .run( - object : - Task.Backgroundable( - project, CodyBundle.getString("context-panel.in-progress"), false) { - override fun run(indicator: ProgressIndicator) { - try { - isReindexingInProgress.set(true) - val cmd = CommandExecuteParams("cody.search.index-update", emptyList()) - agent.server.commandExecute(cmd).get() - } finally { - indicator.stop() - isReindexingInProgress.set(false) - } - } - }) - } - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/context/ui/RemoteRepoPopupController.kt b/src/main/kotlin/com/sourcegraph/cody/context/ui/RemoteRepoPopupController.kt deleted file mode 100644 index 806f68fbe8..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/context/ui/RemoteRepoPopupController.kt +++ /dev/null @@ -1,173 +0,0 @@ -package com.sourcegraph.cody.context.ui - -import com.intellij.codeInsight.AutoPopupController -import com.intellij.codeInsight.completion.BaseCompletionService -import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonShortcuts -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.EditorFactory -import com.intellij.openapi.editor.LogicalPosition -import com.intellij.openapi.editor.ex.EditorEx -import com.intellij.openapi.editor.ex.FocusChangeListener -import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory -import com.intellij.openapi.project.Project -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.psi.PsiDocumentManager -import com.intellij.psi.PsiFileFactory -import com.intellij.ui.SoftWrapsEditorCustomization -import com.intellij.ui.popup.AbstractPopup -import com.intellij.util.LocalTimeCounter -import com.intellij.util.concurrency.annotations.RequiresEdt -import com.intellij.util.ui.JBDimension -import com.intellij.util.ui.JBUI -import com.sourcegraph.cody.context.RemoteRepoFileType -import com.sourcegraph.common.CodyBundle -import com.sourcegraph.common.CodyBundle.fmt -import com.sourcegraph.common.ui.DumbAwareEDTAction -import com.sourcegraph.utils.CodyEditorUtil -import java.awt.BorderLayout -import java.awt.Dimension -import javax.swing.JPanel -import javax.swing.border.CompoundBorder -import kotlin.math.max - -const val MAX_REMOTE_REPOSITORY_COUNT = 10 -const val MIN_POPUP_WIDTH = 400 - -class RemoteRepoPopupController(val project: Project) { - var onAccept: (spec: String) -> Unit = {} - - @RequiresEdt - fun createPopup(width: Int, endpoint: String, initialValue: String = ""): JBPopup { - val psiFile = - PsiFileFactory.getInstance(project) - .createFileFromText( - "RepositoryList", - RemoteRepoFileType.INSTANCE, - initialValue, - LocalTimeCounter.currentTime(), - true, - false) - psiFile.putUserData(BaseCompletionService.FORBID_WORD_COMPLETION, false) - DaemonCodeAnalyzer.getInstance(project).setHighlightingEnabled(psiFile, true) - - val document = PsiDocumentManager.getInstance(project).getDocument(psiFile)!! - - val editor = EditorFactory.getInstance().createEditor(document, project) - editor.putUserData(AutoPopupController.ALWAYS_AUTO_POPUP, true) - editor.putUserData(CodyEditorUtil.KEY_EDITOR_WANTS_AUTOCOMPLETE, false) - - // Put the cursor at the end of the first line. This is a more convenient place to insert new - // repositories. - editor.caretModel.moveToLogicalPosition(LogicalPosition(0, document.getLineEndOffset(0))) - - if (editor is EditorEx) { - editor.apply { - SoftWrapsEditorCustomization.ENABLED.customize(this) - setHorizontalScrollbarVisible(false) - setVerticalScrollbarVisible(true) - highlighter = - EditorHighlighterFactory.getInstance() - .createEditorHighlighter(project, RemoteRepoFileType.INSTANCE) - addFocusListener( - object : FocusChangeListener { - override fun focusGained(editor: Editor) { - super.focusGained(editor) - val project = editor.project - if (project != null) { - AutoPopupController.getInstance(project).scheduleAutoPopup(editor) - } - } - }) - } - } - editor.settings.apply { - additionalLinesCount = 0 - additionalColumnsCount = 1 - isRightMarginShown = false - setRightMargin(-1) - isFoldingOutlineShown = false - isLineNumbersShown = false - isLineMarkerAreaShown = false - isIndentGuidesShown = false - isVirtualSpace = false - isWheelFontChangeEnabled = false - isAdditionalPageAtBottom = false - lineCursorWidth = 1 - } - editor.contentComponent.apply { border = CompoundBorder(JBUI.Borders.empty(2), border) } - - val panel = JPanel(BorderLayout()).apply { add(editor.component, BorderLayout.CENTER) } - val scaledWidth = max(width, JBDimension(MIN_POPUP_WIDTH, 0).width) - val scaledHeight = JBDimension(0, 160).height - val size = Dimension(scaledWidth, scaledHeight) - - var popup: JBPopup? = null - popup = - (JBPopupFactory.getInstance() - .createComponentPopupBuilder(panel, editor.contentComponent) - .apply { - setAdText( - CodyBundle.getString("context-panel.remote-repo.select-repo-advertisement") - .fmt(endpoint)) - setCancelOnClickOutside(true) // Do dismiss if the user clicks outside the popup. - setCancelOnWindowDeactivation(false) // Don't dismiss on alt-tab away and back. - setKeyEventHandler { event -> - // Subtle: We want to OK the popup on CTRL+ENTER or clicks outside, but cancel - // on ESC. Here's how it works: - // - // - Set the default result to OK. See setOk(true) below. - // - Clicks outside get the default success result. - // - If we intercept a close key event (ESC), we flip back to setOk(false). - // - // This relies on this JBPopupFactory creating an AbstractPopup. That is - // documented, see JBPopupFactory "Types of popups in IntelliJ Platform". But if - // the result is not an AbstractPopup, the dialog will still work: Clicks - // outside will cancel instead of OK. - if (AbstractPopup.isCloseRequest(event)) { - (popup as? AbstractPopup)?.setOk(false) - } - false - } - setMayBeParent(true) - setMinSize(size) - setRequestFocus(true) - setResizable(true) - addListener( - object : JBPopupListener { - override fun onClosed(event: LightweightWindowEvent) { - if (event.isOk) { - // We don't use the Psi elements here, because the Annotator may be - // slow, etc. - onAccept(document.text) - } - EditorFactory.getInstance().releaseEditor(editor) - } - }) - }) - .createPopup() - - // Set the default result to OK. This is needed to handle "OK on click away." See - // setKeyEventHandler above. - (popup as? AbstractPopup)?.setOk(true) - - val okAction = - object : DumbAwareEDTAction() { - override fun actionPerformed(event: AnActionEvent) { - unregisterCustomShortcutSet(popup.content) - popup.closeOk(event.inputEvent) - } - } - okAction.registerCustomShortcutSet(CommonShortcuts.CTRL_ENTER, popup.content) - - // If not explicitly set, the popup's minimum size is applied after the popup is shown, which is - // too late to compute placement in showAbove. - popup.size = size - - return popup - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/EditService.kt b/src/main/kotlin/com/sourcegraph/cody/edit/EditService.kt index 4a5d9e49f6..3f8e68e911 100644 --- a/src/main/kotlin/com/sourcegraph/cody/edit/EditService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/edit/EditService.kt @@ -6,9 +6,16 @@ import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.project.Project -import com.sourcegraph.cody.agent.protocol.TextEdit -import com.sourcegraph.cody.agent.protocol.WorkspaceEditParams import com.sourcegraph.cody.agent.protocol_extensions.toOffset +import com.sourcegraph.cody.agent.protocol_generated.CreateFileOperation +import com.sourcegraph.cody.agent.protocol_generated.DeleteFileOperation +import com.sourcegraph.cody.agent.protocol_generated.DeleteTextEdit +import com.sourcegraph.cody.agent.protocol_generated.EditFileOperation +import com.sourcegraph.cody.agent.protocol_generated.InsertTextEdit +import com.sourcegraph.cody.agent.protocol_generated.RenameFileOperation +import com.sourcegraph.cody.agent.protocol_generated.ReplaceTextEdit +import com.sourcegraph.cody.agent.protocol_generated.TextEdit +import com.sourcegraph.cody.agent.protocol_generated.WorkspaceEditParams import com.sourcegraph.utils.CodyEditorUtil @Service(Service.Level.PROJECT) @@ -38,32 +45,20 @@ class EditService(val project: Project) { return WriteCommandAction.runWriteCommandAction(project) { edits.reversed().all { edit -> - when (edit.type) { - "replace", - "delete" -> { - if (edit.range != null) { - document.replaceString( - edit.range.start.toOffset(document), - edit.range.end.toOffset(document), - edit.value ?: "") - true - } else { - logger.warn("Edit range is null for ${edit.type} operation") - false - } + when (edit) { + is ReplaceTextEdit -> { + document.replaceString( + edit.range.start.toOffset(document), edit.range.end.toOffset(document), edit.value) + true } - "insert" -> { - if (edit.position != null) { - document.insertString(edit.position.toOffset(document), edit.value ?: "") - true - } else { - logger.warn("Edit position is null for insert operation") - false - } + is DeleteTextEdit -> { + document.deleteString( + edit.range.start.toOffset(document), edit.range.end.toOffset(document)) + true } - else -> { - logger.warn("Unknown edit type: ${edit.type}") - false + is InsertTextEdit -> { + document.insertString(edit.position.toOffset(document), edit.value) + true } } } @@ -73,35 +68,22 @@ class EditService(val project: Project) { fun performWorkspaceEdit(workspaceEditParams: WorkspaceEditParams): Boolean { return workspaceEditParams.operations.all { op -> // TODO: We need to support the file-level operations. - when (op.type) { - "create-file" -> { + when (op) { + is CreateFileOperation -> { logger.warn("Workspace edit operation created a file: ${op.uri}") return false } - "rename-file" -> { + is RenameFileOperation -> { logger.warn("Workspace edit operation renamed a file: ${op.oldUri} -> ${op.newUri}") return false } - "delete-file" -> { + is DeleteFileOperation -> { logger.warn("Workspace edit operation deleted a file: ${op.uri}") return false } - "edit-file" -> { - if (op.edits == null) { - logger.warn("Workspace edit operation has no edits") - return false - } else if (op.uri == null) { - logger.warn("Workspace edit operation has null uri") - return false - } else { - logger.info("Applying workspace edit to a file: ${op.uri}") - performTextEdits(op.uri, op.edits) - } - } - else -> { - logger.warn( - "DocumentCommand session received unknown workspace edit operation: ${op.type}") - return false + is EditFileOperation -> { + logger.info("Applying workspace edit to a file: ${op.uri}") + performTextEdits(op.uri, op.edits) } } } diff --git a/src/main/kotlin/com/sourcegraph/cody/error/CodyConsole.kt b/src/main/kotlin/com/sourcegraph/cody/error/CodyConsole.kt index 8493e0bc75..b4cd6932c5 100644 --- a/src/main/kotlin/com/sourcegraph/cody/error/CodyConsole.kt +++ b/src/main/kotlin/com/sourcegraph/cody/error/CodyConsole.kt @@ -8,7 +8,7 @@ import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindowManager import com.intellij.ui.content.Content -import com.sourcegraph.cody.agent.protocol.DebugMessage +import com.sourcegraph.cody.agent.protocol_generated.DebugMessage @Service(Service.Level.PROJECT) class CodyConsole(project: Project) { diff --git a/src/main/kotlin/com/sourcegraph/cody/history/node/LeafNode.kt b/src/main/kotlin/com/sourcegraph/cody/history/node/LeafNode.kt deleted file mode 100644 index a42d28c284..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/history/node/LeafNode.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.sourcegraph.cody.history.node - -import com.sourcegraph.cody.history.state.ChatState -import javax.swing.tree.DefaultMutableTreeNode - -class LeafNode(val chat: ChatState) : DefaultMutableTreeNode(chat, false) { - - fun title() = chat.title() ?: "No title" -} diff --git a/src/main/kotlin/com/sourcegraph/cody/history/node/PeriodNode.kt b/src/main/kotlin/com/sourcegraph/cody/history/node/PeriodNode.kt deleted file mode 100644 index b5d4f77153..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/history/node/PeriodNode.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.sourcegraph.cody.history.node - -import javax.swing.tree.DefaultMutableTreeNode - -class PeriodNode(val periodText: String) : DefaultMutableTreeNode(periodText, true) { - - fun leafs(): List = children().toList().filterIsInstance() -} diff --git a/src/main/kotlin/com/sourcegraph/cody/history/node/RootNode.kt b/src/main/kotlin/com/sourcegraph/cody/history/node/RootNode.kt deleted file mode 100644 index 1166412391..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/history/node/RootNode.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.sourcegraph.cody.history.node - -import javax.swing.tree.DefaultMutableTreeNode - -class RootNode : DefaultMutableTreeNode(null, true) { - - fun periods(): List = children().toList().filterIsInstance() -} diff --git a/src/main/kotlin/com/sourcegraph/cody/history/ui/HistoryTreeNodeRenderer.kt b/src/main/kotlin/com/sourcegraph/cody/history/ui/HistoryTreeNodeRenderer.kt deleted file mode 100644 index 1a53919ee7..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/history/ui/HistoryTreeNodeRenderer.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.sourcegraph.cody.history.ui - -import com.intellij.ide.util.treeView.NodeRenderer -import com.intellij.ui.SimpleTextAttributes -import com.sourcegraph.cody.Icons -import com.sourcegraph.cody.history.node.LeafNode -import com.sourcegraph.common.CodyBundle -import com.sourcegraph.common.CodyBundle.fmt -import java.time.LocalDateTime -import java.time.temporal.ChronoUnit -import javax.swing.JTree - -class HistoryTreeNodeRenderer : NodeRenderer() { - - override fun customizeCellRenderer( - tree: JTree, - value: Any?, - selected: Boolean, - expanded: Boolean, - leaf: Boolean, - row: Int, - hasFocus: Boolean - ) { - when (value) { - is LeafNode -> { - icon = Icons.Chat.ChatLeaf - append(" ") - append(value.title()) - append(" ") - - val lastUpdated = value.chat.getUpdatedTimeAt() - if (isShortDuration(lastUpdated)) { - append(" ") - val duration = DurationUnitFormatter.format(lastUpdated) - append( - CodyBundle.getString("duration.x-ago").fmt(duration), - SimpleTextAttributes.GRAYED_ATTRIBUTES) - } - } - else -> append(value.toString()) - } - } - - private fun isShortDuration(since: LocalDateTime) = - ChronoUnit.DAYS.between(since, LocalDateTime.now()).toInt() < 7 -} diff --git a/src/main/kotlin/com/sourcegraph/cody/ignore/CommandPanelIgnoreBanner.kt b/src/main/kotlin/com/sourcegraph/cody/ignore/CommandPanelIgnoreBanner.kt deleted file mode 100644 index 84f3fed292..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/ignore/CommandPanelIgnoreBanner.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.sourcegraph.cody.ignore - -import com.intellij.ide.BrowserUtil -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.editor.colors.EditorColors -import com.intellij.openapi.editor.colors.EditorColorsManager -import com.intellij.ui.EditorNotificationPanel -import com.intellij.ui.SideBorder -import com.intellij.ui.components.panels.NonOpaquePanel -import com.sourcegraph.Icons -import com.sourcegraph.common.CodyBundle -import java.awt.Dimension - -class CommandPanelIgnoreBanner : NonOpaquePanel() { - init { - ApplicationManager.getApplication().assertIsDispatchThread() - - add( - EditorNotificationPanel().apply { - text = CodyBundle.getString("filter.sidebar-panel-ignored-file.text") - createActionLabel( - CodyBundle.getString("filter.sidebar-panel-ignored-file.learn-more-cta"), - { BrowserUtil.browse(CODY_IGNORE_DOCS_URL) }, - false) - icon(Icons.CodyLogoSlash) - }) - - // These colors cribbed from EditorComposite, createTopBottomSideBorder - val scheme = EditorColorsManager.getInstance().globalScheme - val borderColor = scheme.getColor(EditorColors.TEARLINE_COLOR) - border = SideBorder(borderColor, SideBorder.TOP or SideBorder.BOTTOM) - } - - override fun getMaximumSize(): Dimension { - val size = super.getMaximumSize() - size.height = preferredSize.height - return size - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/inspections/CodeActionQuickFix.kt b/src/main/kotlin/com/sourcegraph/cody/inspections/CodeActionQuickFix.kt index 9cc162495a..6825b9a5f3 100644 --- a/src/main/kotlin/com/sourcegraph/cody/inspections/CodeActionQuickFix.kt +++ b/src/main/kotlin/com/sourcegraph/cody/inspections/CodeActionQuickFix.kt @@ -2,7 +2,6 @@ package com.sourcegraph.cody.inspections import com.intellij.codeInsight.intention.IntentionAction import com.intellij.codeInsight.intention.PriorityAction -import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile @@ -24,8 +23,6 @@ class CodeActionQuickFix(private val params: CodeActionQuickFixParams) : const val FAMILY_NAME = "Cody Code Action" } - private val logger = Logger.getInstance(CodeActionQuickFix::class.java) - override fun getPriority(): PriorityAction.Priority { return if (isFixAction()) { PriorityAction.Priority.TOP diff --git a/src/main/kotlin/com/sourcegraph/cody/ui/AccordionSection.kt b/src/main/kotlin/com/sourcegraph/cody/ui/AccordionSection.kt deleted file mode 100644 index c4151deacd..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/ui/AccordionSection.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.sourcegraph.cody.ui - -import com.intellij.openapi.ui.VerticalFlowLayout -import java.awt.BorderLayout -import javax.swing.JButton -import javax.swing.JPanel -import javax.swing.SwingConstants - -class AccordionSection(title: String) : JPanel() { - val contentPanel: JPanel - private val toggleButton: JButton - private val sectionTitle: String - - init { - layout = BorderLayout() - sectionTitle = title - toggleButton = JButton(createToggleButtonHTML(title, true)) - toggleButton.horizontalAlignment = SwingConstants.LEFT - toggleButton.isBorderPainted = false - toggleButton.isFocusPainted = false - toggleButton.isContentAreaFilled = false - contentPanel = JPanel() - toggleButton.addActionListener { _ -> - if (contentPanel.isVisible) { - contentPanel.isVisible = false - toggleButton.text = createToggleButtonHTML(sectionTitle, true) - } else { - contentPanel.isVisible = true - toggleButton.text = createToggleButtonHTML(sectionTitle, false) - } - } - contentPanel.layout = VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 0, true, false) - contentPanel.isVisible = false - add(toggleButton, BorderLayout.NORTH) - add(contentPanel, BorderLayout.CENTER) - } - - private fun createToggleButtonHTML(title: String, isCollapsed: Boolean): String = - """ - - - ${if (isCollapsed) "▶" else "▼"} - -  $title - - """ -} diff --git a/src/main/kotlin/com/sourcegraph/cody/ui/AttributionButtonController.kt b/src/main/kotlin/com/sourcegraph/cody/ui/AttributionButtonController.kt deleted file mode 100644 index 57c3d33105..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/ui/AttributionButtonController.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.sourcegraph.cody.ui - -import com.intellij.openapi.project.Project -import com.intellij.util.concurrency.annotations.RequiresEdt -import com.sourcegraph.cody.agent.CurrentConfigFeatures -import com.sourcegraph.cody.agent.protocol.AttributionSearchResponse -import com.sourcegraph.cody.attribution.AttributionListener -import com.sourcegraph.common.CodyBundle -import com.sourcegraph.common.CodyBundle.fmt - -class AttributionButtonController(val button: ConditionalVisibilityButton) : AttributionListener { - - private val extraUpdates: MutableList = ArrayList() - - companion object { - fun setup(project: Project): AttributionButtonController { - val button = - ConditionalVisibilityButton(CodyBundle.getString("chat.attribution.searching.label")) - button.isEnabled = false // non-clickable - val currentConfigFeatures: CurrentConfigFeatures = - project.getService(CurrentConfigFeatures::class.java) - // Only display the button if attribution is enabled. - button.visibilityAllowed = currentConfigFeatures.get().attribution - return AttributionButtonController(button) - } - } - - @RequiresEdt - override fun onAttributionSearchStart() { - button.toolTipText = CodyBundle.getString("chat.attribution.searching.tooltip") - } - - @RequiresEdt - override fun updateAttribution(attribution: AttributionSearchResponse) { - if (attribution.error != null) { - button.text = CodyBundle.getString("chat.attribution.error.label") - button.toolTipText = - CodyBundle.getString("chat.attribution.error.tooltip").fmt(attribution.error) - } else if (attribution.repoNames.isEmpty()) { - button.text = CodyBundle.getString("chat.attribution.success.label") - button.toolTipText = CodyBundle.getString("chat.attribution.success.tooltip") - } else { - val count = "${attribution.repoNames.size}" + if (attribution.limitHit) "+" else "" - val repoNames = - attribution.repoNames.joinToString( - prefix = "
  • ", separator = "
  • ", postfix = "
") - button.text = CodyBundle.getString("chat.attribution.failure.label") - button.toolTipText = - CodyBundle.getString("chat.attribution.failure.tooltip").fmt(count, repoNames) - } - button.updatePreferredSize() - for (action in extraUpdates) { - action.run() - } - } - - /** Run extra actions on button update, like resizing components. */ - fun onUpdate(action: Runnable) { - extraUpdates += action - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/ui/AutoGrowingTextArea.kt b/src/main/kotlin/com/sourcegraph/cody/ui/AutoGrowingTextArea.kt deleted file mode 100644 index bf6d5cd2bd..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/ui/AutoGrowingTextArea.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.sourcegraph.cody.ui - -import com.intellij.ide.ui.laf.darcula.ui.DarculaTextAreaUI -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.CustomShortcutSet -import com.intellij.openapi.actionSystem.KeyboardShortcut -import com.intellij.openapi.actionSystem.ShortcutSet -import com.intellij.ui.components.JBScrollPane -import com.intellij.ui.components.JBTextArea -import com.intellij.util.ui.UIUtil -import com.sourcegraph.common.ui.SimpleDumbAwareEDTAction -import java.awt.Dimension -import java.awt.event.InputEvent -import java.awt.event.KeyEvent -import javax.swing.JComponent -import javax.swing.KeyStroke -import javax.swing.ScrollPaneConstants -import javax.swing.plaf.basic.BasicTextAreaUI -import javax.swing.text.AttributeSet -import javax.swing.text.Document -import javax.swing.text.PlainDocument -import javax.swing.undo.UndoManager -import kotlin.math.max -import kotlin.math.min - -class AutoGrowingTextArea(private val minRows: Int, maxRows: Int, outerPanel: JComponent) { - val textArea: JBTextArea - val scrollPane: JBScrollPane - private val initialPreferredSize: Dimension - private val autoGrowUpToRow: Int = maxRows + 1 - private val undoManager = UndoManager() - - init { - textArea = createTextArea() - scrollPane = JBScrollPane(textArea) - scrollPane.isFocusable = false - initialPreferredSize = scrollPane.preferredSize - val document: Document = - object : PlainDocument() { - override fun insertString(offs: Int, str: String?, a: AttributeSet?) { - super.insertString(offs, str, a) - updateTextAreaSize() - outerPanel.revalidate() - } - - override fun remove(offs: Int, len: Int) { - super.remove(offs, len) - updateTextAreaSize() - outerPanel.revalidate() - } - } - - textArea.document = document - document.addUndoableEditListener { event -> undoManager.addEdit(event.edit) } - - updateTextAreaSize() - } - - fun getText(): String { - return this.textArea.text - } - - fun setText(newText: String) { - textArea.text = newText - } - - private fun createTextArea(): JBTextArea { - val promptInput: JBTextArea = RoundedJBTextArea(minRows, 10) - val textUI = DarculaTextAreaUI.createUI(promptInput) as BasicTextAreaUI - promptInput.setUI(textUI) - promptInput.font = UIUtil.getLabelFont() - promptInput.lineWrap = true - promptInput.wrapStyleWord = true - promptInput.requestFocusInWindow() - - /* Insert Enter on Shift+Enter, Ctrl+Enter, Alt/Option+Enter, and Meta+Enter */ - val shiftEnter = - KeyboardShortcut( - KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK), null) - val ctrlEnter = - KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.CTRL_DOWN_MASK), null) - val altOrOptionEnter = - KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.ALT_DOWN_MASK), null) - val metaEnter = - KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.META_DOWN_MASK), null) - val insertEnterShortcut: ShortcutSet = - CustomShortcutSet(ctrlEnter, shiftEnter, metaEnter, altOrOptionEnter) - val insertEnterAction: AnAction = SimpleDumbAwareEDTAction { - promptInput.insert("\n", promptInput.caretPosition) - } - insertEnterAction.registerCustomShortcutSet(insertEnterShortcut, promptInput) - return promptInput - } - - private fun updateTextAreaSize() { - // Get the preferred size of the JTextArea based on its content - val preferredSize = textArea.preferredSize - // Limit the number of rows to maxRows - val fontMetrics = textArea.getFontMetrics(textArea.font) - val maxTextAreaHeight = fontMetrics.height * autoGrowUpToRow - var preferredHeight = min(preferredSize.height, maxTextAreaHeight) - preferredHeight = max(preferredHeight, initialPreferredSize.height) - - // Set the preferred size of the JScrollPane to accommodate the JTextArea - val scrollPaneSize = scrollPane.size - scrollPaneSize.height = preferredHeight - scrollPane.preferredSize = scrollPaneSize - val shouldShowScrollbar = preferredSize.height > maxTextAreaHeight - scrollPane.verticalScrollBarPolicy = - if (shouldShowScrollbar) ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS - else ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/ui/ChatScrollPane.kt b/src/main/kotlin/com/sourcegraph/cody/ui/ChatScrollPane.kt deleted file mode 100644 index 24223f1ee5..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/ui/ChatScrollPane.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.sourcegraph.cody.ui - -import com.intellij.ui.components.JBScrollPane -import com.intellij.ui.components.JBViewport -import java.awt.Point -import java.awt.Rectangle -import javax.swing.BorderFactory -import javax.swing.JPanel -import kotlin.math.max - -class ChatScrollPane(private val messagesPanel: JPanel) : - JBScrollPane(messagesPanel, VERTICAL_SCROLLBAR_AS_NEEDED, HORIZONTAL_SCROLLBAR_NEVER) { - - internal var touchingBottom = false - - init { - border = BorderFactory.createEmptyBorder() - verticalScrollBar.addAdjustmentListener { change -> - val distance = change.value - verticalScrollBar.maximum + verticalScrollBar.visibleAmount - touchingBottom = distance == 0 - } - setViewport( - object : JBViewport() { - - override fun scrollRectToVisible(bounds: Rectangle?) {} - - override fun setViewPosition(point: Point) { - if (touchingBottom) { - val maxHeight = max(0, messagesPanel.height - height) - super.setViewPosition(Point(0, maxHeight)) - } else { - super.setViewPosition(point) - } - } - }) - setViewportView(messagesPanel) - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/ui/CodyToolWindowFactory.kt b/src/main/kotlin/com/sourcegraph/cody/ui/CodyToolWindowFactory.kt index 09d0c745ce..80cd2fd184 100644 --- a/src/main/kotlin/com/sourcegraph/cody/ui/CodyToolWindowFactory.kt +++ b/src/main/kotlin/com/sourcegraph/cody/ui/CodyToolWindowFactory.kt @@ -2,11 +2,13 @@ package com.sourcegraph.cody.ui import com.google.gson.Gson import com.google.gson.JsonArray +import com.google.gson.JsonParser 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.diagnostic.Logger +import com.intellij.openapi.ide.CopyPasteManager import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.project.Project import com.intellij.ui.jcef.JBCefBrowserBase @@ -14,7 +16,16 @@ import com.intellij.ui.jcef.JBCefBrowserBuilder import com.intellij.ui.jcef.JBCefJSQuery import com.intellij.util.io.isAncestor import com.jetbrains.rd.util.ConcurrentHashMap -import com.sourcegraph.cody.agent.* +import com.sourcegraph.cody.agent.CodyAgent +import com.sourcegraph.cody.agent.CodyAgentService +import com.sourcegraph.cody.agent.CommandExecuteParams +import com.sourcegraph.cody.agent.WebviewDidDisposeParams +import com.sourcegraph.cody.agent.WebviewPostMessageStringEncodedParams +import com.sourcegraph.cody.agent.WebviewReceiveMessageStringEncodedParams +import com.sourcegraph.cody.agent.WebviewRegisterWebviewViewProviderParams +import com.sourcegraph.cody.agent.WebviewSetHtmlParams +import com.sourcegraph.cody.agent.WebviewSetOptionsParams +import com.sourcegraph.cody.agent.WebviewSetTitleParams import com.sourcegraph.cody.agent.protocol.WebviewCreateWebviewPanelParams import com.sourcegraph.cody.agent.protocol.WebviewOptions import com.sourcegraph.cody.chat.actions.ExportChatsAction.Companion.gson @@ -22,6 +33,7 @@ import com.sourcegraph.cody.config.ui.AccountConfigurable import com.sourcegraph.cody.sidebar.WebTheme import com.sourcegraph.cody.sidebar.WebThemeController import com.sourcegraph.common.BrowserOpener +import java.awt.datatransfer.StringSelection import java.io.IOException import java.net.URI import java.net.URLDecoder @@ -40,7 +52,14 @@ import org.cef.browser.CefBrowser import org.cef.browser.CefFrame import org.cef.callback.CefAuthCallback import org.cef.callback.CefCallback -import org.cef.handler.* +import org.cef.handler.CefCookieAccessFilter +import org.cef.handler.CefFocusHandler +import org.cef.handler.CefFocusHandlerAdapter +import org.cef.handler.CefLifeSpanHandler +import org.cef.handler.CefLoadHandler +import org.cef.handler.CefRequestHandler +import org.cef.handler.CefResourceHandler +import org.cef.handler.CefResourceRequestHandler import org.cef.misc.BoolRef import org.cef.misc.IntRef import org.cef.misc.StringRef @@ -202,6 +221,7 @@ class WebUIService(private val project: Project) { } const val COMMAND_PREFIX = "command:" + // We make up a host name and serve the static resources into the webview apparently from this host. const val PSEUDO_HOST = "file+.sourcegraphstatic.com" const val PSEUDO_ORIGIN = "https://$PSEUDO_HOST" @@ -314,7 +334,7 @@ class WebUIProxy(private val host: WebUIHost, private val browser: JBCefBrowserB val browser = JBCefBrowserBuilder() .apply { - setOffScreenRendering(false) + setOffScreenRendering(true) // TODO: Make this conditional on running in a debug configuration. setEnableOpenDevToolsMenuItem(true) } @@ -417,19 +437,26 @@ class WebUIProxy(private val host: WebUIHost, private val browser: JBCefBrowserB } private fun handleCefQuery(query: String) { - val postMessagePrefix = "{\"what\":\"postMessage\",\"value\":" - val setStatePrefix = "{\"what\":\"setState\",\"value\":" - when { - query.startsWith(postMessagePrefix) -> { - val stringEncodedJsonMessage = - query.substring(postMessagePrefix.length, query.length - "}".length) - host.postMessageWebviewToHost(stringEncodedJsonMessage) + val queryObject = JsonParser.parseString(query).asJsonObject + val queryWhat = queryObject["what"]?.asString + + when (queryWhat) { + "postMessage" -> { + val queryValue = queryObject["value"] ?: return + host.postMessageWebviewToHost(queryValue.toString()) + + val messageObject = if (queryValue.isJsonObject) queryValue.asJsonObject else return + if (messageObject["command"]?.asString == "copy" && + messageObject["text"]?.asString != null) { + val textToCopy = messageObject["text"].asString + CopyPasteManager.getInstance().setContents(StringSelection(textToCopy)) + } } - query.startsWith(setStatePrefix) -> { - val state = query.substring(setStatePrefix.length, query.length - "}".length) - host.stateAsJSONString = state + "setState" -> { + val queryValue = queryObject["value"] ?: return + host.stateAsJSONString = queryValue.toString() } - query == "{\"what\":\"DOMContentLoaded\"}" -> onDOMContentLoaded() + "DOMContentLoaded" -> onDOMContentLoaded() else -> { logger.warn("unhandled query from Webview to host: $query") } @@ -715,6 +742,7 @@ class ExtensionResourceHandler() : CefResourceHandler { var bytesReadFromResource = 0L private var bytesSent = 0L private var bytesWaitingSend = ByteBuffer.allocate(512 * 1024).flip() + // correctly private var contentLength = 0L var contentType = "text/plain" diff --git a/src/main/kotlin/com/sourcegraph/cody/ui/ConditionalVisibilityButton.kt b/src/main/kotlin/com/sourcegraph/cody/ui/ConditionalVisibilityButton.kt deleted file mode 100644 index b2e386c0d3..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/ui/ConditionalVisibilityButton.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.sourcegraph.cody.ui - -import java.awt.Dimension - -/** - * [ConditionalVisibilityButton] is only made visible if visibility is allowed. - * - * This is to implement a hover visibility that is conditional on another factor, like enabling - * attribution setting. - */ -class ConditionalVisibilityButton(text: String) : TransparentButton(text) { - - var visibilityAllowed: Boolean = true - set(value) { - field = value - if (!value) { - super.setVisible(false) - } - } - - override fun setVisible(value: Boolean) { - if ((value && visibilityAllowed) // either make visible if visibility allowed - || (!value) // or make invisible - ) { - super.setVisible(value) - } - } - - override fun getPreferredSize(): Dimension = - if (visibilityAllowed) super.getPreferredSize() else Dimension(0, 0) -} diff --git a/src/main/kotlin/com/sourcegraph/cody/ui/RoundedJBTextArea.kt b/src/main/kotlin/com/sourcegraph/cody/ui/RoundedJBTextArea.kt deleted file mode 100644 index f9a68cc52d..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/ui/RoundedJBTextArea.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.sourcegraph.cody.ui - -import com.intellij.ui.ColorUtil -import com.intellij.ui.components.JBTextArea -import java.awt.Graphics -import java.awt.Graphics2D -import java.awt.RenderingHints -import java.awt.geom.RoundRectangle2D -import javax.swing.BorderFactory - -class RoundedJBTextArea(minRows: Int, private val cornerRadius: Int) : JBTextArea(minRows, 0) { - init { - isOpaque = false - border = BorderFactory.createEmptyBorder(4, 4, 4, 4) - } - - override fun paintComponent(g: Graphics) { - val g2 = g.create() as Graphics2D - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) - val roundRect = - RoundRectangle2D.Float( - 0f, - 0f, - (this.width - 1).toFloat(), - (this.height - 1).toFloat(), - cornerRadius.toFloat(), - cornerRadius.toFloat()) - g2.color = background - g2.fill(roundRect) - g2.color = ColorUtil.brighter(background, 2) - g2.draw(roundRect) - g2.dispose() - super.paintComponent(g) - } -} diff --git a/src/main/kotlin/com/sourcegraph/cody/ui/TransparentButton.kt b/src/main/kotlin/com/sourcegraph/cody/ui/TransparentButton.kt deleted file mode 100644 index 85406d9b89..0000000000 --- a/src/main/kotlin/com/sourcegraph/cody/ui/TransparentButton.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.sourcegraph.cody.ui - -import com.intellij.ide.ui.UISettings -import com.intellij.ui.ColorUtil -import com.intellij.util.ui.UIUtil -import java.awt.AlphaComposite -import java.awt.BasicStroke -import java.awt.Dimension -import java.awt.FontMetrics -import java.awt.Graphics -import java.awt.Graphics2D -import javax.swing.JButton - -open class TransparentButton(text: String) : JButton(text) { - private val cornerRadius = 5 - private val fontMetric: FontMetrics - - init { - isContentAreaFilled = false - isFocusPainted = false - isBorderPainted = false - isVisible = false - - this.fontMetric = getFontMetrics(font) - updatePreferredSize() - } - - /** Calculate the preferred size based on the size of the text. */ - fun updatePreferredSize() { - val horizontalPadding = 10 - val verticalPadding = 5 - val width = fontMetric.stringWidth(text) + horizontalPadding * 2 - val height = fontMetric.height + verticalPadding * 2 - preferredSize = Dimension(width, height) - } - - override fun paintComponent(g: Graphics) { - UISettings.setupAntialiasing(g) - val g2 = g.create() as Graphics2D - - if (isEnabled) { - g2.composite = AlphaComposite.SrcOver.derive(0.7f) - g.color = UIUtil.getLabelForeground() - } else { - g2.composite = AlphaComposite.SrcOver.derive(0.4f) - g.color = ColorUtil.darker(UIUtil.getLabelForeground(), 3) - } - - g2.color = background - g2.fillRoundRect(0, 0, width, height, cornerRadius, cornerRadius) - g2.color = foreground - g2.stroke = BasicStroke(1f) - g2.drawRoundRect(0, 0, width - 1, height - 1, cornerRadius, cornerRadius) - g2.dispose() - - val fm = g.fontMetrics - val rect = fm.getStringBounds(text, g) - val textHeight = rect.height.toInt() - val textWidth = rect.width.toInt() - - // Center text horizontally and vertically - val x = (width - textWidth) / 2 - val y = (height - textHeight) / 2 + fm.ascent - g.drawString(text, x, y) - } -} diff --git a/src/main/resources/CodyBundle.properties b/src/main/resources/CodyBundle.properties index 9dc39a0dee..d9e6db1404 100644 --- a/src/main/resources/CodyBundle.properties +++ b/src/main/resources/CodyBundle.properties @@ -29,43 +29,8 @@ status-widget.warning.upgrade=\
\ (Already upgraded to Pro? Restart your IDE for changes to take effect) status-widget.warning.explain=The allowed number of request per day is limited at the moment to ensure the service stays functional. -chat.rate-limit-error.upgrade=\ - \ - You've used up your chat and commands for the month: \ - You've used all chat messages and commands for the month. \ - Upgrade to Cody Pro for unlimited autocompletes, chats, and commands. \ - Upgrade \ - or learn more.

\ - (Already upgraded to Pro? Restart your IDE for changes to take effect)\ - -chat.rate-limit-error.explain=\ - \ - Thank you for using Cody so heavily today! \ - To ensure that Cody can stay operational for all Cody users, please come back tomorrow for more chats, commands, and autocompletes. \ - Learn more.\ - -chat.general-error=\ - \ - ⚠ Error performing this action

\ - Please retry sending your message. If you tried to run a command, try it again.
\ - If the issue persists, please create a support ticket.

\ - Error: {1}\ - -chat.attribution.searching.label=Attribution search -chat.attribution.searching.tooltip=Guardrails: Running Code Attribution Check... -chat.attribution.error.label=Guardrails API Error -chat.attribution.error.tooltip=Guardrails API Error: {0} -chat.attribution.success.label=Guardrails Check Passed -chat.attribution.success.tooltip=Snippet not found on Sourcegraph.com -chat.attribution.failure.label=Guardrails Check Failed -chat.attribution.failure.tooltip=Guardrails Check Failed. Code found in {0} repositories: {1} -my-account-tab.chat-rate-limit-error=You've used all your chat messages and commands for the month. Upgrade to Pro for unlimited usage. -my-account-tab.autocomplete-rate-limit-error=You've used all your autocompletions for the month. Upgrade to Pro for unlimited usage. -my-account-tab.chat-and-autocomplete-rate-limit-error=You've used all your autocompletions and chats for this month. Upgrade to Cody Pro to get unlimited interactions. -my-account-tab.cody-pro-label=Cody Pro my-account-tab.cody-free-label=Cody Free my-account-tab.loading-label=Loading... -my-account-tab.already-pro=(Already upgraded to Pro? Restart your IDE for changes to take effect) commands-tab.message-in-progress=Message generation in progress... UpgradeToCodyProNotification.title.upgrade=You've used up your autocompletes for the month UpgradeToCodyProNotification.title.explain=Thank you for using Cody so heavily today! @@ -76,26 +41,15 @@ UpgradeToCodyProNotification.content.upgrade=\ (Already upgraded to Pro? Restart your IDE for changes to take effect)\ UpgradeToCodyProNotification.content.explain=To ensure that Cody can stay operational for all Cody users, please come back tomorrow for more chats, commands, and autocompletes. -context-panel.button.reindex=Reindex Local Project context-panel.button.help=Help context-panel.in-progress=Running Cody 'Keyword Search' indexer... -context-panel.remote-repo.contact-admin-advertisement=Contact your Sourcegraph admin to add a missing repo context-panel.remote-repo.generic-endpoint-name=a Sourcegraph instance context-panel.remote-repo.error-duplicate-repository=Duplicate repository name context-panel.remote-repo.error-not-found=Repository not found -context-panel.remote-repo.error-resolution-failed.detail=Try setting repositories again so Cody can find code related to your chat session. -context-panel.remote-repo.error-resolution-failed.do-not-show-again=Do not show again -context-panel.remote-repo.error-resolution-failed.ignore=cody.ignore.notification.remote-repo-resolution-failed -context-panel.remote-repo.error-resolution-failed.title=Repository resolution failed context-panel.remote-repo.error-too-many-repositories=Add up to {0} repositories context-panel.remote-repo.error-too-many-repositories.tooltip=Too many repositories -context-panel.remote-repo.select-repo-advertisement=Type to add repos from {0} context-panel.tree.node-auto.tooltip=Included automatically based on your project context-panel.tree.node.checkbox.remove-tooltip=Uncheck to remove from enhanced context -context-panel.tree.node-chat-context=Chat Context -context-panel.tree.node-chat-context.detailed=Enhanced Context {1} {2} -context-panel.tree.node-edit-repos.label-edit=Add or remove repositories -context-panel.tree.node-edit-repos.label-add=Add repositories context-panel.tree.node-ignored.tooltip=Repository restricted by an admin setting context-panel.tree.node-remote-repo.label={1} {2} context-panel.tree.node-remote-repo.auto=(Project repository) @@ -155,15 +109,9 @@ duration.last-year=Last year duration.x-years-ago={0} years ago duration.x-ago={0} ago -popup.select-chat=Select Chat -popup.export-chat=Export Chat As JSON -popup.remove-chat=Remove Chat -popup.remove-all-chats=Remove All Chats export.failed=Cody: Chat export failed. Please retry... export.timed-out=Cody: Chat export timed out. Please retry... -PromptPanel.ask-cody.message=Message (type @ to include specific files as context) -PromptPanel.ask-cody.follow-up-message=Follow-up message (type @ to include specific files as context) LlmDropdown.disabled.text=Start a new chat to change the model # GotItTooltip @@ -184,12 +132,8 @@ filter.action-in-ignored-file.detail=This file has been restricted by an admin. filter.action-in-ignored-file.learn-more-cta=Learn about Context Filters filter.action-in-ignored-file.title=Cody is disabled on this file filter.status-bar-ignored-file.tooltip=This file has been restricted by an admin.\nAutocomplete, commands, and other Cody features are disabled. -filter.sidebar-panel-ignored-file.text=This file has been restricted by an admin, which means commands that rely on its contents cannot be executed. -filter.sidebar-panel-ignored-file.learn-more-cta=Learn more - # Other Actions action.cody.not-working=Cody is disabled or still starting up -chat.enhanced_context.title=Chat Context Settings action.sourcegraph.disabled.description=Log in to Sourcegraph to enable Cody features # Settings Migration diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index a0fcdb98b1..ccade00bf3 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -96,23 +96,6 @@ fieldName="INSTANCE"/> - - - - - - -