Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation for Inline Edit and Document Code #440

Merged
merged 59 commits into from
Mar 21, 2024

Conversation

steveyegge
Copy link
Contributor

Committing before it is ready because Olaf would like to play with it. Current state: Document Code makes a successful roundtrip to the agent. And we have Edit Instructions dialog. The rest is not wired up yet.

Test plan

Locally tested, but the code is by no means ready to use yet.

@steveyegge
Copy link
Contributor Author

steveyegge commented Jan 30, 2024

Note: Please do not approve or merge this PR.

Update March 18th -- the PR is ready for review & merge.


@JsonRequest("textDocument/edit")
public CompletableFuture<Boolean> textDocumentEdit(TextDocumentEditParams params) {
var future = new CompletableFuture<Boolean>();
Copy link
Contributor

@pkukielka pkukielka Feb 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could be val I think :)
As a side note if you never set result to false and only ned it to indicate there were no error maybe CompletableFuture<Unit> could be sufficient.

init {
// Call validate() initially to ensure the layout is updated
rendererPane.add(component)
component.validate()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General comment so I won't forget later:
be careful with EDT calls in init, as then every object creation of EditCodeInlayRenderer need to be in the EDT as well, and in past we had hard to spot bugs because of this.

) {
// Various failed attempts to get something to render in the inlay area.
rendererPane.bounds = targetRegion
component.setBounds(0, 0, targetRegion.width, targetRegion.height)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably won't be the case in your situation, but please double check CPU consumption when changing bounds or recalculating layout in paint or repaint function.
I introduced nasty bug which spiked our CPU usage significantly in past because of this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure where this is used, but I assume component isn't part of the layout, it is just being used to paint into the renderer pane. So laying it out here should have less potential for mayhem.

@steveyegge steveyegge force-pushed the stevey/inline-edits branch 3 times, most recently from e300708 to 7640a52 Compare February 11, 2024 02:09
@steveyegge steveyegge force-pushed the stevey/inline-edits branch 3 times, most recently from 525c415 to b3cd51c Compare February 20, 2024 01:57
@steveyegge steveyegge force-pushed the stevey/inline-edits branch 5 times, most recently from 679bea2 to 11a96e7 Compare March 16, 2024 07:48
@steveyegge steveyegge changed the title Initial scaffolding for inline fixups. Initial implementation for Inline Edit and Document Code Mar 18, 2024
@steveyegge
Copy link
Contributor Author

This branch now has the full implementation, months in the making, of Document Code and Edit Code. It requires the stevey/extension-client-api Cody PR in order to function properly.

This is essentially just a "Happy Path Only" implementation. There is quite a bit more work left to do, but if you are careful, you can use the feature pretty reliably.

  • Document Code and Edit Code are available from the right-click context menu (only)
  • They are disabled by default; there is a new Setting for enabling them
  • Cancel, Undo, and Accept all work

There are still missing features (e.g. Diff, Retry), issues with concurrent editing tasks, behavioral annoyances (such as not auto-formatting the code after rewriting it), and so on. I will create GH issues for each of them.

Test Plan

Will start on e2e integration testing framework next.

Copy link
Contributor

@dominiccooney dominiccooney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've only looked through half of this but some feedback inline.

CONTRIBUTING.md Outdated
## Debugging VS Code

Sometimes, the Agent behaves differently when called from IntelliJ than
when called from VS Code, and you may need to debug the same code paths
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does in mean for the agent to be called from VSCode? When the extension calls VSCode APIs and hits agent shims?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fair feedback; I'll rewrite it to use the right terminology.

CONTRIBUTING.md Outdated
through the Agent twice. First, when called from the JetBrains extension
side, and again when called from the VS Code extension side.

To accomplish the latter, you can use VSC to debug itself. It works very
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VSC => VSCode, ditto below.

Suggested change
To accomplish the latter, you can use VSC to debug itself. It works very
To accomplish the latter, you can use VSCode to debug itself. It works very

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

CONTRIBUTING.md Outdated

## Known Issues

- Force-stopping the target often corrupts the indexes, requiring an
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naive question but what are "the" indexes?

Nit but can we tighten this up... a list with one bullet, with a sub bullet, and workarounds mentioned in the sub bullet and the next paragraph?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

} else {
logger.warn("No callback registered for textDocument/edit");
}
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this change, but I think a lot of our future hopes and dreams hinge on this boolean.

