Skip to content

Commit

Permalink
feat: generate code mode and add snippet to chat (#40)
Browse files Browse the repository at this point in the history
* fix: accept and reject error

* feat: add code generate manager

* chore: remove unused code

* feat: add snippet reference

* feat: send generate code command to webview

* feat: add generate code command to LLM

* feat: add id to file reference

* feat: hide snippet preview

* feat: add chat status

* feat: do not store generate chat session

* fix: bug

* feat: regenerate code

* add file status

* remove unused code

* clear file when accept or reject is complete

* fix: improve condition for canceling code generation in ChatStatus component

* docs: add new feature to readme
  • Loading branch information
lee88688 authored Dec 1, 2024
1 parent 8992bb6 commit 6bf1636
Show file tree
Hide file tree
Showing 28 changed files with 1,087 additions and 77 deletions.
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
# aider-composer
# Aider Composer

Aider Composer is a VSCode extension that integrates [Aider](https://github.com/Aider-AI/aider) into your development workflow. This extension is highly inspired by [cursor](https://www.cursor.com/) and [cline](https://github.com/cline/cline).

![demo](./resources/demo.gif)

It is highly recommended to read the [Requirements](#requirements) and [Extension Settings](#extension-settings) sections for initial configuration, otherwise the extension may not work correctly.

## 🎉🎉News🎉🎉

- [Generate Code Mode](#generate-code)
![generate-code](./resources/generate-code.gif)
- Add Code Snippet to Chat
- Add [Inline Diff Preview](#inline-diff-preview)

## Features

- Easily add and remove files, and toggle between read-only and editable modes with just a click
Expand Down Expand Up @@ -69,12 +76,38 @@ In this extension, file reference is above the chat input area, you can click th

![file-reference](./resources/file-reference.gif)

### Confirm Modify
### Code Review

When Aider modifies code, it will show you the code. You have two review options:
- Use diff editor (default)
- Use inline diff preview

#### Diff Editor

when Aider modify code, it will show you a diff editor, you can review the code changes and confirm to apply them by clicking the button `` at editor toolbar.

![confirm-modify](./resources/confirm-modify.png)

#### Inline Diff Preview

When Aider modifies code, it will show you an inline diff preview. You can review the code changes and accept or reject each snippet by clicking the `accept` or `reject` button before the diff.

To enable this feature, you need to set `aider-composer.inlineDiff.enable` to `true` in VSCode settings and **restart** VSCode.

![inline-preview](./resources/inline-preview.gif)

### Add Code Snippet

You can add a code snippet to the chat by selecting code in the editor and pressing `ctrl+shift+k`.

![line-reference](./resources/line-reference.gif)

### Generate Code

You can enter generate code mode by pressing `ctrl+shift+l` in the editor. The current line will be highlighted, and the code generated by Aider will appear below the highlighted line.

![generate-code](./resources/generate-code.gif)

---

**Enjoy!**
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@
"key": "ctrl+alt+r",
"mac": "cmd+alt+r",
"when": "aider-composer.hasChanges"
},
{
"command": "aider-composer.GenerateCode",
"key": "ctrl+shift+k",
"mac": "cmd+shift+k",
"when": "editorTextFocus && !editorReadonly"
},
{
"command": "aider-composer.InsertIntoChat",
"key": "ctrl+shift+l",
"mac": "cmd+shift+l",
"when": "editorTextFocus && !editorReadonly"
}
],
"viewsContainers": {
Expand Down Expand Up @@ -110,6 +122,16 @@
"command": "aider-composer.RejectAllChanges",
"title": "Reject All Changes",
"icon": "$(close-all)"
},
{
"command": "aider-composer.GenerateCode",
"title": "Generate Code",
"icon": "$(code)"
},
{
"command": "aider-composer.InsertIntoChat",
"title": "Insert Into Chat",
"icon": "$(arrow-right)"
}
],
"menus": {
Expand Down
Binary file added resources/generate-code.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/inline-preview.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/line-reference.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 44 additions & 1 deletion src/diffView/InlineDiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export class InlineDiffViewManager
}

this.fileChangeMap.delete(uri);
this._onDidChange.fire({
type: 'reject',
path: doc.uri.scheme === 'file' ? doc.uri.fsPath : doc.uri.path,
});
this.outputChannel.debug(
`Cleaned up decorations for ${doc.uri.fsPath}`,
);
Expand Down Expand Up @@ -147,7 +151,6 @@ export class InlineDiffViewManager
},
),

