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

Edit: Add codelens shortcuts #2757

Merged
merged 18 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ This is a log of all notable changes to Cody for VS Code. [Unreleased] changes a

### Added

- Edit: Added keyboard shortcuts for codelens actions such as "Undo" and "Retry" [pull/2757][https://github.com/sourcegraph/cody/pull/2757]

### Fixed

- Autocomplete: Fixed the completion partial removal upon acceptance caused by `cody.autocomplete.formatOnAccept`. [pull/3083](https://github.com/sourcegraph/cody/pull/3083)
Expand Down
20 changes: 20 additions & 0 deletions vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,26 @@
"command": "cody.autocomplete.manual-trigger",
"key": "alt+\\",
"when": "editorTextFocus && !editorHasSelection && config.cody.autocomplete.enabled && !inlineSuggestionsVisible"
},
{
"command": "cody.fixup.acceptNearest",
"key": "alt+a",
"when": "cody.activated && !editorReadonly && cody.hasActionableEdit"
},
{
"command": "cody.fixup.retryNearest",
"key": "alt+r",
"when": "cody.activated && !editorReadonly && cody.hasActionableEdit"
},
{
"command": "cody.fixup.undoNearest",
"key": "alt+x",
"when": "cody.activated && !editorReadonly && cody.hasActionableEdit"
},
{
"command": "cody.fixup.cancelNearest",
"key": "alt+z",
"when": "cody.activated && !editorReadonly && cody.hasActionableEdit"
}
],
"submenus": [
Expand Down
63 changes: 61 additions & 2 deletions vscode/src/non-stop/FixupController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { countCode } from '../services/utils/code-count'
import { getEditorInsertSpaces, getEditorTabSize } from '../utils'

import { computeDiff, type Diff } from './diff'
import { FixupCodeLenses } from './FixupCodeLenses'
import { FixupCodeLenses } from './codelenses/provider'
import { ContentProvider } from './FixupContentStore'
import { FixupDecorator } from './FixupDecorator'
import { FixupDocumentEditObserver } from './FixupDocumentEditObserver'
Expand All @@ -26,9 +26,10 @@ import { FixupFileObserver } from './FixupFileObserver'
import { FixupScheduler } from './FixupScheduler'
import { FixupTask, type taskID } from './FixupTask'
import type { FixupFileCollection, FixupIdleTaskRunner, FixupTextChanged } from './roles'
import { CodyTaskState } from './utils'
import { CodyTaskState, getMinimumDistanceToRangeBoundary } from './utils'
import { getInput } from '../edit/input/get-input'
import type { AuthProvider } from '../services/AuthProvider'
import { ACTIONABLE_TASK_STATES, CANCELABLE_TASK_STATES } from './codelenses/constants'

// This class acts as the factory for Fixup Tasks and handles communication between the Tree View and editor
export class FixupController
Expand Down Expand Up @@ -104,6 +105,34 @@ export class FixupController
})
telemetryRecorder.recordEvent('cody.fixup.codeLens', 'skipFormatting')
return this.skipFormatting(id)
}),
vscode.commands.registerCommand('cody.fixup.cancelNearest', () => {
const nearestTask = this.getNearestTask({ filter: { states: CANCELABLE_TASK_STATES } })
if (!nearestTask) {
return
}
return vscode.commands.executeCommand('cody.fixup.codelens.cancel', nearestTask.id)
}),
vscode.commands.registerCommand('cody.fixup.acceptNearest', () => {
const nearestTask = this.getNearestTask({ filter: { states: ACTIONABLE_TASK_STATES } })
if (!nearestTask) {
return
}
return vscode.commands.executeCommand('cody.fixup.codelens.accept', nearestTask.id)
}),
vscode.commands.registerCommand('cody.fixup.retryNearest', () => {
const nearestTask = this.getNearestTask({ filter: { states: ACTIONABLE_TASK_STATES } })
if (!nearestTask) {
return
}
return vscode.commands.executeCommand('cody.fixup.codelens.retry', nearestTask.id)
}),
vscode.commands.registerCommand('cody.fixup.undoNearest', () => {
const nearestTask = this.getNearestTask({ filter: { states: ACTIONABLE_TASK_STATES } })
if (!nearestTask) {
return
}
return vscode.commands.executeCommand('cody.fixup.codelens.undo', nearestTask.id)
})
)
// Observe file renaming and deletion
Expand Down Expand Up @@ -946,6 +975,9 @@ export class FixupController
for (const [file, editors] of editorsByFile.entries()) {
this.decorator.didChangeVisibleTextEditors(file, editors)
}

// Update shortcut enablement for visible files
this.codelenses.updateKeyboardShortcutEnablement([...editorsByFile.keys()])
}

