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

Custom Command: supports keybinding registration #3242

Merged
merged 11 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all 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: 1 addition & 1 deletion .vscode/cody.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@
}
}
}
}
}
2 changes: 1 addition & 1 deletion lib/shared/src/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export interface CodyCommand {
* - 'ask' mode is the default mode, run prompt in chat view
* - 'edit' mode will run prompt with edit command which replace selection with cody's response
* - 'insert' mode is the same as edit, it adds to the top of the selection instead of replacing selection
* - 'file' mode create a new file with cody's response as content
* - 'file' mode create a new file with cody's response as content - not supported yet
*/
type CodyCommandMode = 'ask' | 'edit' | 'insert' | 'file'

Expand Down
6 changes: 5 additions & 1 deletion vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ This is a log of all notable changes to Cody for VS Code. [Unreleased] changes a
### Added

- Chat: Adds support for line ranges with @-mentioned files (Example: `Explain @src/README.md:1-5`). [pull/3174](https://github.com/sourcegraph/cody/pull/3174)
- Chat: Command prompts are now editable and compatible with @ mentions. [pull/3243](https://github.com/sourcegraph/cody/pull/3243)
- Commands: Updated the prompts for the `Explain Code` and `Find Code Smell` commands to include file ranges. [pull/3243](https://github.com/sourcegraph/cody/pull/3243)
- Custom Command: All custom commands are now listed individually under the `Custom Commands` section in the Cody sidebar. [pull/3245](https://github.com/sourcegraph/cody/pull/3245)
- Custom Commands: You can now assign keybindings to individual custom commands. Simply search for `cody.command.custom.{CUSTOM_COMMAND_NAME}` (e.g. `cody.command.custom.commit`) in the Keyboard Shortcuts editor to add keybinding. [pull/3242](https://github.com/sourcegraph/cody/pull/3242)

### Fixed

Expand All @@ -33,7 +37,7 @@ This is a log of all notable changes to Cody for VS Code. [Unreleased] changes a

### Changed

- Autocomplete: Removes the latency for cached completions. [https://github.com/sourcegraph/cody/pull/3138](https://github.com/sourcegraph/cody/pull/3138)
- Autocomplete: Removes the latency for cached completions. [pull/3138](https://github.com/sourcegraph/cody/pull/3138)
- Autocomplete: Enable the recent jaccard similarity improvements by default. [pull/3135](https://github.com/sourcegraph/cody/pull/3135)
- Autocomplete: Start retrieval phase earlier to improve latency. [pull/3149](https://github.com/sourcegraph/cody/pull/3149)
- Autocomplete: Trigger one LLM request instead of three for multiline completions to reduce the response latency. [pull/3176](https://github.com/sourcegraph/cody/pull/3176)
Expand Down
Binary file added vscode/doc/images/keyboard_editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions vscode/doc/keyboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Keyboard Shortcuts

Cody offers an extensive set of default key bindings (keyboard shortcuts) to make it easy for you to perform various actions using the keyboard. You can easily update these bindings in the `Keyboard Shortcuts editor`, which you can access by clicking on the **Keyboard Shortcuts** icon in the Cody sidebar under `**SETTINGS AND SUPPORT**`.

This feature can be particularly useful if the default key bindings conflict with your current or preferred key bindings.

## Keyboard Shortcuts Editor

The Keyboard Shortcuts editor allows you to easily update the key bindings for any of the Cody commands. To open the `Keyboard Shortcuts Editor`:
1. Open the Cody sidebar and expand the `**SETTINGS AND SUPPORT**` section.
2. Click on the `**Keyboard Shortcuts**` icon.

## Custom Commands

![editor](images/keyboard_editor.png)

You have the option to assign key bindings (keyboard shortcuts) to individual custom commands:
1. In the Cody sidebar under `**SETTINGS AND SUPPORT**`, click on the `**Keyboard Shortcuts**` icon to open the Keyboard Shortcuts editor.
2. Search for `cody.command.custom.{CUSTOM_COMMAND_NAME}`.
3. Replace `{CUSTOM_COMMAND_NAME}` with the name or key of your custom command, for example, `cody.command.custom.commit`.
4. Click on the `+` icon next to the command to assign a custom keybinding for your custom command.

For more information on keyboard settings in VS Code, please refer to the official documentation on [Key Bindings for Visual Studio Code](https://code.visualstudio.com/docs/getstarted/keybindings#_keyboard-shortcuts-editor).

15 changes: 15 additions & 0 deletions vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@
"media": {
"markdown": "walkthroughs/search.md"
}
},
{
"id": "keyboard",
"title": "Keyboard Shortcuts",
"description": "Easily customize the default keyboard shortcuts to suit your workflow.\n[Show Keyboard Shortcuts Editor](command:cody.sidebar.keyboardShortcuts)",
"media": {
"markdown": "walkthroughs/keyboard.md"
}
}
]
}
Expand Down Expand Up @@ -309,6 +317,13 @@
"icon": "$(tools)",
"when": "cody.activated && workspaceFolderCount > 0"
},
{
"command": "cody.menu.commands-settings",
"category": "Cody Command",
"title": "Custom Commands Settings",
"icon": "$(gear)",
"when": "cody.activated"
},
{
"command": "cody.command.context-search",
"category": "Cody",
Expand Down
96 changes: 47 additions & 49 deletions vscode/src/commands/services/custom-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { buildCodyCommandMap } from '../utils/get-commands'
import { CustomCommandType } from '@sourcegraph/cody-shared/src/commands/types'
import { getConfiguration } from '../../configuration'
import { isMac } from '@sourcegraph/cody-shared/src/common/platform'
import { getDocText } from '../utils/workspace-files'
import type { TreeViewProvider } from '../../services/tree-views/TreeViewProvider'
import { getCommandTreeItems } from '../../services/tree-views/commands'

Expand All @@ -27,10 +28,10 @@ const userHomePath = os.homedir() || process.env.HOME || process.env.USERPROFILE
export class CustomCommandsManager implements vscode.Disposable {
// Watchers for the cody.json files
private fileWatcherDisposables: vscode.Disposable[] = []
private registeredCommands: vscode.Disposable[] = []
private disposables: vscode.Disposable[] = []

public customCommandsMap = new Map<string, CodyCommand>()
public userJSON: Record<string, unknown> | null = null

// Configuration files
protected configFileName
Expand Down Expand Up @@ -96,7 +97,9 @@ export class CustomCommandsManager implements vscode.Disposable {
}
}

logDebug('CommandsController:fileWatcherInit', 'watchers created')
if (this.fileWatcherDisposables.length) {
logDebug('CommandsController:init', 'watchers created')
}
}

/**
Expand All @@ -108,15 +111,20 @@ export class CustomCommandsManager implements vscode.Disposable {
return configFileUri
}

/**
* Rebuild the Custom Commands Map from the cody.json files
*/
public async refresh(): Promise<CodyCommandsFile> {
try {
// Deregister all commands before rebuilding them to avoid duplicates
this.disposeRegisteredCommands()
// Reset the map before rebuilding
this.customCommandsMap = new Map<string, CodyCommand>()
// user commands
if (this.userConfigFile?.path) {
await this.build(CustomCommandType.User)
}
// only build workspace prompts if the workspace is trusted
// 🚨 SECURITY: Only build workspace command in trusted workspace
if (vscode.workspace.isTrusted) {
await this.build(CustomCommandType.Workspace)
}
Expand All @@ -127,27 +135,36 @@ export class CustomCommandsManager implements vscode.Disposable {
return { commands: this.customCommandsMap }
}

/**
* Handles building the Custom Commands Map from the cody.json files
*
* 🚨 SECURITY: Only build workspace command in trusted workspace
*/
public async build(type: CustomCommandType): Promise<Map<string, CodyCommand> | null> {
const uri = this.getConfigFileByType(type)
// Security: Make sure workspace is trusted before building commands from workspace
if (!uri || (type === CustomCommandType.Workspace && !vscode.workspace.isTrusted)) {
return null
}
try {
const bytes = await vscode.workspace.fs.readFile(uri)
const content = new TextDecoder('utf-8').decode(bytes)
const content = await getDocText(uri)
if (!content.trim()) {
throw new Error('Empty file')
return null
}
const customCommandsMap = buildCodyCommandMap(type, content)
this.customCommandsMap = new Map([...this.customCommandsMap, ...customCommandsMap])

// Keep a copy of the user json file for recreating the commands later
if (type === CustomCommandType.User) {
this.userJSON = JSON.parse(content)
// Register Custom Commands as VS Code commands
for (const [key, _command] of customCommandsMap) {
this.registeredCommands.push(
vscode.commands.registerCommand(`cody.command.custom.${key}`, () =>
vscode.commands.executeCommand('cody.action.command', key, {
source: 'editor',
})
)
)
}
} catch (error) {
logDebug('CustomCommandsProvider:build', 'failed', { verbose: error })
console.error('CustomCommandsProvider:build', 'failed', { verbose: error })
}
return this.customCommandsMap
}
Expand Down Expand Up @@ -186,39 +203,22 @@ export class CustomCommandsManager implements vscode.Disposable {
}

/**
* Add the newly create command via quick pick to the cody.json file
* Add the newly create command via quick pick to the cody.json file on disk
*/
private async save(
id: string,
command: CodyCommand,
type: CustomCommandType = CustomCommandType.User
): Promise<void> {
this.customCommandsMap.set(id, command)
const updated: Omit<CodyCommand, 'key'> | undefined = omit(command, ['key', 'type'])

// Filter map to remove commands with non-match type
const filtered = new Map<string, Omit<CodyCommand, 'key'>>()
for (const [key, _command] of this.customCommandsMap) {
if (_command.type === type) {
filtered.set(key, omit(_command, ['key', 'type']))
}
}

// Add the new command to the filtered map
filtered.set(id, updated)

// turn map into json
const jsonContext = { ...this.userJSON }
jsonContext.commands = Object.fromEntries(filtered)
const uri = this.getConfigFileByType(type)
if (!uri) {
throw new Error('Invalid file path')
}
try {
await saveJSONFile(jsonContext, uri)
} catch (error) {
logError('CustomCommandsProvider:save', 'failed', { verbose: error })
return
}
const fileContent = await getDocText(uri)
const parsed = JSON.parse(fileContent) as Record<string, any>
const commands = parsed.commands ?? parsed
commands[id] = omit(command, 'key')
await saveJSONFile(parsed, uri)
}

private async configFileActions(
Expand Down Expand Up @@ -288,17 +288,23 @@ export class CustomCommandsManager implements vscode.Disposable {
for (const disposable of this.disposables) {
disposable.dispose()
}
this.disposeRegisteredCommands()
this.disposeWatchers()
this.customCommandsMap = new Map<string, CodyCommand>()
this.userJSON = null
}

private disposeWatchers(): void {
for (const disposable of this.fileWatcherDisposables) {
disposable.dispose()
}
this.fileWatcherDisposables = []
logDebug('CommandsController:disposeWatchers', 'watchers disposed')
}

private disposeRegisteredCommands(): void {
for (const rc of this.registeredCommands) {
rc.dispose()
}
this.registeredCommands = []
}
}

Expand Down Expand Up @@ -333,24 +339,16 @@ export async function migrateCommandFiles(): Promise<void> {
}

async function migrateContent(oldFile: vscode.Uri, newFile: vscode.Uri): Promise<void> {
const oldUserContent = await tryReadFile(newFile)
if (!oldUserContent) {
const oldUserContent = await getDocText(newFile)
if (!oldUserContent.trim()) {
return
}

const oldContent = await tryReadFile(oldFile)
const oldContent = await getDocText(oldFile)
const workspaceEditor = new vscode.WorkspaceEdit()
workspaceEditor.createFile(newFile, { ignoreIfExists: true })
workspaceEditor.insert(newFile, new vscode.Position(0, 0), JSON.stringify(oldContent, null, 2))
await vscode.workspace.applyEdit(workspaceEditor)
const doc = await vscode.workspace.openTextDocument(newFile)
await doc.save()
workspaceEditor.deleteFile(oldFile, { ignoreIfNotExists: true })
}

async function tryReadFile(fileUri: vscode.Uri): Promise<string | undefined> {
return vscode.workspace.fs.readFile(fileUri).then(
content => new TextDecoder('utf-8').decode(content),
error => undefined
)
await vscode.workspace.openTextDocument(newFile).then(doc => doc.save())
}
11 changes: 4 additions & 7 deletions vscode/src/commands/utils/workspace-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ export async function doesFileExist(uri: vscode.Uri): Promise<boolean> {
* @returns A Promise resolving to the decoded text contents of the file.
*/
export async function getDocText(fileUri: URI): Promise<string> {
try {
const bytes = await vscode.workspace.fs.readFile(fileUri)
const decoded = new TextDecoder('utf-8').decode(bytes)
return decoded
} catch {
return ''
}
return vscode.workspace.fs.readFile(fileUri).then(
bytes => new TextDecoder('utf-8').decode(bytes),
error => ''
)
}
18 changes: 18 additions & 0 deletions vscode/walkthroughs/keyboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## Keyboard Shortcuts

Cody offers an extensive set of default key bindings (keyboard shortcuts) to make it easy for you to perform various actions using the keyboard. You can also update these bindings in the `Keyboard Shortcuts Editor`.

### Keyboard Shortcuts Editor

The `Keyboard Shortcuts Editor` allows you to easily update the default bindings (keyboard shortcuts). This is particularly useful if the default key bindings conflict with your current or preferred key bindings. By accessing the `Keyboard Shortcuts Editor`, you can search for specific commands, reassign keybindings, and customize your keyboard shortcuts to suit your workflow.

### Getting Started

To get started:
1. Click on the **Keyboard Shortcuts** icon in the Cody sidebar under `**SETTINGS AND SUPPORT**` to open the `Keyboard Shortcuts Editor`.
2. In the `Keyboard Shortcuts Editor`, you can search for a specific command by typing in the search bar.
3. Once you have found the command you want to reassign keybindings for, click on it to select it.
4. Follow the instruction on screen to assign a new keybinding.

**✨ Pro-tips for assigning keyboard shortcuts for Cody**
<br>• You can assign key bindings (keyboard shortcuts) to individual custom commands.
Loading