// 添加活动编辑器变化的监听
vscode.window.onDidChangeActiveTextEditor((editor) => {
if (editor) {
const uri = editor.document.uri.toString();
Expand Down Expand Up @@ -364,6 +367,10 @@ export class InlineDiffViewManager
false,
);
this.fileChangeMap.delete(uri);
this._onDidChange.fire({
type: 'accept',
path: editor.document.uri.fsPath,
});
await this.saveDocument(editor);
}
}
Expand Down Expand Up @@ -428,6 +435,10 @@ export class InlineDiffViewManager
false,
);
this.fileChangeMap.delete(uri);
this._onDidChange.fire({
type: 'reject',
path: editor.document.uri.fsPath,
});
if (editor.document.uri.scheme === 'file') {
await this.saveDocument(editor);
} else {
Expand Down Expand Up @@ -471,6 +482,10 @@ export class InlineDiffViewManager
}
await vscode.workspace.applyEdit(edit);
this.fileChangeMap.delete(uri.toString());
this._onDidChange.fire({
type: 'accept',
path: uri.fsPath,
});

await vscode.commands.executeCommand(
'setContext',
Expand Down Expand Up @@ -522,6 +537,10 @@ export class InlineDiffViewManager
}
await vscode.workspace.applyEdit(edit);
this.fileChangeMap.delete(uri.toString());
this._onDidChange.fire({
type: 'reject',
path: uri.fsPath,
});

await vscode.commands.executeCommand(
'setContext',
Expand Down Expand Up @@ -629,6 +648,10 @@ export class InlineDiffViewManager
changes: changes,
};
this.fileChangeMap.set(uri.toString(), fileChange);
this._onDidChange.fire({
type: 'add',
path: uri.fsPath,
});

// Update context when changes exist
vscode.commands.executeCommand(
Expand All @@ -645,4 +668,24 @@ export class InlineDiffViewManager
throw error;
}
}

async acceptAllFile(): Promise<void> {
for (const uri of this.fileChangeMap.keys()) {
await this.acceptAllChanges(vscode.Uri.parse(uri));
}
}

async rejectAllFile(): Promise<void> {
for (const uri of this.fileChangeMap.keys()) {
await this.rejectAllChanges(vscode.Uri.parse(uri));
}
}

async acceptFile(path: string): Promise<void> {
await this.acceptAllChanges(vscode.Uri.parse(path));
}

async rejectFile(path: string): Promise<void> {
await this.rejectAllChanges(vscode.Uri.parse(path));
}
}
98 changes: 94 additions & 4 deletions src/diffView/diffEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class DiffContentProvider implements vscode.TextDocumentContentProvider {
export class DiffEditorViewManager extends DiffViewManager {
static readonly DiffContentProviderId = 'aider-diff';

private fileChangeSet = new Set<string>();
// uri -> content
private fileChangeSet = new Map<string, string>();

constructor(
private context: vscode.ExtensionContext,
Expand Down Expand Up @@ -46,7 +47,11 @@ export class DiffEditorViewManager extends DiffViewManager {
outputChannel.error(`Error writing file: ${error}`);
}

this.fileChangeSet.delete(uri.path);
this.fileChangeSet.delete(uri.toString());
this._onDidChange.fire({
type: 'accept',
path: fileUri.fsPath,
});

await vscode.commands.executeCommand(
'workbench.action.closeActiveEditor',
Expand All @@ -68,7 +73,11 @@ export class DiffEditorViewManager extends DiffViewManager {
this.outputChannel.debug(
`Diff document closed for: ${document.uri.path}`,
);
this.fileChangeSet.delete(document.uri.path);
this.fileChangeSet.delete(document.uri.toString());
this._onDidChange.fire({
type: 'reject',
path: document.uri.fsPath,
});
}
}),
);
Expand Down Expand Up @@ -115,6 +124,87 @@ export class DiffEditorViewManager extends DiffViewManager {
this.outputChannel.error(`Error opening diff: ${error}`);
}

this.fileChangeSet.add(data.path);
this.fileChangeSet.set(vscode.Uri.file(data.path).toString(), data.content);
this._onDidChange.fire({
type: 'add',
path: data.path,
});
}