private updateDiffs(): void {
Expand Down Expand Up @@ -1130,6 +1162,33 @@ export class FixupController
}
}

private getNearestTask({ filter }: { filter: { states: CodyTaskState[] } }): FixupTask | undefined {
const editor = vscode.window.activeTextEditor
if (!editor) {
return
}

const fixupFile = this.maybeFileForUri(editor.document.uri)
if (!fixupFile) {
return
}

const position = editor.selection.active

/**
* Get the task closest to the current cursor position from the tasks associated with the current file.
*/
const closestTask = this.tasksForFile(fixupFile)
.filter(({ state }) => filter.states.includes(state))
.sort(
(a, b) =>
getMinimumDistanceToRangeBoundary(position, a.selectionRange) -
getMinimumDistanceToRangeBoundary(position, b.selectionRange)
)[0]

return closestTask
}

private reset(): void {
this.tasks = new Map<taskID, FixupTask>()
}
Expand Down
19 changes: 19 additions & 0 deletions vscode/src/non-stop/codelenses/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { CodyTaskState } from '../utils'

export const CANCELABLE_TASK_STATES = [
CodyTaskState.pending,
CodyTaskState.working,
CodyTaskState.inserting,
CodyTaskState.applying,
]

export const ACTIONABLE_TASK_STATES = [
// User can Accept, Undo, Retry, etc
CodyTaskState.applied,
]

/**
* The task states where there is a direct command that the users is likely to action.
* This is used to help enable/disable keyboard shortcuts depending on the states in the document
*/
export const ALL_ACTIONABLE_TASK_STATES = [...ACTIONABLE_TASK_STATES, ...CANCELABLE_TASK_STATES]
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import * as vscode from 'vscode'

import { isRateLimitError } from '@sourcegraph/cody-shared'

import type { FixupTask } from './FixupTask'
import { CodyTaskState } from './utils'
import type { FixupTask } from '../FixupTask'
import { CodyTaskState } from '../utils'

