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

V1 new client extension API, for inline edits + document code #3445

Merged
merged 18 commits into from
Mar 21, 2024

Conversation

steveyegge
Copy link
Contributor

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.

agent/src/agent.ts Outdated Show resolved Hide resolved
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.

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)
Copy link
Contributor

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.)

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!

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.
Copy link
Contributor

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.

Copy link
Contributor Author

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.

agent/src/agent.ts Outdated Show resolved Hide resolved
'cody.command.edit-code',
args
)
// Wrap the task in a Thenable that returns a CommandResult, required by createEditTask().
Copy link
Contributor

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 })
)

Copy link
Contributor Author

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 => {
Copy link
Contributor

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
Copy link
Contributor

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.)

Copy link
Contributor Author

@steveyegge steveyegge Mar 20, 2024

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,
Copy link
Contributor

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.)

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 all this code, which was definitely dubious.

Copy link
Contributor

@abeatrix abeatrix left a comment

Choose a reason for hiding this comment

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

From my testing I can confirm that all edit commands are working correctly!

Doc command:
image

Edit command:
image

Test command:
image

document,
mode: 'edit',
},
source: DefaultEditCommands.Test,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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(
Copy link
Contributor

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 :)

@steveyegge steveyegge enabled auto-merge (squash) March 20, 2024 14:57
@abeatrix abeatrix disabled auto-merge March 21, 2024 05:05
dominiccooney and others added 17 commits March 21, 2024 14:55
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]>
@@ -67,5 +67,5 @@ export async function activate(
console.error(error)
}

return api
return new ExtensionApi(context.extensionMode)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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)
Copy link
Contributor

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

@dominiccooney dominiccooney force-pushed the stevey/extension-client-api-rebased branch from 8803528 to c6ca5b8 Compare March 21, 2024 06:24
@dominiccooney dominiccooney merged commit 5356bb5 into main Mar 21, 2024
20 checks passed
@dominiccooney dominiccooney deleted the stevey/extension-client-api-rebased branch March 21, 2024 06:33
dominiccooney added a commit that referenced this pull request Mar 21, 2024
…#3445)"

This reverts commit 5356bb5.

This is missing code review feedback which went to
stevey/extension-client-api branch.
dominiccooney added a commit that referenced this pull request Mar 21, 2024
Copy link
Contributor

@umpox umpox left a 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)
Copy link
Contributor

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')
Copy link
Contributor

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)

Comment on lines +14 to +42
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>
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome

Comment on lines +8 to +28
// 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 {}
}
Copy link
Contributor

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

Comment on lines +13 to +18
// 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
Copy link
Contributor

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

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