// close all diff editor with DiffContentProviderId
private async closeAllDiffEditor(): Promise<void> {
// Find all tab groups
const tabGroups = vscode.window.tabGroups.all;

for (const group of tabGroups) {
// Find tabs that match our diff URI scheme
const diffTabs = group.tabs.filter(
(tab) =>
tab.input instanceof vscode.TabInputTextDiff &&
tab.input.modified.scheme ===
DiffEditorViewManager.DiffContentProviderId,
);

// Close the matching tabs
if (diffTabs.length > 0) {
await vscode.window.tabGroups.close(diffTabs);
}
}

// Clear the file change set after closing all editors
this.fileChangeSet.clear();
}

async acceptAllFile(): Promise<void> {
for (const [uri, content] of this.fileChangeSet.entries()) {
await vscode.workspace.fs.writeFile(
vscode.Uri.parse(uri),
Buffer.from(content),
);
}
await this.closeAllDiffEditor();
}

async rejectAllFile(): Promise<void> {
await this.closeAllDiffEditor();
}

private async closeDiffEditor(path: string): Promise<void> {
// Find all tab groups
const tabGroups = vscode.window.tabGroups.all;
const targetUri = vscode.Uri.file(path).toString();

for (const group of tabGroups) {
// Find tabs that match our diff URI scheme and the specified path
const diffTabs = group.tabs.filter(
(tab) =>
tab.input instanceof vscode.TabInputTextDiff &&
tab.input.modified.scheme ===
DiffEditorViewManager.DiffContentProviderId &&
tab.input.modified.path === path,
);

// Close the matching tabs
if (diffTabs.length > 0) {
await vscode.window.tabGroups.close(diffTabs);
}
}

// Remove the file from the change set
this.fileChangeSet.delete(targetUri);
}

async acceptFile(path: string): Promise<void> {
const uri = vscode.Uri.file(path);
const content = this.fileChangeSet.get(uri.toString());

if (content) {
await vscode.workspace.fs.writeFile(uri, Buffer.from(content));
await this.closeDiffEditor(path);
}
}

async rejectFile(path: string): Promise<void> {
await this.closeDiffEditor(path);
}
}
29 changes: 29 additions & 0 deletions src/diffView/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import * as vscode from 'vscode';

type DiffViewChange =
| {
type: 'add';
path: string;
}
| {
type: 'accept' | 'reject';
/** file path, URI.fsPath */
path: string;
};

export abstract class DiffViewManager {
protected disposables: vscode.Disposable[] = [];
protected _onDidChange = new vscode.EventEmitter<DiffViewChange>();
readonly onDidChange = this._onDidChange.event;

constructor() {
this.disposables.push(this._onDidChange);
}

public dispose = () => {
while (this.disposables.length) {
Expand All @@ -13,4 +30,16 @@ export abstract class DiffViewManager {
};

abstract openDiffView(data: { path: string; content: string }): Promise<void>;

// accept all code generate by aider
abstract acceptAllFile(): Promise<void>;

// reject all code generate by aider
abstract rejectAllFile(): Promise<void>;

// accept a file
abstract acceptFile(path: string): Promise<void>;

// reject a file
abstract rejectFile(path: string): Promise<void>;
}
7 changes: 6 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import * as vscode from 'vscode';
import * as fsPromises from 'fs/promises';
import VscodeReactView from './webViewProvider';
import { DiffContentProviderId } from './types';
import AiderChatService from './aiderChatService';
import { InlineDiffViewManager } from './diffView/InlineDiff';
import { DiffEditorViewManager } from './diffView/diffEditor';
import { isProductionMode } from './utils/isProductionMode';
import { DiffViewManager } from './diffView';
import GenerateCodeManager from './generateCode/generateCodeManager';

let outputChannel: vscode.LogOutputChannel;

Expand Down Expand Up @@ -41,11 +41,16 @@ export function activate(context: vscode.ExtensionContext) {
diffViewManager = diffEditorDiffManager;
}

// generate code manager
const generateCodeManager = new GenerateCodeManager(outputChannel);
context.subscriptions.push(generateCodeManager);

// webview provider
const webviewProvider = new VscodeReactView(
context,
outputChannel,
diffViewManager,
generateCodeManager,
);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
Expand Down
Loading

0 comments on commit 6bf1636

Please sign in to comment.