Skip to content

Commit

Permalink
initial scaffolding for inline edits
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
steveyegge committed Feb 6, 2024
1 parent 67c6910 commit e300708
Show file tree
Hide file tree
Showing 39 changed files with 1,000 additions and 57 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
# IntelliJ project
*.iml

# User local IDEA run configurations
.run/

# Build output & caches for IntelliJ plugin development
build/
idea-sandbox/
Expand Down
9 changes: 6 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -29,7 +29,8 @@ public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile f
(Computable<Document>)
() -> 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);
}
});
Expand All @@ -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())));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
67 changes: 56 additions & 11 deletions src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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<EditTask> onEditTaskDidChange = null;

private Consumer<TextDocumentEditParams> onTextDocumentEdit;

public void setOnEditTaskDidChange(Consumer<EditTask> callback) {
onEditTaskDidChange = callback;
}

@JsonNotification("editTaskState/didChange")
public void editTaskStateDidChange(EditTask params) {
onEditTaskDidChange.accept(params);
}

public void setOnTextDocumentEdit(Consumer<TextDocumentEditParams> callback) {
onTextDocumentEdit = callback;
}

@JsonRequest("textDocument/edit")
public CompletableFuture<Boolean> textDocumentEdit(TextDocumentEditParams params) {
var future = new CompletableFuture<Boolean>();
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 <T> CompletableFuture<T> onEventThread(Supplier<T> handler) {
private <T> @NotNull CompletableFuture<T> onEventThread(Supplier<T> handler) {
CompletableFuture<T> result = new CompletableFuture<>();
ApplicationManager.getApplication()
.invokeLater(
Expand All @@ -49,24 +94,24 @@ private <T> CompletableFuture<T> onEventThread(Supplier<T> handler) {
return result;
}

// Webviews
@JsonRequest("webview/create")
public CompletableFuture<Void> 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<Void> 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
Expand Down
6 changes: 3 additions & 3 deletions src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class CodyAgentCodebase(val project: Project) {
companion object {
@JvmStatic
fun getInstance(project: Project): CodyAgentCodebase {
return project.service<CodyAgentCodebase>()
return project.service()
}
}
}
15 changes: 11 additions & 4 deletions src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -91,6 +96,8 @@ interface CodyAgentServer {

@JsonRequest("commands/smell") fun commandsSmell(): CompletableFuture<String>

@JsonRequest("commands/document") fun commandsDocument(): CompletableFuture<EditTask>

@JsonRequest("chat/new") fun chatNew(): CompletableFuture<String>

@JsonRequest("chat/submitMessage")
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.sourcegraph.cody.agent.protocol

data class DisplayCodeLensParams(
val uri: String,
val codeLenses: List<ProtocolCodeLens>)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.sourcegraph.cody.agent.protocol

data class EditTask(val id: String, val state: CodyTaskState)
13 changes: 12 additions & 1 deletion src/main/kotlin/com/sourcegraph/cody/agent/protocol/Position.kt
Original file line number Diff line number Diff line change
@@ -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
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.sourcegraph.cody.agent.protocol

data class ProtocolCodeLens(
val range: Range,
val command: ProtocolCommand? = null,
val isResolved: Boolean
)
Original file line number Diff line number Diff line change
@@ -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<*>
)
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand All @@ -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)
}
}
}
9 changes: 8 additions & 1 deletion src/main/kotlin/com/sourcegraph/cody/agent/protocol/Range.kt
Original file line number Diff line number Diff line change
@@ -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<Int, Int> {
return Pair(start.toOffset(document), end.toOffset(document))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.sourcegraph.cody.agent.protocol

data class TextDocumentEditOptions(val undoStopBefore: Boolean, val undoStopAfter: Boolean)
Loading

0 comments on commit e300708

Please sign in to comment.