-
Notifications
You must be signed in to change notification settings - Fork 297
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
V1 new client extension API, for inline edits + document code #3445
Conversation
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.
Some minor feedback inline, PTAL. LGTM.
There's a format for bylines in commit descriptions. Also need to add @abeatrix here for the edit command:
Co-authored-by: Dominic Cooney <[email protected]>
Co-authored-by: Beatrix <[email protected]>
this.registerNotification('textDocument/didFocus', document => { | ||
this.registerNotification('textDocument/didFocus', (document: ProtocolTextDocument) => { | ||
function isEmpty(range: Range | undefined): boolean { | ||
return !range || range === new vscode.Range(0, 0, 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.
This ===
isn't right; that's strict object identity just like JavaScript so trivially false.
vscode.Range has something called isEmpty that means the range start and end are the same, but that's not the intent here, so let's call it something else. (Since this comparison was wrong, can we just delete it? It means the 0-width range at (0,0)-(0,0) is still a valid thing.)
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!
return !range || range === new vscode.Range(0, 0, 0, 0) | ||
} | ||
const documentWithUri = ProtocolTextDocumentWithUri.fromDocument(document) | ||
// If the caller elided the content, as is the sensible thing to do, reconstruct it 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.
Elide the judgement "as is the sensible thing to do" and just state it plainly, like "because the caller elides the content" if that always happens; "when the caller omits the content" if it may happen, etc.
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.
Fair enough; elided. I do actually want to have it be recommended to client writers, as part of a broader effort to document the expected semantics of all the request and response objects. Not sure exactly where yet.
'cody.command.edit-code', | ||
args | ||
) | ||
// Wrap the task in a Thenable that returns a CommandResult, required by createEditTask(). |
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.
In my experience, no good comes of providing Promise-like APIs when Promises are available, yet here we are...
This should be more succinct by not reiterating the types, and relying on failures to propagate through without mentioning them explicitly. Something like the following but with proper indentation.
return this.createEditTask(
vscode.commands.executeCommand<FixupTask | undefined>('cody.command.edit-code', args)
.then(task => task && { type: 'edit', task })
)
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.
Works like a charm.
export async function executeEditCommand( | ||
args: ExecuteEditArguments | ||
): Promise<EditCommandResult | undefined> { | ||
return wrapInActiveSpan('command.test', async span => { |
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 span identifier looks like copypasta from the test command.
return wrapInActiveSpan('command.test', async span => { | ||
span.setAttribute('sampled', true) | ||
const instruction = args.configuration?.instruction // get the instruction | ||
const editor = getEditor()?.active // get the active 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.
Just chain them, const document = getEditor()?.active.document
(Sorry for not using GitHub suggestions but something is wrong with multiline select in the web GitHub UI rn.)
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 was able to fix it and commit the fix.
document, | ||
mode: 'edit', | ||
}, | ||
source: DefaultEditCommands.Test, |
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 source also looks iffy for "edit" (or the source is right and the method name is iffy.)
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 all this code, which was definitely dubious.
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.
document, | ||
mode: 'edit', | ||
}, | ||
source: DefaultEditCommands.Test, |
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.
source: DefaultEditCommands.Test, | |
source: args.source || 'editor' |
I think we can use editor here as the default source here
|
||
import { wrapInActiveSpan } from '@sourcegraph/cody-shared/src/tracing' | ||
|
||
export async function executeEditCommand( |
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.
Prehaps we can just call the executeEdit function directly without this? (I might have missed some context so please ignore me if this is needed)
edit: Oh right, we wanted to move away from calling vs code command directly so yea ignore this :)
Fixed a serious bug with document contents disappearing in the Agent. The problem was that textDocument/didFocus would simply move the document from the wire into the workspace's document map for that URI. JetBrains was not sending the document content along with every didFocus, because it clutters the logs significantly, and the contents have not changed. I fixed this on the Agent side by copying in the existing document's content if none was passed. Also added 'editTask/getFoldingRanges' request to expose the indentation-based folding ranges API to clients (such as JetBrains) which cannot reliably compute folding ranges in a language-neutral way. This fixed a bug with not knowing the initial position for the spinner while we wait for the FixupController to reply to our initial commands/document request.
textDocument/didFocus was blowing away the cached selection as well as the cached document content. Changed to copy in the cached selection if none is present in the protocol document.
It was assuming that cody.commands.edit-code was returning an EditCommandResult, but it was just returning a FixupTask. So I had to write a little adapter.
Co-authored-by: Dominic Cooney <[email protected]>
vscode/src/extension.common.ts
Outdated
@@ -67,5 +67,5 @@ export async function activate( | |||
console.error(error) | |||
} | |||
|
|||
return api | |||
return new ExtensionApi(context.extensionMode) |
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.
return new ExtensionApi(context.extensionMode) | |
return api |
reverting this change fixed the integration test
} | ||
|
||
export async function activate( | ||
context: vscode.ExtensionContext, | ||
platformContext: PlatformContext | ||
): Promise<ExtensionApi> { | ||
const api = new ExtensionApi(context.extensionMode) |
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.
reverting this change fixed the integration test
8803528
to
c6ca5b8
Compare
Co-authored-by: Steve Yegge <[email protected]>
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.
Finally got chance to review this, great work @steveyegge and @dominiccooney. I think these are sensible abstractions 👍
private readonly authProvider: AuthProvider, | ||
client: ExtensionClient | ||
) { | ||
this.controlApplicator = client.createFixupControlApplicator(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 like this abstraction 👍
hasV2Event: true, | ||
} | ||
) | ||
telemetryRecorder.recordEvent('cody.fixup.codeLens', 'cancel') |
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.
Assuming that we are expecting that each client implements their own telemetry (I think this is fine)
export interface FixupActor { | ||
/** | ||
* Mark a task as accepted and stop tracking the task. Only applicable to | ||
* tasks in the "applied" state. Sets the task state to "finished" and | ||
* discards the task. | ||
*/ | ||
accept(task: FixupTask): void | ||
|
||
/** | ||
* Undo a task's edits and stop tracking the task. Only applicable to | ||
* tasks in the "applied" state. If the undo succeeds, the task state is | ||
* set to "finished" and the task is discarded. | ||
*/ | ||
undo(task: FixupTask): Promise<void> | ||
|
||
/** | ||
* Cancel a task. Sets the task state to "error" or "finished" and stops | ||
* tracking the task. Tasks in any state can be cancelled. | ||
*/ | ||
cancel(task: FixupTask): void | ||
|
||
/** | ||
* Undo the task (see `undo`), prompt for updated instructions, and start | ||
* a new task to try again. Only applicable to tasks in the "applied" state. | ||
* @param task the task to retry. | ||
* @param source the source of the retry, for event logging. | ||
*/ | ||
retry(task: FixupTask, source: ChatEventSource): Promise<FixupTask | undefined> | ||
} |
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.
Awesome
// An interface for decorating fixup tasks with controls. | ||
export interface FixupControlApplicator extends vscode.Disposable { | ||
didUpdateTask(task: FixupTask): void | ||
didDeleteTask(task: FixupTask): void | ||
// Called when visible files changed. | ||
// TODO: This API design is gross: this is *not* called when a new task | ||
// is created in a file that is already visible. It *is* called every time | ||
// visible files change, so be prepared to handle repeated calls with | ||
// an empty or unchanged set of files efficiently. Unearth a consistent | ||
// API here. | ||
visibleFilesWithTasksMaybeChanged(files: readonly FixupFile[]): void | ||
} | ||
|
||
// A FixupControlApplicator which does not present any controls for fixup | ||
// tasks. | ||
export class NullFixupControlApplicator implements FixupControlApplicator { | ||
public didUpdateTask(task: FixupTask): void {} | ||
public didDeleteTask(task: FixupTask): void {} | ||
public visibleFilesWithTasksMaybeChanged(files: readonly FixupFile[]): void {} | ||
public dispose(): void {} | ||
} |
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.
Just a general comment, ideally we should use /** doc */
for doc strings, as they are attached for hovers
// TODO: This API design is gross: this is *not* called when a new task | ||
// is created in a file that is already visible. It *is* called every time | ||
// visible files change, so be prepared to handle repeated calls with | ||
// an empty or unchanged set of files efficiently. Unearth a consistent | ||
// API here. | ||
visibleFilesWithTasksMaybeChanged(files: readonly FixupFile[]): void |
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.
We do need to listen to when the visible files change, regardless of if they are empty or not. This is currently used purely to enable/disable keyboard shortcuts when there is a visible edit task (in one of the visible open files). Just listening for a task being created is not sufficient here
This commit is part 1 of a joint effort between @dominiccooney and @steveyegge to make our client API simpler, more robust, and easier to test. The main change from the client's perspective is that the client no longer has to receive lenses as part of the protocol, focusing only on state transitions. This enabled a significant cleanup of the JetBrains-side logic.
The FixupController has been rewritten to use Strategy objects to abstract the operations and decouple the UI. There are several bug fixes in the Agent, as well as a new getFoldingRanges API for clients that need it. It's useful for figuring out the very first place to put a spinner, before the FixupController's task has finished initializing.
The commit goes hand in hand with stevey/inline-edits on the jetbrains repo, which implements v1 of Document Code and Edit Code.
Test plan
We will start immediately on the integration test framework for JetBrains. This commit is just a checkpoint so that we can start parallelizing some of this work.