-
Notifications
You must be signed in to change notification settings - Fork 23
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
Conversation
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>(); |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
e300708
to
7640a52
Compare
525c415
to
b3cd51c
Compare
b3cd51c
to
71611b9
Compare
bd5e3ea
to
83aa220
Compare
679bea2
to
11a96e7
Compare
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.
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 PlanWill start on e2e integration testing framework next. |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VSC => VSCode, ditto below.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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) : |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 URI
s 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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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:
- It won't have effect on the thread in which callbacks will be called
- You ensure proper thread for callbacks, e.g. in
editTaskDidUpdate
usingonEventThread
, so you are covered
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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}") |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha! Good catch; fixed.
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
5961204
to
16f4a34
Compare
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
There was a problem hiding this 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
:)
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.