From e30070813e25d83875bf1559b2c73ccfcacc898e Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Mon, 5 Feb 2024 20:53:58 -0800 Subject: [PATCH] initial scaffolding for inline edits This code is WIP and is being pushed up to the branch so Olaf can play with it Current state is that it should be making successful roundtrips now, but is not robust yet, and the UI and editing are not wired up. --- .gitignore | 3 + CONTRIBUTING.md | 9 +- .../cody/CodyFileDocumentManagerListener.kt | 19 ++ .../cody/CodyFileEditorListener.java | 9 +- .../cody/CodyFocusChangeListener.java | 4 +- .../cody/agent/CodyAgentClient.java | 67 ++++- .../com/sourcegraph/cody/agent/CodyAgent.kt | 6 +- .../cody/agent/CodyAgentCodebase.kt | 2 +- .../sourcegraph/cody/agent/CodyAgentServer.kt | 15 +- .../cody/agent/protocol/ClientCapabilities.kt | 10 + .../cody/agent/protocol/ClientInfo.kt | 6 +- .../cody/agent/protocol/CodyTaskState.kt | 20 ++ .../agent/protocol/DisplayCodeLensParams.kt | 5 + .../cody/agent/protocol/EditTask.kt | 3 + .../cody/agent/protocol/Position.kt | 13 +- .../cody/agent/protocol/ProtocolCodeLens.kt | 7 + .../cody/agent/protocol/ProtocolCommand.kt | 8 + ...extDocument.kt => ProtocolTextDocument.kt} | 10 +- .../sourcegraph/cody/agent/protocol/Range.kt | 9 +- .../agent/protocol/TextDocumentEditOptions.kt | 3 + .../agent/protocol/TextDocumentEditParams.kt | 9 + .../cody/agent/protocol/TextEdit.kt | 15 ++ .../autocomplete/CodyAutocompleteManager.kt | 19 +- .../autocomplete/CodyEditorFactoryListener.kt | 5 +- .../cody/edit/CodeSmellsActionHandler.kt | 25 ++ .../cody/edit/DocumentCodeActionHandler.kt | 25 ++ .../cody/edit/DocumentCommandSession.kt | 104 ++++++++ .../cody/edit/EditCodeActionHandler.kt | 24 ++ .../cody/edit/EditCodeInlayRenderer.kt | 61 +++++ .../cody/edit/EditCommandPrompt.kt | 231 ++++++++++++++++++ .../cody/edit/EditCommandSession.kt | 29 +++ .../cody/edit/ExplainCodeActionHandler.kt | 25 ++ .../edit/GenerateUnitTestsActionHandler.kt | 25 ++ .../cody/edit/InlineFixupCommandSession.kt | 60 +++++ .../com/sourcegraph/cody/edit/InlineFixups.kt | 80 ++++++ .../cody/edit/NewChatActionHandler.kt | 24 ++ .../com/sourcegraph/cody/vscode/Position.kt | 12 +- .../com/sourcegraph/utils/CodyProjectUtil.kt | 2 + src/main/resources/META-INF/plugin.xml | 54 +++- 39 files changed, 1000 insertions(+), 57 deletions(-) create mode 100644 src/main/java/com/sourcegraph/cody/CodyFileDocumentManagerListener.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/agent/protocol/ClientCapabilities.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/agent/protocol/CodyTaskState.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/agent/protocol/DisplayCodeLensParams.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/agent/protocol/EditTask.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolCodeLens.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolCommand.kt rename src/main/kotlin/com/sourcegraph/cody/agent/protocol/{TextDocument.kt => ProtocolTextDocument.kt} (64%) create mode 100644 src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextDocumentEditOptions.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextDocumentEditParams.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextEdit.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/CodeSmellsActionHandler.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/DocumentCodeActionHandler.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/DocumentCommandSession.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/EditCodeActionHandler.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/EditCodeInlayRenderer.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/EditCommandSession.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/ExplainCodeActionHandler.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/GenerateUnitTestsActionHandler.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/InlineFixupCommandSession.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/InlineFixups.kt create mode 100644 src/main/kotlin/com/sourcegraph/cody/edit/NewChatActionHandler.kt diff --git a/.gitignore b/.gitignore index 32f50b1ca7..f2811e0bd8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ # IntelliJ project *.iml +# User local IDEA run configurations +.run/ + # Build output & caches for IntelliJ plugin development build/ idea-sandbox/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91b00f289b..1d24326b01 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -163,12 +163,15 @@ There are two supported configurations for debugging this way: - The Cody extension connects via socket to the "remote" agent Option 1 is the simplest, and probably makes the most sense for you -to use if you are uncertain which method to use for debugging. +to use if you are uncertain which method to use for debugging. Option 2 +is especially useful when you need to set a breakpoint very early in +the Agent startup. ## How to set up Run Configurations -Run configurations are basically IDEA's launcher scripts. You will need to create one -run configuration in each project window, using Run → Edit Configurations. +Run configurations are basically IDEA's launcher scripts. You will need +to create one run configuration in each project window, using Run → Edit +Configurations. For both debugging setups (Cody-spawns and JB-spawned), you will need: diff --git a/src/main/java/com/sourcegraph/cody/CodyFileDocumentManagerListener.kt b/src/main/java/com/sourcegraph/cody/CodyFileDocumentManagerListener.kt new file mode 100644 index 0000000000..492565fd4c --- /dev/null +++ b/src/main/java/com/sourcegraph/cody/CodyFileDocumentManagerListener.kt @@ -0,0 +1,19 @@ +package com.sourcegraph.cody + +import com.intellij.openapi.editor.Document +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.FileDocumentManagerListener +import com.intellij.openapi.project.Project +import com.sourcegraph.cody.agent.CodyAgentService +import com.sourcegraph.cody.agent.protocol.ProtocolTextDocument + +class CodyFileDocumentManagerListener(val project: Project) : FileDocumentManagerListener { + + override fun beforeDocumentSaving(document: Document) { + CodyAgentService.applyAgentOnBackgroundThread(project) { agent -> + FileDocumentManager.getInstance().getFile(document)?.path?.let { path -> + agent.server.textDocumentDidSave(ProtocolTextDocument.fromPath(path)) + } + } + } +} diff --git a/src/main/java/com/sourcegraph/cody/CodyFileEditorListener.java b/src/main/java/com/sourcegraph/cody/CodyFileEditorListener.java index 63d322124a..9d0a55caf1 100644 --- a/src/main/java/com/sourcegraph/cody/CodyFileEditorListener.java +++ b/src/main/java/com/sourcegraph/cody/CodyFileEditorListener.java @@ -9,7 +9,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.sourcegraph.cody.agent.CodyAgentCodebase; import com.sourcegraph.cody.agent.CodyAgentService; -import com.sourcegraph.cody.agent.protocol.TextDocument; +import com.sourcegraph.cody.agent.protocol.ProtocolTextDocument; import com.sourcegraph.config.ConfigUtil; import org.jetbrains.annotations.NotNull; @@ -29,7 +29,8 @@ public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile f (Computable) () -> FileDocumentManager.getInstance().getDocument(file)); if (document != null) { - TextDocument textDocument = TextDocument.fromPath(file.getPath(), document.getText()); + ProtocolTextDocument textDocument = + ProtocolTextDocument.fromPath(file.getPath(), document.getText()); agent.getServer().textDocumentDidOpen(textDocument); } }); @@ -42,9 +43,9 @@ public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile f if (!ConfigUtil.isCodyEnabled()) { return; } - CodyAgentService.applyAgentOnBackgroundThread( source.getProject(), - agent -> agent.getServer().textDocumentDidClose(TextDocument.fromPath(file.getPath()))); + agent -> + agent.getServer().textDocumentDidClose(ProtocolTextDocument.fromPath(file.getPath()))); } } diff --git a/src/main/java/com/sourcegraph/cody/CodyFocusChangeListener.java b/src/main/java/com/sourcegraph/cody/CodyFocusChangeListener.java index 9d31dbaa3d..5fe0d55174 100644 --- a/src/main/java/com/sourcegraph/cody/CodyFocusChangeListener.java +++ b/src/main/java/com/sourcegraph/cody/CodyFocusChangeListener.java @@ -11,7 +11,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.sourcegraph.cody.agent.CodyAgentCodebase; import com.sourcegraph.cody.agent.CodyAgentService; -import com.sourcegraph.cody.agent.protocol.TextDocument; +import com.sourcegraph.cody.agent.protocol.ProtocolTextDocument; import com.sourcegraph.config.ConfigUtil; import org.jetbrains.annotations.NotNull; @@ -54,7 +54,7 @@ public void focusGained(@NotNull Editor editor) { Thread.sleep(100); } catch (InterruptedException ignored) { } - agent.getServer().textDocumentDidFocus(TextDocument.fromPath(file.getPath())); + agent.getServer().textDocumentDidFocus(ProtocolTextDocument.fromPath(file.getPath())); }); CodyAgentCodebase.getInstance(project).onFileOpened(project, file); diff --git a/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java b/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java index b58484ba94..70664cf60a 100644 --- a/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java +++ b/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java @@ -5,13 +5,18 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.sourcegraph.cody.agent.protocol.DebugMessage; -import java.lang.ref.WeakReference; + import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Supplier; + +import com.sourcegraph.cody.agent.protocol.DisplayCodeLensParams; +import com.sourcegraph.cody.agent.protocol.EditTask; +import com.sourcegraph.cody.agent.protocol.TextDocumentEditParams; 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. */ @@ -30,12 +35,52 @@ public class CodyAgentClient { @Nullable public Editor editor; + // List of callbacks for the "editTaskState/didChange" notification. + // This enables multiple concurrent inline editing tasks. + private Consumer onEditTaskDidChange = null; + + private Consumer onTextDocumentEdit; + + public void setOnEditTaskDidChange(Consumer callback) { + onEditTaskDidChange = callback; + } + + @JsonNotification("editTaskState/didChange") + public void editTaskStateDidChange(EditTask params) { + onEditTaskDidChange.accept(params); + } + + public void setOnTextDocumentEdit(Consumer callback) { + onTextDocumentEdit = callback; + } + + @JsonRequest("textDocument/edit") + public CompletableFuture textDocumentEdit(TextDocumentEditParams params) { + var future = new CompletableFuture(); + ApplicationManager.getApplication() + .invokeLater( + () -> { + try { + onTextDocumentEdit.accept(params); + future.complete(true); + } catch (Error e) { + future.completeExceptionally(e); + } + }); + return future; + } + + @JsonNotification("codeLenses/display") + public void codeLensesDisplay(DisplayCodeLensParams params) { + logger.info("codeLensesDisplay"); + } + /** * 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 CompletableFuture onEventThread(Supplier handler) { + private @NotNull CompletableFuture onEventThread(Supplier handler) { CompletableFuture result = new CompletableFuture<>(); ApplicationManager.getApplication() .invokeLater( @@ -49,24 +94,24 @@ private CompletableFuture onEventThread(Supplier handler) { return result; } + // 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(DebugMessage msg) { + public void debugMessage(@NotNull DebugMessage msg) { logger.warn(String.format("%s: %s", msg.getChannel(), msg.getMessage())); } - // 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); - } - @JsonNotification("webview/postMessage") - public void webviewPostMessage(WebviewPostMessageParams params) { + public void webviewPostMessage(@NotNull WebviewPostMessageParams params) { ExtensionMessage extensionMessage = params.getMessage(); if (onNewMessage != null diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index ef63e98e22..2e712b06d9 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -106,9 +106,9 @@ private constructor( .initialize( ClientInfo( version = ConfigUtil.getPluginVersion(), - workspaceRootUri = - ConfigUtil.getWorkspaceRootPath(project).toUri().toString(), - extensionConfiguration = ConfigUtil.getAgentConfiguration(project))) + workspaceRootUri = ConfigUtil.getWorkspaceRootPath(project).toUri(), + extensionConfiguration = ConfigUtil.getAgentConfiguration(project), + capabilities = ClientCapabilities(edit = "enabled", codeLenses = "enabled"))) .thenApply { info -> logger.info("Connected to Cody agent " + info.name) server.initialized() diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentCodebase.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentCodebase.kt index d0212d1c00..e6e60d430a 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentCodebase.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentCodebase.kt @@ -34,7 +34,7 @@ class CodyAgentCodebase(val project: Project) { companion object { @JvmStatic fun getInstance(project: Project): CodyAgentCodebase { - return project.service() + return project.service() } } } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt index 87fdd4e04f..c389839817 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt @@ -56,13 +56,18 @@ interface CodyAgentServer { @JsonNotification("extensionConfiguration/didChange") fun configurationDidChange(document: ExtensionConfiguration) - @JsonNotification("textDocument/didFocus") fun textDocumentDidFocus(document: TextDocument) + @JsonNotification("textDocument/didFocus") + fun textDocumentDidFocus(document: ProtocolTextDocument) - @JsonNotification("textDocument/didOpen") fun textDocumentDidOpen(document: TextDocument) + @JsonNotification("textDocument/didOpen") fun textDocumentDidOpen(document: ProtocolTextDocument) - @JsonNotification("textDocument/didChange") fun textDocumentDidChange(document: TextDocument) + @JsonNotification("textDocument/didChange") + fun textDocumentDidChange(document: ProtocolTextDocument) - @JsonNotification("textDocument/didClose") fun textDocumentDidClose(document: TextDocument) + @JsonNotification("textDocument/didClose") + fun textDocumentDidClose(document: ProtocolTextDocument) + + @JsonNotification("textDocument/didSave") fun textDocumentDidSave(document: ProtocolTextDocument) @JsonNotification("debug/message") fun debugMessage(message: DebugMessage) @@ -91,6 +96,8 @@ interface CodyAgentServer { @JsonRequest("commands/smell") fun commandsSmell(): CompletableFuture + @JsonRequest("commands/document") fun commandsDocument(): CompletableFuture + @JsonRequest("chat/new") fun chatNew(): CompletableFuture @JsonRequest("chat/submitMessage") diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ClientCapabilities.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ClientCapabilities.kt new file mode 100644 index 0000000000..1521e574e5 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ClientCapabilities.kt @@ -0,0 +1,10 @@ +package com.sourcegraph.cody.agent.protocol + +data class ClientCapabilities( + var completions: String? = null, + var chat: String? = null, + var git: String? = null, + var progressBars: String? = null, + var edit: String? = null, + var codeLenses: String? = null, +) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ClientInfo.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ClientInfo.kt index 926b8f117e..6ef5b13c54 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ClientInfo.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ClientInfo.kt @@ -1,11 +1,13 @@ package com.sourcegraph.cody.agent.protocol import com.sourcegraph.cody.agent.ExtensionConfiguration +import java.net.URI data class ClientInfo( var version: String, - var workspaceRootUri: String, - var extensionConfiguration: ExtensionConfiguration? = null + var workspaceRootUri: URI, + var extensionConfiguration: ExtensionConfiguration? = null, + var capabilities: ClientCapabilities? = null, ) { val name = "JetBrains" } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/CodyTaskState.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/CodyTaskState.kt new file mode 100644 index 0000000000..92528e86fe --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/CodyTaskState.kt @@ -0,0 +1,20 @@ +package com.sourcegraph.cody.agent.protocol + +enum class CodyTaskState(val value: Int) { + idle(1), + working(2), + inserting(3), + applying(4), + formatting(5), + applied(6), + finished(7), + error(8), + pending(9) +} + +val CodyTaskState.isTerminal + get() = when(this) { + CodyTaskState.finished, + CodyTaskState.error -> true + else -> false + } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/DisplayCodeLensParams.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/DisplayCodeLensParams.kt new file mode 100644 index 0000000000..6296687372 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/DisplayCodeLensParams.kt @@ -0,0 +1,5 @@ +package com.sourcegraph.cody.agent.protocol + +data class DisplayCodeLensParams( + val uri: String, + val codeLenses: List) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/EditTask.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/EditTask.kt new file mode 100644 index 0000000000..c5bc08178d --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/EditTask.kt @@ -0,0 +1,3 @@ +package com.sourcegraph.cody.agent.protocol + +data class EditTask(val id: String, val state: CodyTaskState) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/Position.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/Position.kt index 8890e65ef6..2c1fa55f1f 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/Position.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/Position.kt @@ -1,3 +1,14 @@ package com.sourcegraph.cody.agent.protocol -data class Position(val line: Int, val character: Int) +import com.intellij.openapi.editor.Document + +data class Position(val line: Int, val character: Int) { + + /** Return zero-based offset of this position in the document. */ + fun toOffset(document: Document): Int { + val lineStartOffset = document.getLineStartOffset(line) + return lineStartOffset + character + } + +} + diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolCodeLens.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolCodeLens.kt new file mode 100644 index 0000000000..a9fbbfdb69 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolCodeLens.kt @@ -0,0 +1,7 @@ +package com.sourcegraph.cody.agent.protocol + +data class ProtocolCodeLens( + val range: Range, + val command: ProtocolCommand? = null, + val isResolved: Boolean +) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolCommand.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolCommand.kt new file mode 100644 index 0000000000..18a112ba3d --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolCommand.kt @@ -0,0 +1,8 @@ +package com.sourcegraph.cody.agent.protocol + +data class ProtocolCommand( + val title: String, + val command: String, + val tooltip: String? = null, + val arguments: List<*> +) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextDocument.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolTextDocument.kt similarity index 64% rename from src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextDocument.kt rename to src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolTextDocument.kt index a3e972e8f0..9561b2db59 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextDocument.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/ProtocolTextDocument.kt @@ -3,7 +3,7 @@ package com.sourcegraph.cody.agent.protocol import com.sourcegraph.cody.agent.protocol.util.Rfc3986UriEncoder import java.nio.file.Paths -class TextDocument +class ProtocolTextDocument private constructor( var uri: String, var content: String?, @@ -14,10 +14,14 @@ private constructor( @JvmStatic @JvmOverloads - fun fromPath(path: String, content: String? = null, selection: Range? = null): TextDocument { + fun fromPath( + path: String, + content: String? = null, + selection: Range? = null + ): ProtocolTextDocument { val uri = Paths.get(path).toUri().toString() val rfc3986Uri = Rfc3986UriEncoder.encode(uri) - return TextDocument(rfc3986Uri, content, selection) + return ProtocolTextDocument(rfc3986Uri, content, selection) } } } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/Range.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/Range.kt index d0e61b9a1e..97b047fe7a 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/Range.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/Range.kt @@ -1,3 +1,10 @@ package com.sourcegraph.cody.agent.protocol -data class Range(val start: Position, val end: Position) +import com.intellij.openapi.editor.Document + +data class Range(val start: Position, val end: Position) { + + fun toOffsets(document: Document): Pair { + return Pair(start.toOffset(document), end.toOffset(document)) + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextDocumentEditOptions.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextDocumentEditOptions.kt new file mode 100644 index 0000000000..0a5f5de640 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextDocumentEditOptions.kt @@ -0,0 +1,3 @@ +package com.sourcegraph.cody.agent.protocol + +data class TextDocumentEditOptions(val undoStopBefore: Boolean, val undoStopAfter: Boolean) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextDocumentEditParams.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextDocumentEditParams.kt new file mode 100644 index 0000000000..02363c31cb --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextDocumentEditParams.kt @@ -0,0 +1,9 @@ +package com.sourcegraph.cody.agent.protocol + +data class TextDocumentEditParams( + // TODO: Should we include a task id? + // Otherwise, the agent can pretty much write at will to any open file. + 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 new file mode 100644 index 0000000000..f8904caeed --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/TextEdit.kt @@ -0,0 +1,15 @@ +package com.sourcegraph.cody.agent.protocol + +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/autocomplete/CodyAutocompleteManager.kt b/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyAutocompleteManager.kt index aa5782b88f..c2761debba 100644 --- a/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyAutocompleteManager.kt +++ b/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyAutocompleteManager.kt @@ -138,11 +138,8 @@ class CodyAutocompleteManager { isCommandExcluded(currentCommand)) { return } - val project = editor.project - if (project == null) { - logger.warn("triggered autocomplete with null project") - return - } + val project = editor.project ?: return logger.warn("triggered autocomplete with null project") + val textDocument: TextDocument = IntelliJTextDocument(editor, project) if (isTriggeredExplicitly && CodyAuthenticationManager.instance.hasNoActiveAccount(project)) { @@ -252,7 +249,7 @@ class CodyAutocompleteManager { null } .completeOnTimeout(null, 3, TimeUnit.SECONDS) - .thenRun { + .thenRun { // This is a terminal operation, so we needn't call get(). resetApplication(project) resultOuter.complete(null) } @@ -264,13 +261,11 @@ class CodyAutocompleteManager { private fun handleError(project: Project, error: Throwable?) { if (error is ResponseErrorException) { - val errorCode = error.toErrorCode() - if (errorCode == ErrorCode.RateLimitError) { - val rateLimitError = error.toRateLimitError() - UpgradeToCodyProNotification.autocompleteRateLimitError.set(rateLimitError) + if (error.toErrorCode() == ErrorCode.RateLimitError) { + UpgradeToCodyProNotification.autocompleteRateLimitError.set(error.toRateLimitError()) UpgradeToCodyProNotification.isFirstRLEOnAutomaticAutocompletionsShown = true ApplicationManager.getApplication().executeOnPooledThread { - UpgradeToCodyProNotification.notify(rateLimitError, project) + UpgradeToCodyProNotification.notify(error.toRateLimitError(), project) CodyToolWindowContent.executeOnInstanceIfNotDisposed(project) { refreshSubscriptionTab() } } } @@ -303,7 +298,6 @@ class CodyAutocompleteManager { } cancellationToken.dispose() clearAutocompleteSuggestions(editor) - // https://github.com/sourcegraph/jetbrains/issues/350 // CodyFormatter.formatStringBasedOnDocument needs to be on a write action. WriteCommandAction.runWriteCommandAction(editor.project) { @@ -318,6 +312,7 @@ class CodyAutocompleteManager { * The reason we have a custom code path to render hints for agent autocompletions is because we * can use `insertText` directly and the `range` encloses the entire line. */ + @RequiresEdt fun displayAgentAutocomplete( editor: Editor, offset: Int, diff --git a/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt b/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt index 80d1109d56..a5b484675c 100644 --- a/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt +++ b/src/main/kotlin/com/sourcegraph/cody/autocomplete/CodyEditorFactoryListener.kt @@ -13,8 +13,8 @@ import com.sourcegraph.cody.agent.CodyAgentCodebase import com.sourcegraph.cody.agent.CodyAgentService import com.sourcegraph.cody.agent.protocol.CompletionItemParams import com.sourcegraph.cody.agent.protocol.Position +import com.sourcegraph.cody.agent.protocol.ProtocolTextDocument import com.sourcegraph.cody.agent.protocol.Range -import com.sourcegraph.cody.agent.protocol.TextDocument import com.sourcegraph.cody.autocomplete.CodyAutocompleteManager.Companion.instance import com.sourcegraph.cody.autocomplete.action.AcceptCodyAutocompleteAction import com.sourcegraph.cody.vscode.InlineCompletionTriggerKind @@ -163,7 +163,8 @@ class CodyEditorFactoryListener : EditorFactoryListener { afterUpdate: () -> Unit = {} ) { val file = FileDocumentManager.getInstance().getFile(editor.document) ?: return - val document = TextDocument.fromPath(file.path, editor.document.text, getSelection(editor)) + val document = + ProtocolTextDocument.fromPath(file.path, editor.document.text, getSelection(editor)) val project = editor.project!! if (hasFileChanged) { diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/CodeSmellsActionHandler.kt b/src/main/kotlin/com/sourcegraph/cody/edit/CodeSmellsActionHandler.kt new file mode 100644 index 0000000000..be1afc3a8e --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/CodeSmellsActionHandler.kt @@ -0,0 +1,25 @@ +package com.sourcegraph.cody.edit + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorAction +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.intellij.openapi.project.DumbAware +import com.sourcegraph.cody.autocomplete.action.CodyAction + +class CodeSmellsAction : EditorAction(CodeSmellsActionHandler()), CodyAction, DumbAware + +class CodeSmellsActionHandler : EditorActionHandler() { + private val logger = Logger.getInstance(CodeSmellsActionHandler::class.java) + + override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { + return caret.hasSelection() // TODO: Make less restrictive + } + + override fun doExecute(editor: Editor, where: Caret?, dataContext: DataContext?) { + // TODO + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/DocumentCodeActionHandler.kt b/src/main/kotlin/com/sourcegraph/cody/edit/DocumentCodeActionHandler.kt new file mode 100644 index 0000000000..4fed9be628 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/DocumentCodeActionHandler.kt @@ -0,0 +1,25 @@ +package com.sourcegraph.cody.edit + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorAction +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.intellij.openapi.project.DumbAware +import com.sourcegraph.cody.autocomplete.action.CodyAction +import com.sourcegraph.utils.CodyEditorUtil + +class DocumentCodeAction : EditorAction(DocumentCodeActionHandler()), CodyAction, DumbAware + +class DocumentCodeActionHandler : EditorActionHandler() { + private val logger = Logger.getInstance(DocumentCodeActionHandler::class.java) + + override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { + return CodyEditorUtil.isEditorValidForAutocomplete(editor) // close enough for now + } + + override fun doExecute(editor: Editor, where: Caret?, dataContext: DataContext?) { + InlineFixups.instance.documentCode(editor) + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/DocumentCommandSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/DocumentCommandSession.kt new file mode 100644 index 0000000000..82a9108cea --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/DocumentCommandSession.kt @@ -0,0 +1,104 @@ +package com.sourcegraph.cody.edit + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.sourcegraph.cody.agent.CodyAgentCodebase +import com.sourcegraph.cody.agent.CodyAgentService +import com.sourcegraph.cody.agent.protocol.CodyTaskState +import com.sourcegraph.cody.agent.protocol.EditTask +import com.sourcegraph.cody.agent.protocol.isTerminal +import com.sourcegraph.cody.vscode.CancellationToken +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + +class DocumentCommandSession(editor: Editor, cancellationToken: CancellationToken) : + InlineFixupCommandSession(editor, cancellationToken) { + private val logger = Logger.getInstance(DocumentCommandSession::class.java) + + init { + triggerDocumentCodeAsync() + } + + // TODO: Refactor this boilerplate into a utility class that generates all this stuff. + private fun triggerDocumentCodeAsync(): CompletableFuture { + val project = editor.project!! + val asyncRequest = CompletableFuture() + CodyAgentService.applyAgentOnBackgroundThread(project) { agent -> + workAroundUninitializedCodebase(editor) + val response = agent.server.commandsDocument() + cancellationToken.onCancellationRequested { response.cancel(true) } + + ApplicationManager.getApplication().executeOnPooledThread { + response + .handle { result, error -> + if (error != null || result == null) { + logger.warn("Error while generating doc string: $error") + } else { + beginTrackingTask(editor, result) + } + null + } + .exceptionally { error: Throwable? -> + logger.warn("Error while generating doc string: $error") + null + } + .completeOnTimeout(null, 3, TimeUnit.SECONDS) + .thenRun { asyncRequest.complete(null) } + } + } + + cancellationToken.onCancellationRequested { asyncRequest.cancel(true) } + return asyncRequest + } + + // We're consistently triggering the 'retrieved codebase context before initialization' error + // in ContextProvider.ts. It's a different initialization path from completions & chat. + // Calling onFileOpened forces the right initialization path. + private fun workAroundUninitializedCodebase(editor: Editor) { + val file = FileDocumentManager.getInstance().getFile(editor.document)!! + val project = editor.project!! + CodyAgentCodebase.getInstance(project).onFileOpened(project, file) + } + + private fun beginTrackingTask(editor: Editor, task: EditTask) { + taskId = task.id + // TODO: (in super) + // Add listeners for notifications from the agent. + // - progress updates (didChange - EditTask) + // - textDocument/edit - perform update + CodyAgentService.applyAgentOnBackgroundThread(editor.project!!) { agent -> + agent.client.setOnEditTaskDidChange { task -> + if (task.id != taskId) return@setOnEditTaskDidChange + if (task.state.isTerminal) { + cancellationToken.abort() // TODO: necessary? + // If we're finished, we close up shop for this listener, + // and wait for the editing notification to arrive. + if (task.state == CodyTaskState.finished) { + logger.warn("Finished task $taskId") + // TODO: Remove progress indicator + } else { + logger.warn("TODO: Handle error case") + } + } else { + logger.warn("Progress update for task $taskId: ${task.state}") + } + } + agent.client.setOnTextDocumentEdit { params -> + if (params.uri != FileDocumentManager.getInstance().getFile(editor.document)?.path) { + logger.warn( + "DocumentCommand session received notification for wrong document: ${params.uri}") + } else { + performInlineEdits(params) + } + } + } + } + + override fun cancel() { + logger.warn("Cancelling DocumentCommandSession -- TODO") + } + + override fun getLogger() = logger +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/EditCodeActionHandler.kt b/src/main/kotlin/com/sourcegraph/cody/edit/EditCodeActionHandler.kt new file mode 100644 index 0000000000..f0e0fcea3d --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/EditCodeActionHandler.kt @@ -0,0 +1,24 @@ +package com.sourcegraph.cody.edit + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorAction +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.intellij.openapi.project.DumbAware +import com.sourcegraph.cody.autocomplete.action.CodyAction + +class EditCodeAction : EditorAction(EditCodeActionHandler()), CodyAction, DumbAware + +class EditCodeActionHandler : EditorActionHandler() { + private val logger = Logger.getInstance(EditCodeActionHandler::class.java) + + override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { + return !editor.isDisposed + } + + override fun doExecute(editor: Editor, where: Caret?, dataContext: DataContext?) { + InlineFixups.instance.startCodeEdit(editor, where) + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/EditCodeInlayRenderer.kt b/src/main/kotlin/com/sourcegraph/cody/edit/EditCodeInlayRenderer.kt new file mode 100644 index 0000000000..185816912d --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/EditCodeInlayRenderer.kt @@ -0,0 +1,61 @@ +package com.sourcegraph.cody.edit + +import com.intellij.openapi.editor.EditorCustomElementRenderer +import com.intellij.openapi.editor.Inlay +import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.ui.JBColor +import java.awt.Graphics +import java.awt.Rectangle +import javax.swing.CellRendererPane +import javax.swing.JPanel + +class EditCodeInlayRenderer(private val component: JPanel, private val requiredHeight: Int) : + EditorCustomElementRenderer { + + private val rendererPane = CellRendererPane() + + init { + // Call validate() initially to ensure the layout is updated + rendererPane.add(component) + component.validate() + } + + override fun calcWidthInPixels(inlay: Inlay<*>): Int { + // TODO: compute the width based on the line width or editor viewport width + return maxOf(component.preferredSize.width, MAX_EDITOR_WIDTH) + } + + override fun calcHeightInPixels(inlay: Inlay<*>): Int { + return requiredHeight + } + + override fun paint( + inlay: Inlay<*>, + g: Graphics, + targetRegion: Rectangle, + textAttributes: TextAttributes + ) { + // Various failed attempts to get something to render in the inlay area. + rendererPane.bounds = targetRegion + component.setBounds(0, 0, targetRegion.width, targetRegion.height) + component.validate() + rendererPane.validate() + rendererPane.background = JBColor.WHITE + component.background = JBColor.orange + + // Paint the component using the rendererPane + rendererPane.paintComponent( + g, + component, + null, + targetRegion.x, + targetRegion.y, + targetRegion.width, + targetRegion.height, + true) + } + + companion object { + private const val MAX_EDITOR_WIDTH = 500 + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt b/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt new file mode 100644 index 0000000000..2630ac95be --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandPrompt.kt @@ -0,0 +1,231 @@ +package com.sourcegraph.cody.edit + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.wm.WindowManager +import com.intellij.ui.components.fields.ExpandableTextField +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent +import javax.swing.JComponent +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.SwingUtilities +import javax.swing.event.DocumentEvent +import javax.swing.event.DocumentListener + +/** Pop up a user interface for giving Cody instructions to fix up code at the cursor. */ +class EditCommandPrompt(val editor: Editor) { + private val logger = Logger.getInstance(EditCommandPrompt::class.java) + private val offset = editor.caretModel.primaryCaret.offset + private val controller = InlineFixups.instance + private val promptHistory = mutableListOf() + + private var dialog: EditCommandPrompt.InstructionsDialog? = null + + private val instructionsField = + ExpandableTextField().apply { + val screenWidth = getScreenWidth(editor) + val preferredWidth = minOf(screenWidth / 2, DEFAULT_TEXT_FIELD_WIDTH) + preferredSize = Dimension(preferredWidth, preferredSize.height) + minimumSize = Dimension(preferredWidth, minimumSize.height) + emptyText.text = "Instructions (@ to include code)" + } + + lateinit var modelComboBox: ComboBox + + // History navigation helper + private val historyCursor = HistoryCursor() + + init { + setupTextField() + setupKeyListener() + } + + fun displayPromptUI() { + ApplicationManager.getApplication().invokeLater { + val dlg = dialog + if (dlg == null || dlg.isDisposed) { + dialog = InstructionsDialog() + } + dialog?.show() + } + } + + fun getText(): String = instructionsField.text + + private fun setupTextField() { + instructionsField.document.addDocumentListener( + object : DocumentListener { + override fun insertUpdate(e: DocumentEvent?) { + handleDocumentChange() + } + + override fun removeUpdate(e: DocumentEvent?) { + handleDocumentChange() + } + + override fun changedUpdate(e: DocumentEvent?) { + handleDocumentChange() + } + + private fun handleDocumentChange() { + ApplicationManager.getApplication().invokeLater { + updateOkButtonState() + checkForInterruptions() + } + } + }) + } + + private fun updateOkButtonState() { + dialog?.isOKActionEnabled = instructionsField.text.isNotBlank() + } + + private fun checkForInterruptions() { + if (editor.isDisposed || editor.isViewer || !editor.document.isWritable) { + dialog?.apply { + close(DialogWrapper.CANCEL_EXIT_CODE) + disposeIfNeeded() + } + } + } + + private fun setupKeyListener() { + instructionsField.addKeyListener( + object : KeyAdapter() { + override fun keyPressed(e: KeyEvent) { + when (e.keyCode) { + KeyEvent.VK_UP -> fetchPreviousHistoryItem() + KeyEvent.VK_DOWN -> fetchNextHistoryItem() + } + updateOkButtonState() + } + }) + } + + private fun fetchPreviousHistoryItem() { + updateTextFromHistory(historyCursor.getPreviousHistoryItem()) + } + + private fun fetchNextHistoryItem() { + updateTextFromHistory(historyCursor.getNextHistoryItem()) + } + + private fun updateTextFromHistory(text: String) { + instructionsField.text = text + instructionsField.caretPosition = text.length + updateOkButtonState() + } + + private inner class HistoryCursor { + private var historyIndex = -1 + + fun getPreviousHistoryItem() = getHistoryItemByDelta(-1) + + fun getNextHistoryItem() = getHistoryItemByDelta(1) + + private fun getHistoryItemByDelta(delta: Int): String { + if (promptHistory.isNotEmpty()) { + historyIndex = (historyIndex + delta).coerceIn(0, promptHistory.size - 1) + return promptHistory[historyIndex] + } + return "" + } + } + + private inner class InstructionsDialog : + DialogWrapper(editor.project, false, IdeModalityType.MODELESS) { + init { + init() + title = "Edit Code with Cody" + + instructionsField.text = controller.getLastPrompt() + updateOkButtonState() + } + + override fun getPreferredFocusedComponent() = instructionsField + + override fun createCenterPanel(): JComponent { + val result = generatePromptUI(offset) + updateOkButtonState() + return result + } + + override fun doOKAction() { + val text = instructionsField.text + val model = modelComboBox.item + super.doOKAction() + controller.setCurrentModel(model) + if (text.isNotBlank()) { + addToHistory(text) + EditCommandSession(editor, text, controller.resetCancellationToken()) + } + } + + override fun doCancelAction() { + super.doCancelAction() + dialog?.disposeIfNeeded() + dialog = null + } + } // InstructionsDialog + + fun addToHistory(prompt: String) { + if (prompt.isNotBlank() && !promptHistory.contains(prompt)) { + promptHistory.add(prompt) + } + } + + fun getHistory(): List { + return promptHistory + } + + private fun generatePromptUI(offset: Int): JPanel { + val root = JPanel(BorderLayout()) + + val topRow = JPanel(BorderLayout()) + val (line, col) = editor.offsetToLogicalPosition(offset).let { Pair(it.line, it.column) } + val file = FileDocumentManager.getInstance().getFile(editor.document)?.name ?: "unknown file" + topRow.add(JLabel("Editing $file at $line:$col"), BorderLayout.CENTER) + + val southRow = JPanel(BorderLayout()) + + val historyLabel = + JLabel().apply { + text = + if (promptHistory.isNotEmpty()) { + "↑↓ for history" + } else { + "" + } + font = font.deriveFont(font.size - 1f) + } + southRow.add(historyLabel, BorderLayout.CENTER) + + modelComboBox = ComboBox(controller.getModels().toTypedArray()) + modelComboBox.selectedItem = controller.getCurrentModel() + southRow.add(modelComboBox, BorderLayout.EAST) + + root.add(topRow, BorderLayout.NORTH) + root.add(southRow, BorderLayout.SOUTH) + root.add(instructionsField, BorderLayout.CENTER) + + return root + } + + private fun getScreenWidth(editor: Editor): Int { + val frame = WindowManager.getInstance().getIdeFrame(editor.project) + val screenSize = frame?.component?.let { SwingUtilities.getWindowAncestor(it).size } + return screenSize?.width ?: DEFAULT_TEXT_FIELD_WIDTH + } + + companion object { + // TODO: make this smarter + const val DEFAULT_TEXT_FIELD_WIDTH: Int = 620 + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandSession.kt new file mode 100644 index 0000000000..2aed42f9bc --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/EditCommandSession.kt @@ -0,0 +1,29 @@ +package com.sourcegraph.cody.edit + +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Editor +import com.sourcegraph.cody.vscode.CancellationToken + +/** + * Manages the state machine for inline-edit requests. + * + * @param instructions The user's instructions for fixing up the code. + */ +class EditCommandSession( + editor: Editor, + val instructions: String, + cancellationToken: CancellationToken +) : InlineFixupCommandSession(editor, cancellationToken) { + private val logger = Logger.getInstance(EditCommandSession::class.java) + + override fun cancel() { + TODO("Not yet implemented") + } + + override fun getLogger() = logger + + private fun sendRequest(prompt: String, model: String) { + logger.info("Sending inline-edit request: $prompt") + // TODO: This will be very similar to DocumentCommandSession.sendRequest() + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/ExplainCodeActionHandler.kt b/src/main/kotlin/com/sourcegraph/cody/edit/ExplainCodeActionHandler.kt new file mode 100644 index 0000000000..b450dcd331 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/ExplainCodeActionHandler.kt @@ -0,0 +1,25 @@ +package com.sourcegraph.cody.edit + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorAction +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.intellij.openapi.project.DumbAware +import com.sourcegraph.cody.autocomplete.action.CodyAction + +class ExplainCodeAction : EditorAction(ExplainCodeActionHandler()), CodyAction, DumbAware + +class ExplainCodeActionHandler : EditorActionHandler() { + private val logger = Logger.getInstance(ExplainCodeActionHandler::class.java) + + override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { + return caret.hasSelection() // TODO: Make less restrictive + } + + override fun doExecute(editor: Editor, where: Caret?, dataContext: DataContext?) { + // TODO + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/GenerateUnitTestsActionHandler.kt b/src/main/kotlin/com/sourcegraph/cody/edit/GenerateUnitTestsActionHandler.kt new file mode 100644 index 0000000000..7ecafa4d6f --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/GenerateUnitTestsActionHandler.kt @@ -0,0 +1,25 @@ +package com.sourcegraph.cody.edit + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorAction +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.intellij.openapi.project.DumbAware +import com.sourcegraph.cody.autocomplete.action.CodyAction + +class GenerateUnitTestsAction : EditorAction(GenerateUnitTestsActionHandler()), CodyAction, DumbAware + +class GenerateUnitTestsActionHandler : EditorActionHandler() { + private val logger = Logger.getInstance(GenerateUnitTestsActionHandler::class.java) + + override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { + return caret.hasSelection() // TODO: Make less restrictive + } + + override fun doExecute(editor: Editor, where: Caret?, dataContext: DataContext?) { + // TODO + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/InlineFixupCommandSession.kt b/src/main/kotlin/com/sourcegraph/cody/edit/InlineFixupCommandSession.kt new file mode 100644 index 0000000000..26455e6075 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/InlineFixupCommandSession.kt @@ -0,0 +1,60 @@ +package com.sourcegraph.cody.edit + +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.sourcegraph.cody.agent.protocol.TextDocumentEditParams +import com.sourcegraph.cody.agent.protocol.TextEdit +import com.sourcegraph.cody.vscode.CancellationToken + +/** + * Common functionality for commands that let the agent edit the code inline, + * such as adding a doc string, or fixing up a region according to user instructions. + */ +abstract class InlineFixupCommandSession(val editor: Editor, val cancellationToken: CancellationToken) { + protected val controller = InlineFixups.instance + + protected var taskId: String? = null + + abstract fun cancel() + + abstract fun getLogger(): Logger + + fun performInlineEdits(params: TextDocumentEditParams) { + if (!controller.isEligibleForInlineEdit(editor)) { + getLogger().warn("Inline edit not eligible") + return + } + WriteCommandAction.runWriteCommandAction(editor.project ?: return) { + for (edit in params.edits) { + val doc: Document = editor.document + // TODO: handle options if present (currently just undo bounds) + when (edit.type) { + "replace" -> performReplace(doc, edit) + "insert" -> performInsert(doc, edit) + "delete" -> performDelete(doc, edit) + else -> getLogger().warn("Unknown edit type: ${edit.type}") + } + } + } + // TODO: Fix up selection. + editor.caretModel.primaryCaret.removeSelection() + } + + private fun performReplace(doc: Document, edit: TextEdit) { + val (start, end) = edit.range?.toOffsets(doc) ?: return + doc.replaceString(start, end, edit.value ?: return) + } + + private fun performInsert(doc: Document, edit: TextEdit) { + val start = edit.range?.start?.toOffset(doc) ?: return + doc.insertString(start, edit.value ?: return) + } + + private fun performDelete(doc: Document, edit: TextEdit) { + val (start, end) = edit.range?.toOffsets(doc) ?: return + doc.deleteString(start, end) + } + +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/InlineFixups.kt b/src/main/kotlin/com/sourcegraph/cody/edit/InlineFixups.kt new file mode 100644 index 0000000000..c240134e86 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/InlineFixups.kt @@ -0,0 +1,80 @@ +package com.sourcegraph.cody.edit + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.sourcegraph.cody.vscode.CancellationToken +import com.sourcegraph.config.ConfigUtil.isCodyEnabled +import com.sourcegraph.utils.CodyEditorUtil +import java.util.concurrent.atomic.AtomicReference + +/** Controller for commands that allow the LLM to edit the code directly. */ +@Service +class InlineFixups { + private val logger = Logger.getInstance(InlineFixups::class.java) + private val currentJob = AtomicReference(CancellationToken().apply { abort() }) + private var activeSession: InlineFixupCommandSession? = null + private var currentModel = "GPT-3.5" // last selected from dropdown + + // The last text the user typed in without saving it, for continuity. + private var lastPrompt: String = "" + + private fun cancelCurrentSession() { + activeSession?.cancel() + } + + private fun setSession(session: InlineFixupCommandSession?) { + cancelCurrentSession() + activeSession = session + } + + // Prompt user for instructions for editing selected code. + fun startCodeEdit(editor: Editor, where: Caret?) { + if (!isEligibleForInlineEdit(editor)) return + where ?: editor.caretModel.primaryCaret + EditCommandPrompt(editor).displayPromptUI() + } + + // Generate and insert a doc string for the current code. + fun documentCode(editor: Editor) { + // Check eligibility before we send the request, and also when we get the response. + if (!isEligibleForInlineEdit(editor)) return + setSession(DocumentCommandSession(editor, resetCancellationToken())) + } + + fun resetCancellationToken(): CancellationToken { + currentJob.get().abort() + return CancellationToken().apply { currentJob.set(this) } + } + + fun isEligibleForInlineEdit(editor: Editor): Boolean { + if (!isCodyEnabled()) { + logger.warn("Edit code invoked when Cody not enabled") + return false + } + if (!CodyEditorUtil.isEditorValidForAutocomplete(editor)) { + logger.warn("Inline edit invoked when editing not available") + return false + } + return true + } + + // TODO: get model list from protocol + fun getModels(): List = listOf("GPT-4", "GPT-3.5") + + fun getCurrentModel(): String = currentModel + + fun setCurrentModel(model: String) { + currentModel = model + } + + fun getLastPrompt(): String = lastPrompt + + companion object { + @JvmStatic + val instance: InlineFixups + get() = service() + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/edit/NewChatActionHandler.kt b/src/main/kotlin/com/sourcegraph/cody/edit/NewChatActionHandler.kt new file mode 100644 index 0000000000..e2b409b159 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/edit/NewChatActionHandler.kt @@ -0,0 +1,24 @@ +package com.sourcegraph.cody.edit + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorAction +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.intellij.openapi.project.DumbAware +import com.sourcegraph.cody.autocomplete.action.CodyAction + +class NewChatAction : EditorAction(NewChatActionHandler()), CodyAction, DumbAware + +class NewChatActionHandler : EditorActionHandler() { + private val logger = Logger.getInstance(NewChatActionHandler::class.java) + + override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { + return caret.hasSelection() // TODO: Make less restrictive + } + + override fun doExecute(editor: Editor, where: Caret?, dataContext: DataContext?) { + // TODO + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/vscode/Position.kt b/src/main/kotlin/com/sourcegraph/cody/vscode/Position.kt index ca91b13a23..5bd4351352 100644 --- a/src/main/kotlin/com/sourcegraph/cody/vscode/Position.kt +++ b/src/main/kotlin/com/sourcegraph/cody/vscode/Position.kt @@ -1,3 +1,13 @@ package com.sourcegraph.cody.vscode -data class Position(@JvmField val line: Int, @JvmField val character: Int) +import com.intellij.openapi.editor.Document + +data class Position(@JvmField val line: Int, @JvmField val character: Int) { + + /** Returns zero-based document offset for this position. */ + fun toOffset(document: Document): Int { + val lineStartOffset = document.getLineStartOffset(line) + return lineStartOffset + character + } + +} diff --git a/src/main/kotlin/com/sourcegraph/utils/CodyProjectUtil.kt b/src/main/kotlin/com/sourcegraph/utils/CodyProjectUtil.kt index bdc06cfcb5..99c107c533 100644 --- a/src/main/kotlin/com/sourcegraph/utils/CodyProjectUtil.kt +++ b/src/main/kotlin/com/sourcegraph/utils/CodyProjectUtil.kt @@ -1,7 +1,9 @@ package com.sourcegraph.utils import com.intellij.ide.lightEdit.LightEdit +import com.intellij.openapi.editor.Document import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager; object CodyProjectUtil { @JvmStatic diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 618c3399d3..55ab3d2d6a 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -67,7 +67,7 @@ - + @@ -143,8 +143,7 @@ + class="com.sourcegraph.cody.autocomplete.action.AcceptCodyAutocompleteAction"> + + + + + + + + + + + + + + + + + + - - - + icon="/icons/codyLogoSm.svg" searchable="false" + class="com.sourcegraph.cody.CodyActionGroup"> + + + + + + + + @@ -235,5 +273,7 @@ class="com.sourcegraph.cody.editor.CodyLookupListener"/> +