// Create Lenses based on state
export function getLensesForTask(task: FixupTask): vscode.CodeLens[] {
Expand All @@ -17,15 +17,15 @@ export function getLensesForTask(task: FixupTask): vscode.CodeLens[] {
return [title, cancel]
}
case CodyTaskState.inserting: {
let title = getInsertingLens(codeLensRange)
if (isTest) {
title = getUnitTestLens(codeLensRange)
return [getUnitTestLens(codeLensRange)]
}
return [title]
return [getInsertingLens(codeLensRange), getCancelLens(codeLensRange, task.id)]
}
case CodyTaskState.applying: {
const title = getApplyingLens(codeLensRange)
return [title]
const cancel = getCancelLens(codeLensRange, task.id)
return [title, cancel]
}
case CodyTaskState.formatting: {
const title = getFormattingLens(codeLensRange)
Expand Down Expand Up @@ -135,8 +135,9 @@ function getFormattingSkipLens(codeLensRange: vscode.Range, id: string): vscode.

function getCancelLens(codeLensRange: vscode.Range, id: string): vscode.CodeLens {
const lens = new vscode.CodeLens(codeLensRange)
const shortcut = process.platform === 'win32' ? 'Alt+C' : '⌥Z'
umpox marked this conversation as resolved.
Show resolved Hide resolved
lens.command = {
title: 'Cancel',
title: `Cancel [${shortcut}]`,
command: 'cody.fixup.codelens.cancel',
arguments: [id],
}
Expand Down Expand Up @@ -165,8 +166,9 @@ function getAppliedLens(codeLensRange: vscode.Range, id: string): vscode.CodeLen

function getRetryLens(codeLensRange: vscode.Range, id: string): vscode.CodeLens {
const lens = new vscode.CodeLens(codeLensRange)
const shortcut = process.platform === 'win32' ? 'Alt+R' : '⌥R'
umpox marked this conversation as resolved.
Show resolved Hide resolved
lens.command = {
title: 'Retry',
title: `Edit & Retry [${shortcut}]`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you know why this is not affecting the e2e test where we only look for Edit?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Where we look for Retry? I think it's because the text is still there. I'll update the e2e test so it matches exactly

command: 'cody.fixup.codelens.retry',
arguments: [id],
}
Expand All @@ -175,8 +177,9 @@ function getRetryLens(codeLensRange: vscode.Range, id: string): vscode.CodeLens

function getUndoLens(codeLensRange: vscode.Range, id: string): vscode.CodeLens {
const lens = new vscode.CodeLens(codeLensRange)
const shortcut = process.platform === 'win32' ? 'Alt+X' : '⌥X'
umpox marked this conversation as resolved.
Show resolved Hide resolved
lens.command = {
title: 'Undo',
title: `Undo [${shortcut}]`,
command: 'cody.fixup.codelens.undo',
arguments: [id],
}
Expand All @@ -185,8 +188,9 @@ function getUndoLens(codeLensRange: vscode.Range, id: string): vscode.CodeLens {

function getAcceptLens(codeLensRange: vscode.Range, id: string): vscode.CodeLens {
const lens = new vscode.CodeLens(codeLensRange)
const shortcut = process.platform === 'win32' ? 'Alt+A' : '⌥A'
umpox marked this conversation as resolved.
Show resolved Hide resolved
lens.command = {
title: 'Accept',
title: `Accept [${shortcut}]`,
command: 'cody.fixup.codelens.accept',
arguments: [id],
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as vscode from 'vscode'

import { getLensesForTask } from './codelenses'
import type { FixupTask } from './FixupTask'
import type { FixupFileCollection } from './roles'
import { CodyTaskState } from './utils'
import { getLensesForTask } from './items'
import type { FixupTask } from '../FixupTask'
import type { FixupFileCollection } from '../roles'
import { CodyTaskState } from '../utils'
import type { FixupFile } from '../FixupFile'
import { ACTIONABLE_TASK_STATES } from './constants'

export class FixupCodeLenses implements vscode.CodeLensProvider {
private taskLenses = new Map<FixupTask, vscode.CodeLens[]>()
Expand Down Expand Up @@ -39,6 +41,7 @@ export class FixupCodeLenses implements vscode.CodeLensProvider {
}

public didUpdateTask(task: FixupTask): void {
this.updateKeyboardShortcutEnablement([task.fixupFile])
if (task.state === CodyTaskState.finished) {
this.removeLensesFor(task)
return
Expand All @@ -48,6 +51,7 @@ export class FixupCodeLenses implements vscode.CodeLensProvider {
}

public didDeleteTask(task: FixupTask): void {
this.updateKeyboardShortcutEnablement([task.fixupFile])
this.removeLensesFor(task)
}

Expand All @@ -58,6 +62,21 @@ export class FixupCodeLenses implements vscode.CodeLensProvider {
}
}

/**
* For a set of active files, check to see if any tasks within these files are currently actionable.
* If they are, enable the code lens keyboard shortcuts in the editor.
*/
public updateKeyboardShortcutEnablement(activeFiles: FixupFile[]): void {
const allTasks = activeFiles
.filter(file =>
vscode.window.visibleTextEditors.some(editor => editor.document.uri === file.uri)
)
.flatMap(file => this.files.tasksForFile(file))

const hasActionableEdit = allTasks.some(task => ACTIONABLE_TASK_STATES.includes(task.state))
void vscode.commands.executeCommand('setContext', 'cody.hasActionableEdit', hasActionableEdit)
}

private notifyCodeLensesChanged(): void {
this._onDidChangeCodeLenses.fire()
}
Expand Down
27 changes: 27 additions & 0 deletions vscode/src/non-stop/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest'
import * as vscode from 'vscode'

import { getMinimumDistanceToRangeBoundary } from './utils'

describe('getMinimumDistanceToRangeBoundary', () => {
it('returns start distance when position is before range', () => {
const position = new vscode.Position(5, 0)
const range = new vscode.Range(10, 0, 20, 0)
const minDistance = getMinimumDistanceToRangeBoundary(position, range)
expect(minDistance).toBe(5)
})

it('returns end distance when position is after range', () => {
const position = new vscode.Position(25, 0)
const range = new vscode.Range(10, 0, 20, 0)
const minDistance = getMinimumDistanceToRangeBoundary(position, range)
expect(minDistance).toBe(5)
})

it('returns smaller of start and end distances when position is in range', () => {
const position = new vscode.Position(18, 0)
const range = new vscode.Range(10, 0, 20, 0)
const minDistance = getMinimumDistanceToRangeBoundary(position, range)
expect(minDistance).toBe(2)
})
})
14 changes: 14 additions & 0 deletions vscode/src/non-stop/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type * as vscode from 'vscode'

export enum CodyTaskState {
idle = 1,
working = 2,
Expand All @@ -19,3 +21,15 @@ export function isTerminalCodyTaskState(state: CodyTaskState): boolean {
return false
}
}

/**
* Calculates the minimum distance from the given position to the start or end of the provided range.
*/
export function getMinimumDistanceToRangeBoundary(
position: vscode.Position,
range: vscode.Range
): number {
const startDistance = Math.abs(position.line - range.start.line)
const endDistance = Math.abs(position.line - range.end.line)
return Math.min(startDistance, endDistance)
}
Loading