The VSCode edit API can fail, and this boolean indicates the success or failure of the edit. When we have concurrent edits (in the sense that the Agent's model of the document isn't caught up to the JetBrains reality because the user has edited but the extension sends an edit), we could detect that and fail here.

Punchline is that extension code doesn't handle those failures! We would need to have the extension handle that. But there's probably some not terribly burdensome abstraction, a "retryable edit", that could do that. Interested in your thoughts here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, let's do it -- I've seen it fail to apply once (no code returned by LLM) and would love to get more feedback to the user when this happens.

@Nullable private Consumer<WorkspaceEditParams> onWorkspaceEdit;

public void setOnEditTaskDidUpdate(@Nullable Consumer<EditTask> callback) {
onEditTaskDidUpdate = callback;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine for now, but longer term is this inviting disaster with overwriting a single callback? Should this throw if the callback is already set, or can the wire-up move to some run once initialization step?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say let's remove setOnEditTaskDidUpdate method and move onEditTaskDidUpdate update code into CodyAgentService::onStartup as other similar callback.
Current code won't work is some scenarios, e.g. after agent restart after a crash or account change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but there are 8 of these callbacks (I think my PR only added one), and we should rearchitect all of them in one go. I've added issue 1136 to track it.

fun isZero() = start.isZero() && end.isZero()

companion object {
fun nullRange() = Range(Position(0, 0), Position(0, 0))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mildly nervous about this "null range", "zero position", etc. idea. Because if you start a new file and tell Cody to do stuff, the position is going to be like this.

Could we make the protocol actually use undefined or null for missing, and trust when zeros do appear that we meant it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies; there were 3 unused methods here and this was one. I've done a sweep for other warnings as well.


// Brings up a diff view showing the changes the AI made.
override fun diff() {
// The FixupController issues a vscode.diff command to show the smart diff in the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer true. The VSCode code lens UI does this, but we're using the "client UI" now and I made the vscode.diff an implementation detail of the VSCode code lens UI. (Of course, it is software, we can do whatever we want. But that's where the Agent/extension/VSCode side changes are at right now.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

import javax.swing.CellRendererPane
import javax.swing.JPanel

class EditCodeInlayRenderer(private val component: JPanel, private val requiredHeight: Int) :
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to be dense, but where is this used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was a leftover, not used - removed.

) {
// Various failed attempts to get something to render in the inlay area.
rendererPane.bounds = targetRegion
component.setBounds(0, 0, targetRegion.width, targetRegion.height)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure where this is used, but I assume component isn't part of the layout, it is just being used to paint into the renderer pane. So laying it out here should have less potential for mayhem.

component.setBounds(0, 0, targetRegion.width, targetRegion.height)
component.validate()
rendererPane.validate()
rendererPane.background = JBColor.WHITE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theming?

val type: String,

// Valid for replace & delete.
val range: Range? = null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make sense to create two different types of edits, one position based and one renge based? This way we don't need this comments and all fields would always be required and used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably, but it's a bigger change; maybe we can wait until after this PR is merged?


data class WorkspaceEditOperation(
val type: String, // all
val uri: String? = null, // created, delete, edit
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make sure URI parameters are sent as URIs and not strings?
Generally I think we should be extra careful around uri/path objects, they were source of many conversion problems in paths between VSCode <-> Intellij, especially adding additional platforms as windows.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They have been bouncing back and forth in origin/main between URI and String and causing me no end of merge headaches. My understanding is that URI has trouble serializing in some cases.

It works now, and I don't want to break it, so let's plan an API cleanup pass afterwards.

import javax.swing.event.DocumentListener

/** Pop up a user interface for giving Cody instructions to fix up code at the cursor. */
class EditCommandPrompt(val controller: FixupService, val editor: Editor, val dialogTitle: String) {
Copy link
Contributor

@pkukielka pkukielka Mar 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please annotate methods which requires dispatch thread with @RequiresEdt?
It can give us additional safety net in the newest IJ versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!


init {
// JetBrains docs say avoid heavy lifting in the constructor, so pass to another thread.
backgroundThread {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodyAgentService.withAgent(project) spawns actions on background thread itself, so you don't need to additionally wrap it in backgroundThread.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - fixed!

// JetBrains docs say avoid heavy lifting in the constructor, so pass to another thread.
backgroundThread {
CodyAgentService.withAgent(project) { agent ->
ApplicationManager.getApplication().invokeLater {
Copy link
Contributor

@pkukielka pkukielka Mar 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That invokeLater seems to be unnecessary:

  1. It won't have effect on the thread in which callbacks will be called
  2. You ensure proper thread for callbacks, e.g. in editTaskDidUpdate using onEventThread, so you are covered

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch - was the result of some refactoring. Fixed.

}

// TODO: get model list from protocol
fun getModels(): List<String> = listOf("GPT-4", "GPT-3.5")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI we are already doing it for LLM selector so maybe you can reuse that code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes, forgot that's not wired up yet -- I've added a GH issue to track it. Thanks.


// We only use this for multiplexing task updates from the Agent to concurrent sessions.
// TODO: Consider doing the multiplexing in CodyAgentClient instead.
private var activeSessions: MutableMap<String, FixupSession> = mutableMapOf()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to have more than one fixup running at the same time, e.g. in different chat window?
If so, this most likely need to be changed to be multithread-safe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is, and there's a tracking bug to fix all the concurrency issues with the existing code. (Or, if we don't have time to get it done for release, we need to block concurrent edit tasks.)

for (op in params.operations) {
when (op.type) {
"create-file" -> {
logger.warn("Workspace edit operation created a file: ${op.uri}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should those be warnings or just info?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're really TODOs in disguise. Create/Rename/Delete file ops need to be supported before the first release, and there is a tracking issue for it.

override fun doExecute(editor: Editor, where: Caret?, dataContext: DataContext?) {
val fixupService = editor.project?.getService(FixupService::class.java)
if (fixupService == null) {
logger.warn("FixupService not found")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not really FixupService which was missing, but rather Project, right?
So maybe something like:

val project = editor.project
if (project == null) {
    logger.warn("No project found, cannot run DocumentCodeActionHandler::startDocumentCode")
} else {
    FixupService.getInstance(project).startDocumentCode(editor)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha! Good catch; fixed.

abeatrix and others added 21 commits March 20, 2024 08:03
Fortunately turned out to be a simple restructuring to pass the caret in.
we handle the case where we receive a workspace/edit before the commands/document request returns
Improved the positioning of the lenses and doc string:

- we request folding ranges synchronously, in order to place the first lens

- we nudge the output of IndentationBasedFoldingRangeProvider slightly
ensure the lenses are always visible when first shown
They don't appear if the text field has the focus.
So we have to start with something else having the focus
(the model combo box was convenient), and have it pass
the focus back to the text field when they start typing.
Factored it a bit better, and cleaned it up
it was being discarded with the dialogs - moved to Companion
Inline edits are now hidden by default, and must be enabled with the new CODY_JETBRAINS_FEATURES environment variable.

To enable inline edits:  CODY_JETBRAINS_FEATURES=cody.feature.inline-edits=true
 e.g. in your Run Configuration
@steveyegge steveyegge requested review from pkukielka and removed request for olafurpg March 20, 2024 18:38
Copy link
Contributor

@pkukielka pkukielka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's merge it and do a remaining work on main :)

@steveyegge steveyegge merged commit f734679 into main Mar 21, 2024
5 checks passed
@steveyegge steveyegge deleted the stevey/inline-edits branch March 21, 2024 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants