Compare commits

...

25 Commits

Author SHA1 Message Date
Andrew 6d12737fda add throttling 2024-10-28 03:52:19 -07:00
mp eae1af331f Create Ctrl + K 2024-10-28 03:36:07 -07:00
Andrew 2e92da500f lines sweep works! Need to polish it. 2024-10-28 03:19:40 -07:00
mp 9eb897b655 Better file reading function 2024-10-25 21:55:15 -07:00
mp a738ca0b2c Reafactor DiffProvider for ctrl+k 2024-10-25 21:47:00 -07:00
Andrew 1b8e7f3643 amend 2024-10-24 19:59:25 -07:00
Andrew 420ecb320e Merge branch 'main' into speculative-edits 2024-10-24 19:58:58 -07:00
mp 097f279ecb Throttle edit requests, refactor 2024-10-24 19:48:31 -07:00
Andrew 9a1d4e2b9c comment 2024-10-24 19:34:09 -07:00
Andrew a926efcd2a attempt windows build 2024-10-24 19:25:20 -07:00
Andrew Pareles 40aa319d18 update react comment 2024-10-24 15:37:13 -07:00
Andrew Pareles 549dabbde6 X [ERROR] Cannot start service: Host version "0.23.1" 2024-10-24 15:35:43 -07:00
Andrew Pareles 42e52647f3 Merge pull request #121 from jcommaret/main
Doc : Build command fix
2024-10-23 11:33:09 -07:00
Andrew Pareles a12ffd6b5e Update CONTRIBUTING.md 2024-10-23 11:32:52 -07:00
Andrew Pareles 3d74e77760 Update CONTRIBUTING.md 2024-10-23 11:32:27 -07:00
Jérôme Commaret 0ffd66e747 Doc : Build command fix 2024-10-23 12:30:57 +02:00
Andrew Pareles e78fa459cb Merge pull request #120 from voideditor/@andrewpareles/editor-insets
discord
2024-10-23 03:17:52 -07:00
Andrew Pareles 45c7b951e9 or just run 2024-10-23 01:48:39 -07:00
Andrew Pareles f31f5e7ad1 Merge pull request #119 from voideditor/@andrewpareles/editor-insets
Editor Insets
2024-10-23 01:46:54 -07:00
mp 1e4c932313 Track call time 2024-10-22 14:32:39 -07:00
mp 7bd9e01a15 Working draft of speculative edits 2024-10-21 18:33:00 -07:00
mp 9ecf596cbb Prepare to debug 2024-10-19 20:17:54 -07:00
mp f6cad40a44 Finish speculative draft 2024-10-18 19:18:11 -07:00
mp e25368ef7f Merge branch 'main' into speculative-edits 2024-10-18 17:25:13 -07:00
mp 7ce17a0854 Initial draft for speculative edits 2024-10-18 17:19:38 -07:00
21 changed files with 2336 additions and 839 deletions
+21 -18
View File
@@ -12,6 +12,7 @@ We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-f
For some useful links we've compiled see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
## 1. Building the Extension
Here's how you can start contributing to the Void extension. This is where you should get started if you're new.
1. Clone the repository:
@@ -20,11 +21,7 @@ Here's how you can start contributing to the Void extension. This is where you s
git clone https://github.com/voideditor/void
```
2. Open the folder `/extensions/void` in VSCode (open it in a new workspace, *don't* just cd into it):
```
open /extensions/void in vscode
```
2. Open the folder `/extensions/void` in VSCode (open it in a new workspace, _don't_ just cd into it).
3. Install dependencies:
@@ -32,7 +29,7 @@ open /extensions/void in vscode
npm install
```
4. Build the project. We created this build command so that we could run React in vscode - it converts `sidebar/index.tsx` into a CSS/JS bundle in `dist/`.
4. Compile the React by running `npm run build`. We created this build command to convert `sidebar/index.tsx` into `dist/`.
```
npm run build
@@ -42,17 +39,13 @@ npm run build
This will start a new instance of VSCode with the extension enabled. If this doesn't work, you can press <kbd>Ctrl+Shift+P</kbd>, select "Debug: Start Debugging", and select "VSCode Extension Development".
## 2. Building the full IDE
If you want to work on the full IDE, please follow the steps below. If you have any questions/issues, you can refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page, which is where the steps below come from. Also feel free to submit an issue or get in touch with us with any build errors.
### a. Building on a Mac
To build on a Mac, open `void/` in VSCode. Make sure you've built the extension (`cd ./extensions/void && npm run build && npm run compile && cd ../..`). Also make sure you have Python and XCode installed on your system (you probably do by default).
To build on a Mac, open `void/` in VSCode. Make sure you've built the extension by following the steps above (or just run `cd ./extensions/void && npm install && npm run build && npm run compile && cd ../..`). Also make sure you have Python and XCode installed on your system (you probably do by default).
1. Install all dependencies.
@@ -81,16 +74,26 @@ To bundle the IDE, run `npm run gulp vscode-darwin-arm64`. Here are the full opt
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page!
**Common Fixes:**
- Make sure you have the same NodeJS version as `.nvmrc`.
- If you see `X [ERROR] Cannot start service: Host version "0.23.1" does not match binary version "0.23.0"`, run `npm i -D esbuild@0.23.0`
### b. Building on Windows
To build on Windows, please refer to [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute). We recommend building on Mac; we're Windows users who switch to Mac to build right now.
If you're on Windows, we recommend running the project inside a dev container. VSCode should prompt you to do this automatically.
<!-- Get [Visual Studio 2022](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=Community). Also find the boxes for "Desktop development with C++" and "Node.js development" and get those, too.
If you get a node-gyp error in the next few steps, you should also get [Visual Studio Build Tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools), find the Visual Studio Build Tools box, click Install (or Modify), then in Individual Components:
check every item under `MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)`, `C++ ATL for latest build tools with Spectre Mitigations`, and `C++ MFC for latest build tools with Spectre Mitigations`.
```
npm config set msvs_version 2022
```
-->
## Roadmap
@@ -124,7 +127,7 @@ Please don't make big refactors without speaking with us first. We'd like to kee
Please submit a pull request once you've made a change. Here are a few guidelines:
- A PR should be about one *single* feature change. The fewer items you change, the more likely the PR is to be accepted.
- A PR should be about one _single_ feature change. The fewer items you change, the more likely the PR is to be accepted.
- Your PR should contain a description that first explains at a high level what you did, and then describes the exact changes you made (and to which files). Please don't use vague statements like "refactored code" or "improved types" (instead, describe what code you refactored, or what types you changed).
@@ -142,11 +145,11 @@ We keep track of all the files we've changed with Void so it's easy to rebase:
- src/vs/workbench/api/common/{extHost.api.impl.ts | extHostApiCommands.ts}
- src/vs/workbench/workbench.common.main.ts
- src/vs/workbench/contrib/void/*
- extensions/void/*
- src/vs/workbench/contrib/void/\*
- extensions/void/\*
- .github/*
- .vscode/settings/*
- .github/\*
- .vscode/settings/\*
- .eslintrc.json
- build/hygiene.js
- build/lib/i18n.resources.json
+37 -18
View File
@@ -15,6 +15,7 @@
"@types/diff": "^5.2.2",
"@types/diff-match-patch": "^1.0.36",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.12",
"@types/mocha": "^10.0.8",
"@types/node": "^22.5.1",
"@types/react": "^18.3.4",
@@ -33,11 +34,12 @@
"eslint-plugin-react": "^7.35.1",
"eslint-plugin-react-hooks": "^4.6.2",
"globals": "^15.9.0",
"lodash": "^4.17.21",
"marked": "^14.1.0",
"ollama": "^0.5.9",
"openai": "^4.68.1",
"openai": "^4.68.4",
"postcss": "^8.4.41",
"posthog-js": "^1.174.0",
"posthog-js": "^1.176.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
@@ -1134,6 +1136,12 @@
"pretty-format": "^29.0.0"
}
},
"node_modules/@types/lodash": {
"version": "4.17.12",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.12.tgz",
"integrity": "sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==",
"dev": true
},
"node_modules/@types/mdast": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
@@ -2409,7 +2417,6 @@
"integrity": "sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
@@ -3369,8 +3376,7 @@
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
@@ -5019,6 +5025,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -6001,6 +6013,14 @@
"node": ">=10"
}
},
"node_modules/monaco-editor": {
"version": "0.52.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz",
"integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -6265,9 +6285,9 @@
}
},
"node_modules/openai": {
"version": "4.68.2",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.68.2.tgz",
"integrity": "sha512-Ys3Jl9vkBUFtrFj4pgrF7rMte4JNekZoMgI6dWkkpOIwNUKGkc4I8jTqv86LB+TcoqkTPzV6DS269dPR9ILWsQ==",
"version": "4.68.4",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.68.4.tgz",
"integrity": "sha512-LRinV8iU9VQplkr25oZlyrsYGPGasIwYN8KFMAAFTHHLHjHhejtJ5BALuLFrkGzY4wfbKhOhuT+7lcHZ+F3iEA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -6589,9 +6609,9 @@
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"dev": true,
"license": "ISC"
},
@@ -6820,9 +6840,9 @@
"license": "MIT"
},
"node_modules/posthog-js": {
"version": "1.174.3",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.174.3.tgz",
"integrity": "sha512-fRLncd3jkT9Y7gLiyQe8v8sJ9yuTIiQBBWcYQ8l+vv+m504LWFtxl+/JZtHXPhaG3Eyf7AzZ/Kafkw8jorWV9w==",
"version": "1.176.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.176.0.tgz",
"integrity": "sha512-T5XKNtRzp7q6CGb7Vc7wAI76rWap9fiuDUPxPsyPBPDkreKya91x9RIsSapAVFafwD1AEin1QMczCmt9Le9BWw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6837,7 +6857,6 @@
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
"dev": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -8654,9 +8673,9 @@
}
},
"node_modules/web-vitals": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.3.tgz",
"integrity": "sha512-/CFAm1mNxSmOj6i0Co+iGFJ58OS4NRGVP+AWS/l509uIK5a1bSoIVaHz/ZumpHTfHSZBpgrJ+wjfpAOrTHok5Q==",
"dev": true,
"license": "Apache-2.0"
},
+4 -2
View File
@@ -116,6 +116,7 @@
"@types/diff": "^5.2.2",
"@types/diff-match-patch": "^1.0.36",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.12",
"@types/mocha": "^10.0.8",
"@types/node": "^22.5.1",
"@types/react": "^18.3.4",
@@ -134,11 +135,12 @@
"eslint-plugin-react": "^7.35.1",
"eslint-plugin-react-hooks": "^4.6.2",
"globals": "^15.9.0",
"lodash": "^4.17.21",
"marked": "^14.1.0",
"ollama": "^0.5.9",
"openai": "^4.68.1",
"openai": "^4.68.4",
"postcss": "^8.4.41",
"posthog-js": "^1.174.0",
"posthog-js": "^1.176.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
+560
View File
@@ -0,0 +1,560 @@
import * as vscode from 'vscode';
import { findDiffs } from './findDiffs';
import { Diff, DiffArea, BaseDiff, } from './common/shared_types';
import { readFileContentOfUri } from './common/readFileContentOfUri';
import { throttle } from 'lodash';
const THROTTLE_TIME = 100
// TODO in theory this should be disposed
const greenDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(0 255 51 / 0.2)',
isWholeLine: false, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:...
})
const lightGrayDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(218 218 218 / .2)',
isWholeLine: true,
})
const darkGrayDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgb(148 148 148 / .2)',
isWholeLine: true,
})
// responsible for displaying diffs and showing accept/reject buttons
export class DiffProvider implements vscode.CodeLensProvider {
private _originalFileOfDocument: { [docUriStr: string]: string } = {}
private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {}
private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {}
private _diffareaidPool = 0
private _diffidPool = 0
// used internally by vscode
private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
// used internally by vscode
public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
const docUriStr = document.uri.toString()
return this._diffsOfDocument[docUriStr]?.flatMap(diff => diff.lenses) ?? []
}
// declared by us, registered with vscode.languages.registerCodeLensProvider()
constructor() {
console.log('Creating DisplayChangesProvider')
// this acts as a useEffect every time text changes
vscode.workspace.onDidChangeTextDocument((e) => {
const editor = vscode.window.activeTextEditor
if (!editor) return
const docUriStr = editor.document.uri.toString()
const changes = e.contentChanges.map(c => ({ startLine: c.range.start.line, endLine: c.range.end.line, text: c.text, }))
// on user change, grow/shrink/merge/delete diff areas
this.refreshDiffAreasModel(docUriStr, changes, 'currentFile')
// refresh the diffAreas
this.refreshStylesAndDiffs(docUriStr)
})
}
// used by us only
public createDiffArea(uri: vscode.Uri, partialDiffArea: Omit<DiffArea, 'diffareaid'>, originalFile: string) {
const uriStr = uri.toString()
this._originalFileOfDocument[uriStr] = originalFile
// make sure array is defined
if (!this._diffAreasOfDocument[uriStr]) this._diffAreasOfDocument[uriStr] = []
// remove all diffAreas that the new `diffArea` is overlapping with
this._diffAreasOfDocument[uriStr] = this._diffAreasOfDocument[uriStr].filter(da => {
const noOverlap = da.startLine > partialDiffArea.endLine || da.endLine < partialDiffArea.startLine
if (!noOverlap) return false
return true
})
// add `diffArea` to storage
const diffArea = {
...partialDiffArea,
diffareaid: this._diffareaidPool
}
this._diffAreasOfDocument[uriStr].push(diffArea)
this._diffareaidPool += 1
return diffArea
}
// used by us only
// changes the start/line locations based on the changes that were recently made. does not change any of the diffs in the diff areas
// changes tells us how many lines were inserted/deleted so we can grow/shrink the diffAreas accordingly
public refreshDiffAreasModel(docUriStr: string, changes: { text: string, startLine: number, endLine: number }[], changesTo: 'originalFile' | 'currentFile') {
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
let endName
let startName
if (changesTo === 'originalFile') {
endName = 'originalEndLine' as const
startName = 'originalStartLine' as const
} else {
endName = 'endLine' as const
startName = 'startLine' as const
}
for (const change of changes) {
// here, `change.range` is the range of the original file that gets replaced with `change.text`
// compute net number of newlines lines that were added/removed
const numNewLines = (change.text.match(/\n/g) || []).length
const numLineDeletions = change.endLine - change.startLine
const deltaNewlines = numNewLines - numLineDeletions
// compute overlap with each diffArea and shrink/elongate the diffArea accordingly
for (const diffArea of diffAreas) {
// if the change is fully within the diffArea, elongate it by the delta amount of newlines
if (change.startLine >= diffArea[startName] && change.endLine <= diffArea[endName]) {
diffArea[endName] += deltaNewlines
}
// check if the `diffArea` was fully deleted and remove it if so
if (diffArea[startName] > diffArea[endName]) {
//remove it
const index = diffAreas.findIndex(da => da === diffArea)
diffAreas.splice(index, 1)
}
// TODO handle other cases where eg. the change overlaps many diffAreas
}
// if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines
for (const diffArea of diffAreas) {
if (diffArea[startName] > change.endLine) {
diffArea[startName] += deltaNewlines
diffArea[endName] += deltaNewlines
}
}
// TODO merge any diffAreas if they overlap with each other as a result from the shift
}
}
// used by us only
// refreshes all the diffs inside each diff area, and refreshes the styles
public refreshStylesAndDiffs(docUriStr: string) {
const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor
if (!editor) {
console.log('Error: No active editor!')
return;
}
const originalFile = this._originalFileOfDocument[docUriStr]
if (!originalFile) {
console.log('Error: No original file!')
return;
}
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
// reset all diffs (we update them below)
this._diffsOfDocument[docUriStr] = []
// for each diffArea
for (const diffArea of diffAreas) {
// get code inside of diffArea
const originalCode = originalFile.split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
const currentCode = editor.document.getText(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)).replace(/\r\n/g, '\n')
// compute the diffs
const diffs = findDiffs(originalCode, currentCode)
// add the diffs to `this._diffsOfDocument[docUriStr]`
this.createDiffs(editor.document.uri, diffs, diffArea)
// // print diffs
// console.log('!ORIGINAL FILE:', JSON.stringify(originalFile))
// console.log('!NEW FILE :', JSON.stringify(editor.document.getText().replace(/\r\n/g, '\n')))
// console.log('!AREA originalCode:', JSON.stringify(originalCode))
// console.log('!AREA currentCode :', JSON.stringify(currentCode))
// for (const diff of this._diffsOfDocument[docUriStr]) {
// console.log('------------')
// console.log('originalCode:', JSON.stringify(diff.originalCode))
// console.log('currentCode:', JSON.stringify(diff.code))
// console.log('originalRange:', diff.originalRange.start.line, diff.originalRange.end.line,)
// console.log('currentRange:', diff.range.start.line, diff.range.end.line,)
// }
}
// update green highlighting
editor.setDecorations(
greenDecoration,
(this._diffsOfDocument[docUriStr]
.filter(diff => diff.range !== undefined)
.map(diff => diff.range)
)
);
// for each diffArea, highlight its sweepIndex in dark gray
editor.setDecorations(
darkGrayDecoration,
(this._diffAreasOfDocument[docUriStr]
.filter(diffArea => diffArea.sweepIndex !== null)
.map(diffArea => {
let s = diffArea.sweepIndex!
return new vscode.Range(s, 0, s, 0)
})
)
)
// for each diffArea, highlight sweepIndex+1...end in light gray
editor.setDecorations(
lightGrayDecoration,
(this._diffAreasOfDocument[docUriStr]
.filter(diffArea => diffArea.sweepIndex !== null)
.map(diffArea => {
return new vscode.Range(diffArea.sweepIndex! + 1, 0, diffArea.endLine, 0)
})
)
)
// TODO update red highlighting
// this._diffsOfDocument[docUriStr].map(diff => diff.deletedCode)
// update code lenses
this._onDidChangeCodeLenses.fire()
}
// used by us only
public createDiffs(docUri: vscode.Uri, diffs: BaseDiff[], diffArea: DiffArea) {
const docUriStr = docUri.toString()
// if no diffs, set diffs to []
if (!this._diffsOfDocument[docUriStr])
this._diffsOfDocument[docUriStr] = []
// add each diff and its codelens to the document
for (let i = diffs.length - 1; i > -1; i -= 1) {
let suggestedDiff = diffs[i]
this._diffsOfDocument[docUriStr].push({
...suggestedDiff,
diffid: this._diffidPool,
// originalCode: suggestedDiff.deletedText,
lenses: [
new vscode.CodeLens(suggestedDiff.range, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }),
new vscode.CodeLens(suggestedDiff.range, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] })
]
});
this._diffidPool += 1
}
}
// called on void.acceptDiff
public async acceptDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
const editor = vscode.window.activeTextEditor
if (!editor)
return
const docUriStr = editor.document.uri.toString()
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
if (diffIdx === -1) { console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
if (diffareaIdx === -1) { console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
const diff = this._diffsOfDocument[docUriStr][diffIdx]
const originalFile = this._originalFileOfDocument[docUriStr]
const currentFile = await readFileContentOfUri(editor.document.uri)
// Fixed: Handle newlines properly by splitting into lines and joining with proper newlines
const originalLines = originalFile.split('\n');
const currentLines = currentFile.split('\n');
// Get the changed lines from current file
const changedLines = currentLines.slice(diff.range.start.line, diff.range.end.line + 1);
// Create new original file content by replacing the affected lines
const newOriginalLines = [
...originalLines.slice(0, diff.originalRange.start.line),
...changedLines,
...originalLines.slice(diff.originalRange.end.line + 1)
];
this._originalFileOfDocument[docUriStr] = newOriginalLines.join('\n');
// Update diff areas based on the change
this.refreshDiffAreasModel(docUriStr, [{
text: changedLines.join('\n'),
startLine: diff.originalRange.start.line,
endLine: diff.originalRange.end.line
}], 'originalFile')
// Check if diffArea should be removed
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
const originalArea = newOriginalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
console.log('ACCEPT change', changedLines.join('\n'), diff.originalRange.start.line, diff.originalRange.end.line)
console.log('ACCEPT area lines', diffArea.startLine, diffArea.endLine, diffArea.originalStartLine, diffArea.originalEndLine)
console.log('ACCEPT currentArea', currentArea)
console.log('ACCEPT originalArea', originalArea)
if (originalArea === currentArea) {
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1)
}
this.refreshStylesAndDiffs(docUriStr)
}
// called on void.rejectDiff
public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
const editor = vscode.window.activeTextEditor
if (!editor)
return
const docUriStr = editor.document.uri.toString()
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
if (diffIdx === -1) { console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
if (diffareaIdx === -1) { console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
const diff = this._diffsOfDocument[docUriStr][diffIdx]
// Apply the rejection by replacing with original code
// we don't have to edit the original or final file; just do a workspace edit so the code equals the original code
const workspaceEdit = new vscode.WorkspaceEdit();
workspaceEdit.replace(editor.document.uri, diff.range, diff.originalCode)
await vscode.workspace.applyEdit(workspaceEdit)
// Check if diffArea should be removed
const originalFile = this._originalFileOfDocument[docUriStr]
const currentFile = await readFileContentOfUri(editor.document.uri)
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
const currentLines = currentFile.split('\n');
const originalLines = originalFile.split('\n');
const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
const originalArea = originalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
console.log('REJECT diff lines', diff.originalRange.start.line, diff.originalRange.end.line)
console.log('REJECT area lines', diffArea.startLine, diffArea.endLine, diffArea.originalStartLine, diffArea.originalEndLine)
console.log('REJECT currentArea', currentArea)
console.log('REJECT originalArea', originalArea)
if (originalArea === currentArea) {
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1)
}
this.refreshStylesAndDiffs(docUriStr)
}
// used by us only
public updateStream = throttle(async (docUriStr: string, diffArea: DiffArea, newDiffAreaCode: string) => {
const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor
if (!editor) {
console.log('Error: No active editor!')
return;
}
// original code all diffs are based on in the code
const originalDiffAreaCode = (this._originalFileOfDocument[docUriStr] || '').split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
// figure out where to highlight based on where the AI is in the stream right now, use the last diff in findDiffs to figure that out
const diffs = findDiffs(originalDiffAreaCode, newDiffAreaCode)
const lastDiff = diffs[diffs.length - 1] ?? null
// these are two different coordinate systems - new and old line number
let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted
let oldFileStartLine: number // get original[oldStartingPoint...]
if (!lastDiff) {
// if the writing is identical so far, display no changes
newFileEndLine = 0
oldFileStartLine = 0
}
else {
if (lastDiff.type === 'insertion') {
newFileEndLine = lastDiff.range.end.line
oldFileStartLine = lastDiff.originalRange.start.line
}
else if (lastDiff.type === 'deletion') {
newFileEndLine = lastDiff.range.start.line
oldFileStartLine = lastDiff.originalRange.start.line
}
else if (lastDiff.type === 'edit') {
newFileEndLine = lastDiff.range.end.line
oldFileStartLine = lastDiff.originalRange.start.line
}
else {
throw new Error(`updateStream: diff.type not recognized: ${lastDiff.type}`)
}
}
// display
const newFileTop = newDiffAreaCode.split('\n').slice(0, newFileEndLine + 1).join('\n')
const oldFileBottom = originalDiffAreaCode.split('\n').slice(oldFileStartLine + 1, Infinity).join('\n')
let newCode = `${newFileTop}\n${oldFileBottom}`
diffArea.sweepIndex = newFileEndLine
// replace oldDACode with newDACode with a vscode edit
const workspaceEdit = new vscode.WorkspaceEdit();
const diffareaRange = new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)
workspaceEdit.replace(editor.document.uri, diffareaRange, newCode)
await vscode.workspace.applyEdit(workspaceEdit)
}, THROTTLE_TIME)
}
/*
import * as vscode from 'vscode';
import { SuggestedEdit } from './findDiffs';
const greenDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(0 255 51 / 0.2)',
isWholeLine: false, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:...
})
export class DiffProvider {
originalCodeOfDocument: { [docUri: string]: string }
diffsOfDocument: {
[docUri: string]: {
startLine,
startCol,
endLine,
endCol,
originalText,
inset,
diffid,
}
}
// sweep
currentLine: { [docUri: string]: undefined | number }
weAreEditing: boolean = false
constructor() {
vscode.workspace.onDidChangeTextDocument((e) => {
// on user change, grow/shrink/merge/delete diff AREAS
// you dont have to do anything to the diffs here bc they all get recomputed in refresh()
// user changes only get highlighted if theyre in a diffarea
// go thru all diff areas and adjust line numbers based on the user's change
this.refreshStyles(e.document.uri.toString())
})
}
// refreshes styles on page
refreshStyles(docUriStr: string) {
if (this.weAreEditing) return
// recompute all diffs on the page
// run inset.dispose() on all diffs
// original and current code -> diffs
// originalCodeOfDocument[docUriStr]
// create new diffs
const inset = vscode.window.createWebviewTextEditorInset(editor, lineStart, height, {})
inset.webview.html = `
<html>
<body style="pointer-events:none;">Hello World!</body>
</html>
`;
}
// called on void.acceptDiff
public async acceptDiff({ diffid }: { diffid: number }) {
// update original based on the diff
// refresh()
}
// called on void.rejectDiff
public async rejectDiff({ diffid }: { diffid: number }) {
// get diffs[diffid]
// revert current file based on diff
// refresh()
}
// sweep
initializeSweep({ startLine }) {
// reject all diffs on the page
// store original code
// currentLine=start of sweep
}
onUpdateSweep(addedText) {
// update final
// refresh() ?
// currentLine += number of newlines in addedText
}
onAbortSweep() {
}
}
*/
@@ -1,315 +0,0 @@
import * as vscode from 'vscode';
import { findDiffs } from './findDiffs';
import { Diff, BaseDiffArea, BaseDiff, DiffArea } from './common/shared_types';
// TODO in theory this should be disposed
const greenDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(0 255 51 / 0.2)',
isWholeLine: false, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:...
})
// responsible for displaying diffs and showing accept/reject buttons
export class DisplayChangesProvider implements vscode.CodeLensProvider {
private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {}
private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {}
private _diffareaidPool = 0
private _diffidPool = 0
private _weAreEditing: boolean = false
// used internally by vscode
private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
// used internally by vscode
public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
const docUriStr = document.uri.toString()
return this._diffsOfDocument[docUriStr]?.flatMap(diff => diff.lenses) ?? []
}
// declared by us, registered with vscode.languages.registerCodeLensProvider()
constructor() {
console.log('Creating DisplayChangesProvider')
// this acts as a useEffect. Every time text changes, clear the diffs in this editor
vscode.workspace.onDidChangeTextDocument((e) => {
const editor = vscode.window.activeTextEditor
if (!editor)
return
if (this._weAreEditing)
return
const docUri = editor.document.uri
const docUriStr = docUri.toString()
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
// loop through each change
for (const change of e.contentChanges) {
// here, `change.range` is the range of the original file that gets replaced with `change.text`
// compute net number of newlines lines that were added/removed
const numNewLines = (change.text.match(/\n/g) || []).length
const numLineDeletions = change.range.end.line - change.range.start.line
const deltaNewlines = numNewLines - numLineDeletions
// compute overlap with each diffArea and shrink/elongate the diffArea accordingly
for (const diffArea of diffAreas) {
// if the change is fully within the diffArea, elongate it by the delta amount of newlines
if (change.range.start.line >= diffArea.startLine && change.range.end.line <= diffArea.endLine) {
diffArea.endLine += deltaNewlines
}
// check if the `diffArea` was fully deleted and remove it if so
if (diffArea.startLine > diffArea.endLine) {
//remove it
const index = diffAreas.findIndex(da => da === diffArea)
diffAreas.splice(index, 1)
}
// TODO handle other cases where eg. the change overlaps many diffAreas
}
// if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines
for (const diffArea of diffAreas) {
if (diffArea.startLine > change.range.end.line) {
diffArea.startLine += deltaNewlines
diffArea.endLine += deltaNewlines
}
}
// TODO merge any diffAreas if they overlap with each other as a result from the shift
}
// refresh the diffAreas
this.refreshDiffAreas(docUri)
})
}
// used by us only
public addDiffArea(uri: vscode.Uri, diffArea: BaseDiffArea) {
const uriStr = uri.toString()
// make sure array is defined
if (!this._diffAreasOfDocument[uriStr])
this._diffAreasOfDocument[uriStr] = []
// remove all diffAreas that the new `diffArea` is overlapping with
this._diffAreasOfDocument[uriStr] = this._diffAreasOfDocument[uriStr].filter(da => {
const noOverlap = da.startLine > diffArea.endLine || da.endLine < diffArea.startLine
if (!noOverlap) return false
return true
})
// add `diffArea` to storage
this._diffAreasOfDocument[uriStr].push({
...diffArea,
diffareaid: this._diffareaidPool
})
this._diffareaidPool += 1
}
// used by us only
public refreshDiffAreas(docUri: vscode.Uri) {
const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor
if (!editor) {
console.log('Error: No active editor!')
return;
}
const docUriStr = docUri.toString()
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
// reset all diffs (we update them below)
this._diffsOfDocument[docUriStr] = []
// for each diffArea
for (const diffArea of diffAreas) {
// get code inside of diffArea
const currentCode = editor.document.getText(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)).replace(/\r\n/g, '\n')
// compute the diffs
const diffs = findDiffs(diffArea.originalCode, currentCode)
// print diffs
console.log('!CODEBefore:', JSON.stringify(diffArea.originalCode))
console.log('!CODEAfter:', JSON.stringify(currentCode))
// add the diffs to `this._diffsOfDocument[docUriStr]`
this.addDiffs(editor.document.uri, diffs, diffArea)
for (const diff of this._diffsOfDocument[docUriStr]) {
console.log('------------')
console.log('deletedCode:', JSON.stringify(diff.deletedCode))
console.log('insertedCode:', JSON.stringify(diff.insertedCode))
console.log('deletedRange:', diff.deletedRange.start.line, diff.deletedRange.end.line,)
console.log('insertedRange:', diff.insertedRange.start.line, diff.insertedRange.end.line,)
}
}
// update green highlighting
editor.setDecorations(
greenDecoration,
(this._diffsOfDocument[docUriStr]
.filter(diff => diff.insertedRange !== undefined)
.map(diff => diff.insertedRange)
)
);
// TODO update red highlighting
// this._diffsOfDocument[docUriStr].map(diff => diff.deletedCode)
// update code lenses
this._onDidChangeCodeLenses.fire()
}
// used by us only
public addDiffs(docUri: vscode.Uri, diffs: BaseDiff[], diffArea: DiffArea) {
const docUriStr = docUri.toString()
// if no diffs, set diffs to []
if (!this._diffsOfDocument[docUriStr])
this._diffsOfDocument[docUriStr] = []
// add each diff and its codelens to the document
for (let i = diffs.length - 1; i > -1; i -= 1) {
let suggestedDiff = diffs[i]
this._diffsOfDocument[docUriStr].push({
...suggestedDiff,
diffid: this._diffidPool,
// originalCode: suggestedDiff.deletedText,
lenses: [
new vscode.CodeLens(suggestedDiff.insertedRange, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }),
new vscode.CodeLens(suggestedDiff.insertedRange, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] })
]
});
this._diffidPool += 1
}
}
// called on void.acceptDiff
public async acceptDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
const editor = vscode.window.activeTextEditor
if (!editor)
return
// get document uri
const docUri = editor.document.uri
const docUriStr = docUri.toString()
// get relevant diff
// TODO speed up with hashmap
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
if (diffIdx === -1) {
console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
}
// get relevant diffArea
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
if (diffareaIdx === -1) {
console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
}
const diff = this._diffsOfDocument[docUriStr][diffIdx]
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
// replace `originalCode[diff.deletedRange]` with diff.insertedCode
// TODO add a history event to undo this change
const originalLines = diffArea.originalCode.split('\n');
const relativeStart = diff.deletedRange.start.line - diffArea.originalStartLine
const relativeEnd = diff.deletedRange.end.line - diffArea.originalStartLine
diffArea.originalCode = [
...originalLines.slice(0, relativeStart), // lines before the deleted range
...diff.insertedCode.split('\n'), // inserted lines
...originalLines.slice(relativeEnd + 1) // lines after the deleted range
].join('\n')
// if the diffArea has no changes, remove it
const currentDiffAreaCode = editor.document.getText()
.replace(/\r\n/g, '\n')
.split('\n')
.slice(diffArea.startLine, diffArea.endLine + 1)
.join('\n')
if (diffArea.originalCode === currentDiffAreaCode) { // if the currentDiffAreaCode === diffArea.originalCode, remove the diffArea
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1)
}
// refresh the diff area
this.refreshDiffAreas(docUri)
}
// called on void.rejectDiff
public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
const editor = vscode.window.activeTextEditor
if (!editor)
return
// get document uri
const docUri = editor.document.uri
const docUriStr = docUri.toString()
// get relevant diff
// TODO speed up with hashmap
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
if (diffIdx === -1) {
console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
}
// get relevant diffArea
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
if (diffareaIdx === -1) {
console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
}
const diff = this._diffsOfDocument[docUriStr][diffIdx]
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
// replace `editorCode[diff.insertedRange]` with diff.deletedCode
const workspaceEdit = new vscode.WorkspaceEdit();
workspaceEdit.replace(docUri, diff.insertedRange, diff.deletedCode)
this._weAreEditing = true
await vscode.workspace.applyEdit(workspaceEdit)
this._weAreEditing = false
// if the diffArea has no changes, remove it
const currentDiffAreaCode = editor.document.getText()
.replace(/\r\n/g, '\n')
.split('\n')
.slice(diffArea.startLine, diffArea.endLine + 1)
.join('\n')
if (diffArea.originalCode === currentDiffAreaCode) { // if the currentDiffAreaCode === diffArea.originalCode, remove the diffArea
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1)
}
// refresh the diff area
this.refreshDiffAreas(docUri)
}
}
+101
View File
@@ -0,0 +1,101 @@
import * as vscode from 'vscode';
import { AbortRef, OnFinalMessage, OnText, sendLLMMessage } from "./sendLLMMessage"
import { VoidConfig } from '../sidebar/contextForConfig';
import { findDiffs } from '../findDiffs';
import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from './systemPrompts';
import { throttle } from 'lodash';
import { readFileContentOfUri } from './readFileContentOfUri';
type Res<T> = ((value: T) => void)
const THRTOTLE_TIME = 100 // minimum time between edits
const LINES_PER_CHUNK = 20 // number of lines to search at a time
const applyCtrlLChangesToFile = throttle(
({ fileUri, newCurrentLine, oldCurrentLine, fullCompletedStr, oldFileStr, debug }: { fileUri: vscode.Uri, newCurrentLine: number, oldCurrentLine: number, fullCompletedStr: string, oldFileStr: string, debug?: string }) => {
// write the change to the file
const WRITE_TO_FILE = (
fullCompletedStr.split('\n').slice(0, newCurrentLine + 1).join('\n') // newFile[:newCurrentLine+1]
+ oldFileStr.split('\n').slice(oldCurrentLine + 1).join('\n') // oldFile[oldCurrentLine+1:]
)
const workspaceEdit = new vscode.WorkspaceEdit()
workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), WRITE_TO_FILE)
vscode.workspace.applyEdit(workspaceEdit)
// highlight the `newCurrentLine` in white
// highlight the remaining part of the file in gray
},
THRTOTLE_TIME, { trailing: true }
)
const applyCtrlK = async ({ fileUri, startLine, endLine, instructions, voidConfig, abortRef }: { fileUri: vscode.Uri, startLine: number, endLine: number, instructions: string, voidConfig: VoidConfig, abortRef: AbortRef }) => {
const fileStr = await readFileContentOfUri(fileUri)
const fileLines = fileStr.split('\n')
const prefix = fileLines.slice(startLine).join('\n')
const suffix = fileLines.slice(endLine + 1).join('\n')
const selection = fileLines.slice(startLine, endLine + 1).join('\n')
const promptContent = `Here is the user's original selection:
\`\`\`
<MID>${selection}</MID>
\`\`\`
The user wants to apply the following instructions to the selection:
${instructions}
Please rewrite the selection following the user's instructions.
Instructions to follow:
1. Follow the user's instructions
2. You may ONLY CHANGE the selection, and nothing else in the file
3. Make sure all brackets in the new selection are balanced the same was as in the original selection
3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake
Complete the following:
\`\`\`
<PRE>${prefix}</PRE>
<SUF>${suffix}</SUF>
<MID>`;
// TODO initialize stream
// update stream
sendLLMMessage({
messages: [{ role: 'user', content: promptContent, }],
onText: async (tokenStr, completionStr) => {
// TODO update stream
// apply the changes
const newCode = `${prefix}\n${completionStr}\n${suffix}`
const workspaceEdit = new vscode.WorkspaceEdit()
workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), newCode)
vscode.workspace.applyEdit(workspaceEdit)
},
onFinalMessage: (completionStr) => {
// TODO end stream
// apply the changes
const newCode = `${prefix}\n${completionStr}\n${suffix}`
const workspaceEdit = new vscode.WorkspaceEdit()
workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), newCode)
vscode.workspace.applyEdit(workspaceEdit)
},
onError: (e) => {
console.error('Error rewriting file with diff', e);
},
voidConfig,
abortRef,
})
}
export { applyCtrlK }
+191
View File
@@ -0,0 +1,191 @@
import * as vscode from 'vscode';
import { AbortRef, OnFinalMessage, OnText, sendLLMMessage } from "./sendLLMMessage"
import { VoidConfig } from '../sidebar/contextForConfig';
import { findDiffs } from '../findDiffs';
import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from './systemPrompts';
import { throttle } from 'lodash';
import { readFileContentOfUri } from './readFileContentOfUri';
import { DiffProvider } from '../DiffProvider';
import { DiffArea } from './shared_types';
const LINES_PER_CHUNK = 20 // number of lines to search at a time
// const THRTOTLE_TIME = 100 // minimum time between edits
// throttle(
// THRTOTLE_TIME, { trailing: true }
// )
type CompetedReturn = { isFinished: true, } | { isFinished?: undefined, }
const streamChunk = ({ diffProvider, docUri, oldFileStr, completedStr, diffRepr, diffArea, voidConfig, abortRef }: { diffProvider: DiffProvider, docUri: vscode.Uri, oldFileStr: string, completedStr: string, diffRepr: string, voidConfig: VoidConfig, diffArea: DiffArea, abortRef: AbortRef }) => {
const NUM_MATCHUP_TOKENS = 20
const promptContent = `ORIGINAL_FILE
\`\`\`
${oldFileStr}
\`\`\`
DIFF
\`\`\`
${diffRepr}
\`\`\`
INSTRUCTIONS
Please finish writing the new file \`NEW_FILE\`. Return ONLY the completion of the file, without any explanation.
NEW_FILE
\`\`\`
${completedStr}
\`\`\`
`
// create a promise that can be awaited
return new Promise<CompetedReturn>((resolve, reject) => {
let isAnyChangeSoFar = false
// make LLM complete the file to include the diff
sendLLMMessage({
messages: [{ role: 'system', content: writeFileWithDiffInstructions, }, { role: 'user', content: promptContent, }],
onText: (newText, fullText) => {
const fullCompletedStr = completedStr + fullText
diffProvider.updateStream(docUri.toString(), diffArea, fullCompletedStr)
// if there was any change from the original file
if (!oldFileStr.includes(fullCompletedStr)) {
isAnyChangeSoFar = true
}
const isRecentMatchup = false
// the final NUM_MATCHUP_TOKENS characters of fullCompletedStr are the same as the final NUM_MATCHUP_TOKENS characters of the last item in the diffs of oldFileStr that had 0 changes
if (isAnyChangeSoFar && isRecentMatchup) {
diffProvider.updateStream(docUri.toString(), diffArea, fullCompletedStr)
// TODO resolve the promise
// resolve({ speculativeIndex: newCurrentLine + 1 });
// abort the LLM call
abortRef.current?.()
}
},
onFinalMessage: (fullText) => {
const newCompletedStr = completedStr + fullText
diffProvider.updateStream(docUri.toString(), diffArea, newCompletedStr)
resolve({ isFinished: true });
},
onError: (e) => {
resolve({ isFinished: true });
console.error('Error rewriting file with diff', e);
},
voidConfig,
abortRef,
})
})
}
const shouldApplyDiff = ({ diffRepr, oldFileStr: fileStr, speculationStr, voidConfig, abortRef }: { diffRepr: string, oldFileStr: string, speculationStr: string, voidConfig: VoidConfig, abortRef: AbortRef }) => {
const promptContent = `DIFF
\`\`\`
${diffRepr}
\`\`\`
FILES
\`\`\`
${fileStr}
\`\`\`
SELECTION
\`\`\`
${speculationStr}
\`\`\`
Return \`true\` if ANY part of the chunk should be modified, and \`false\` if it should not be modified. You should respond only with \`true\` or \`false\` and nothing else.
`
// create new promise
return new Promise<boolean>((resolve, reject) => {
// send message to LLM
sendLLMMessage({
messages: [{ role: 'system', content: searchDiffChunkInstructions, }, { role: 'user', content: promptContent, }],
onFinalMessage: (finalMessage) => {
const containsTrue = finalMessage
.slice(-10) // check for `true` in last 10 characters
.toLowerCase()
.includes('true')
resolve(containsTrue)
},
onError: (e) => {
resolve(false);
console.error('Error in shouldApplyDiff: ', e)
},
onText: () => { },
voidConfig,
abortRef,
})
})
}
// lazily applies the diff to the file
// we chunk the text in the file, and ask an LLM whether it should edit each chunk
const applyDiffLazily = async ({ docUri, oldFileStr, voidConfig, abortRef, diffRepr, diffProvider, diffArea }: { docUri: vscode.Uri, oldFileStr: string, diffRepr: string, voidConfig: VoidConfig, diffProvider: DiffProvider, diffArea: DiffArea, abortRef: AbortRef }) => {
// stateful variables
let speculativeIndex = 0
let writtenTextSoFar: string[] = []
while (speculativeIndex < oldFileStr.split('\n').length) {
const chunkStr = oldFileStr.split('\n').slice(speculativeIndex, speculativeIndex + LINES_PER_CHUNK).join('\n')
// ask LLM if we should apply the diff to the chunk
const START = new Date().getTime()
let shouldApplyDiff_ = await shouldApplyDiff({ oldFileStr, speculationStr: chunkStr, diffRepr, voidConfig, abortRef })
const END = new Date().getTime()
// if should not change the chunk
if (!shouldApplyDiff_) {
console.log('KEEP CHUNK time: ', END - START)
speculativeIndex += LINES_PER_CHUNK
writtenTextSoFar.push(chunkStr)
diffProvider.updateStream(docUri.toString(), diffArea, writtenTextSoFar.join('\n'))
continue;
}
// ask LLM to rewrite file with diff (if there is significant matchup with the original file, we stop rewriting)
const START2 = new Date().getTime()
const completedStr = (await readFileContentOfUri(docUri)).split('\n').slice(0, speculativeIndex).join('\n');
const result = await streamChunk({ diffProvider, docUri, oldFileStr, completedStr, diffRepr, voidConfig, diffArea, abortRef, })
const END2 = new Date().getTime()
console.log('EDIT CHUNK time: ', END2 - START2);
// if we are finished, stop the loop
if (result.isFinished) {
break;
}
// TODO
// speculativeIndex = result.speculativeIndex
}
}
export { applyDiffLazily }
@@ -0,0 +1,14 @@
import * as vscode from 'vscode'
export const readFileContentOfUri = async (uri: vscode.Uri): Promise<string> => {
const document = await vscode.workspace.openTextDocument(uri);
return document.getText().replace(/\r\n/g, '\n') ?? '' // Normalize line endings
};
// this is the old version, which only reads the most recently saved version
// export const readFileContentOfUri = async (uri: vscode.Uri) => {
// return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8')
// .replace(/\r\n/g, '\n') // replace windows \r\n with \n
// }
+53 -41
View File
@@ -3,37 +3,39 @@ import OpenAI from 'openai';
import { Ollama } from 'ollama/browser'
import { VoidConfig } from '../sidebar/contextForConfig';
export type AbortRef = { current: (() => void) | null }
export type OnText = (newText: string, fullText: string) => void
export type OnFinalMessage = (input: string) => void
type OnText = (newText: string, fullText: string) => void
export type LLMMessageAnthropic = {
role: 'user' | 'assistant',
content: string,
}
export type LLMMessage = {
role: 'user' | 'assistant',
content: string
role: 'system' | 'user' | 'assistant',
content: string,
}
type SendLLMMessageFnTypeInternal = (params: {
messages: LLMMessage[],
onText: OnText,
onFinalMessage: (input: string) => void,
onFinalMessage: OnFinalMessage,
onError: (error: string) => void,
voidConfig: VoidConfig,
})
=> {
abort: () => void
}
abortRef: AbortRef,
}) => void
type SendLLMMessageFnTypeExternal = (params: {
messages: LLMMessage[],
onText: OnText,
onFinalMessage: (input: string) => void,
onFinalMessage: (fullText: string) => void,
onError: (error: string) => void,
voidConfig: VoidConfig | null,
})
=> {
abort: () => void
}
abortRef: AbortRef,
}) => void
@@ -43,10 +45,20 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi
const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
// find system messages and concatenate them
const systemMessage = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n');
// remove system messages for Anthropic
const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
const stream = anthropic.messages.stream({
system: systemMessage,
messages: anthropicMessages,
model: voidConfig.anthropic.model,
max_tokens: parseInt(voidConfig.anthropic.maxTokens),
messages: messages,
max_tokens: parseInt(voidConfig.default.maxTokens),
});
let did_abort = false
@@ -77,25 +89,24 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
const abort = () => {
// stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error
did_abort = true
stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error
}
return { abort }
};
// OpenAI, OpenRouter, OpenAICompatible
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let didAbort = false
let fullText = ''
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
let abort: () => void = () => {
abortRef.current = () => {
didAbort = true;
};
@@ -104,7 +115,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
if (voidConfig.default.whichApi === 'openAI') {
openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true });
options = { model: voidConfig.openAI.model, messages: messages, stream: true, }
options = { model: voidConfig.openAI.model, messages: messages, stream: true, max_completion_tokens: parseInt(voidConfig.default.maxTokens) }
}
else if (voidConfig.default.whichApi === 'openRouter') {
openai = new OpenAI({
@@ -114,11 +125,11 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
"X-Title": 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
},
});
options = { model: voidConfig.openRouter.model, messages: messages, stream: true, }
options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: parseInt(voidConfig.default.maxTokens) }
}
else if (voidConfig.default.whichApi === 'openAICompatible') {
openai = new OpenAI({ baseURL: voidConfig.openAICompatible.endpoint, apiKey: voidConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true })
options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, }
options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, max_completion_tokens: parseInt(voidConfig.default.maxTokens) }
}
else {
console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`)
@@ -128,7 +139,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
openai.chat.completions
.create(options)
.then(async response => {
abort = () => {
abortRef.current = () => {
// response.controller.abort()
didAbort = true;
}
@@ -156,18 +167,17 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
}
})
return { abort };
};
// Ollama
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let didAbort = false
let fullText = ""
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
let abort = () => {
abortRef.current = () => {
didAbort = true;
};
@@ -177,10 +187,11 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
model: voidConfig.ollama.model,
messages: messages,
stream: true,
options: { num_predict: parseInt(voidConfig.default.maxTokens) } // this is max_tokens
})
.then(async stream => {
abort = () => {
// ollama.abort()
abortRef.current = () => {
// stream.abort()
didAbort = true
}
// iterate through the stream
@@ -198,7 +209,6 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
onError(error)
})
return { abort };
};
@@ -207,13 +217,15 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
// https://docs.greptile.com/api-reference/query
// https://docs.greptile.com/quickstart#sample-response-streamed
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let didAbort = false
let fullText = ''
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
let abort: () => void = () => { didAbort = true }
abortRef.current = () => {
didAbort = true
}
fetch('https://api.greptile.com/v2/query', {
@@ -226,7 +238,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
body: JSON.stringify({
messages,
stream: true,
repositories: [voidConfig.greptile.repoinfo]
repositories: [voidConfig.greptile.repoinfo],
}),
})
// this is {message}\n{message}\n{message}...\n
@@ -268,28 +280,28 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
onError(e)
});
return { abort }
}
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
if (!voidConfig) return { abort: () => { } }
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
if (!voidConfig) return;
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
switch (voidConfig.default.whichApi) {
case 'anthropic':
return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig });
return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'openAI':
case 'openRouter':
case 'openAICompatible':
return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig });
return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'ollama':
return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig });
return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'greptile':
return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig });
return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
default:
onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
return { abort: () => { } }
}
}
+20 -18
View File
@@ -10,26 +10,23 @@ type CodeSelection = { selectionStr: string, selectionRange: vscode.Range, fileP
type File = { filepath: vscode.Uri, content: string }
// an area that is currently being diffed
type BaseDiffArea = {
// use `startLine` and `endLine` instead of `range` for mutibility
// bounds are relative to the file, inclusive
startLine: number;
endLine: number;
type DiffArea = {
diffareaid: number,
startLine: number,
endLine: number,
originalStartLine: number,
originalEndLine: number,
originalCode: string, // the original chunk of code (not necessarily the whole file)
// `newCode: string,` is not included because it is the code in the actual file, `document.text()[startline: endLine + 1]`
sweepIndex: number | null // null iff not sweeping
}
type DiffArea = BaseDiffArea & { diffareaid: number }
// the return type of diff creator
type BaseDiff = {
code: string; // representation of the diff in text
deletedRange: vscode.Range; // relative to the file, inclusive
insertedRange: vscode.Range;
deletedCode: string;
insertedCode: string;
type: 'edit' | 'insertion' | 'deletion';
// repr: string; // representation of the diff in text
originalRange: vscode.Range;
originalCode: string;
range: vscode.Range;
code: string;
}
// each diff on the user's screen
@@ -53,7 +50,7 @@ type MessageToSidebar = (
// sidebar -> editor
type MessageFromSidebar = (
| { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar
| { type: 'applyChanges', diffRepr: string } // user clicks "apply" in the sidebar
| { type: 'requestFiles', filepaths: vscode.Uri[] }
| { type: 'getPartialVoidConfig' }
| { type: 'persistPartialVoidConfig', partialVoidConfig: PartialVoidConfig }
@@ -83,12 +80,17 @@ type ChatMessage =
| {
role: "assistant";
content: string; // content received from LLM
displayContent: string; // content displayed to user (this is the same as content for now)
displayContent: string | undefined; // content displayed to user (this is the same as content for now)
}
| {
role: "system";
content: string;
displayContent?: undefined;
}
export {
BaseDiff, BaseDiffArea,
Diff, DiffArea,
BaseDiff, Diff,
DiffArea,
CodeSelection,
File,
MessageFromSidebar,
+406
View File
@@ -0,0 +1,406 @@
const generateDiffInstructions = `
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
All changes made to files must be outputted in unified diff format.
Unified diff format instructions:
1. Each diff must begin with \`\`\`@@ ... @@\`\`\`.
2. Each line must start with a \`+\` or \`-\` or \` \` symbol.
3. Make diffs more than a few lines.
4. Make high-level diffs rather than many one-line diffs.
Here's an example of unified diff format:
\`\`\`
@@ ... @@
-def factorial(n):
- if n == 0:
- return 1
- else:
- return n * factorial(n-1)
+def factorial(number):
+ if number == 0:
+ return 1
+ else:
+ return number * factorial(number-1)
\`\`\`
Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped:
\`\`\`
@@ ... @@ # This is less preferred because edits are close together and should be grouped:
-def factorial(n):
+def factorial(number):
- if n == 0:
+ if number == 0:
return 1
else:
- return n * factorial(n-1)
+ return number * factorial(number-1)
\`\`\`
# Example 1:
FILES
selected file \`test.ts\`:
\`\`\`
x = 1
{{selection}}
z = 3
\`\`\`
SELECTION
\`\`\`const y = 2\`\`\`
INSTRUCTIONS
\`\`\`y = 3\`\`\`
EXPECTED RESULT
We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`.
\`\`\`
@@ ... @@
-x = 1
-
-y = 2
+x = 1
+
+y = 3
\`\`\`
# Example 2:
FILES
selected file \`Sidebar.tsx\`:
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
{{selection}}
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</button>
</li>
))}
</ul>
<button className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</button>
</div>
);
};
export default Sidebar;
\`\`\`
SELECTION
\`\`\` <button\`\`\`
INSTRUCTIONS
\`\`\`make all the buttons like this into divs\`\`\`
EXPECTED OUTPUT
We should change all the buttons like the one selected into a div component. Here is the change:
\`\`\`
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
- {items.map((item, index) => (
- <li key={index}>
- <button
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
- </button>
- </li>
- ))}
-</ul>
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
- Extra Action
-</button>
-</div>
+<div className={styles.sidebar}>
+<ul>
+ {items.map((item, index) => (
+ <li key={index}>
+ <div
+ className={styles.sidebarButton}
+ onClick={() => onItemSelect?.(item.label)}
+ >
+ {item.label}
+ </div>
+ </li>
+ ))}
+</ul>
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
+ Extra Action
+</div>
+</div>
\`\`\`
`;
const searchDiffChunkInstructions = `
You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file.
Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it.
# Example 1:
FILES
selected file \`Sidebar.tsx\`:
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<button
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</button>
</li>
))}
</ul>
<button className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</button>
</div>
);
};
export default Sidebar;
\`\`\`
DIFF
\`\`\`
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
- {items.map((item, index) => (
- <li key={index}>
- <button
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
- </button>
- </li>
- ))}
-</ul>
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
- Extra Action
-</button>
-</div>
+<div className={styles.sidebar}>
+<ul>
+ {items.map((item, index) => (
+ <li key={index}>
+ <div
+ className={styles.sidebarButton}
+ onClick={() => onItemSelect?.(item.label)}
+ >
+ {item.label}
+ </div>
+ </li>
+ ))}
+</ul>
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
+ Extra Action
+</div>
+</div>
\`\`\`
SELECTION
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
\`\`\`
RESULT
The output should be \`true\` because the diff begins on the line with \`<div className={styles.sidebar}>\` and this line is present in the selection.
OUTPUT
\`true\`
`
const writeFileWithDiffInstructions = `
You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
Please finish writing the new file \`new_file\`, according to the diff \`diff\`.
Directions:
1. Continue exactly where the new file \`new_file\` left off.
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change.
# Example 1:
ORIGINAL_FILE
\`Sidebar.tsx\`:
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<button
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</button>
</li>
))}
</ul>
<button className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</button>
</div>
);
};
export default Sidebar;
\`\`\`
DIFF
\`\`\`
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
- {items.map((item, index) => (
- <li key={index}>
- <button
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
- </button>
- </li>
- ))}
-</ul>
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
- Extra Action
-</button>
-</div>
+<div className={styles.sidebar}>
+<ul>
+ {items.map((item, index) => (
+ <li key={index}>
+ <div
+ className={styles.sidebarButton}
+ onClick={() => onItemSelect?.(item.label)}
+ >
+ {item.label}
+ </div>
+ </li>
+ ))}
+</ul>
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
+ Extra Action
+</div>
+</div>
\`\`\`
NEW_FILE
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
\`\`\`
COMPLETION
\`\`\`
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<div
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</div>
</li>
))}
</ul>
<div className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</div>
</div>
);
};
export default Sidebar;\`\`\`
`
export {
generateDiffInstructions,
searchDiffChunkInstructions,
writeFileWithDiffInstructions,
};
+19 -32
View File
@@ -1,8 +1,12 @@
import * as vscode from 'vscode';
import { DisplayChangesProvider } from './DisplayChangesProvider';
import { BaseDiffArea, ChatThreads, MessageFromSidebar, MessageToSidebar } from './common/shared_types';
import { DiffProvider } from './DiffProvider';
import { DiffArea, ChatThreads, MessageFromSidebar, MessageToSidebar } from './common/shared_types';
import { SidebarWebviewProvider } from './SidebarWebviewProvider';
import { v4 as uuidv4 } from 'uuid'
import { applyDiffLazily } from './common/ctrlL';
import { getVoidConfig } from './sidebar/contextForConfig';
import { readFileContentOfUri } from './common/readFileContentOfUri';
import { AbortRef } from './common/sendLLMMessage';
// this comes from vscode.proposed.editorInsets.d.ts
declare module 'vscode' {
@@ -19,13 +23,6 @@ declare module 'vscode' {
}
}
const readFileContentOfUri = async (uri: vscode.Uri) => {
return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8')
.replace(/\r\n/g, '\n') // replace windows \r\n with \n
}
const roundRangeToLines = (selection: vscode.Selection) => {
return new vscode.Range(selection.start.line, 0, selection.end.line, Number.MAX_SAFE_INTEGER)
}
@@ -95,15 +92,15 @@ export function activate(context: vscode.ExtensionContext) {
);
// 3. Show an approve/reject codelens above each change
const displayChangesProvider = new DisplayChangesProvider();
context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', displayChangesProvider));
const diffProvider = new DiffProvider();
context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', diffProvider));
// 4. Add approve/reject commands
context.subscriptions.push(vscode.commands.registerCommand('void.acceptDiff', async (params) => {
displayChangesProvider.acceptDiff(params)
diffProvider.acceptDiff(params)
}));
context.subscriptions.push(vscode.commands.registerCommand('void.rejectDiff', async (params) => {
displayChangesProvider.rejectDiff(params)
diffProvider.rejectDiff(params)
}));
// 5. Receive messages from sidebar
@@ -124,6 +121,8 @@ export function activate(context: vscode.ExtensionContext) {
// Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`)
webview.onDidReceiveMessage(async (m: MessageFromSidebar) => {
const abortApplyRef: AbortRef = { current: null }
if (m.type === 'requestFiles') {
// get contents of all file paths
@@ -142,32 +141,21 @@ export function activate(context: vscode.ExtensionContext) {
vscode.window.showInformationMessage('No active editor!')
return
}
// create an area to show diffs
const diffArea: BaseDiffArea = {
const partialDiffArea: Omit<DiffArea, 'diffareaid'> = {
startLine: 0, // in ctrl+L the start and end lines are the full document
endLine: editor.document.lineCount,
originalStartLine: 0,
originalEndLine: editor.document.lineCount,
originalCode: await readFileContentOfUri(editor.document.uri),
sweepIndex: null,
}
displayChangesProvider.addDiffArea(editor.document.uri, diffArea)
const diffArea = diffProvider.createDiffArea(editor.document.uri, partialDiffArea, await readFileContentOfUri(editor.document.uri))
const docUri = editor.document.uri
const fileStr = await readFileContentOfUri(docUri)
const voidConfig = getVoidConfig(context.globalState.get('partialVoidConfig') ?? {})
// write new code `m.code` to the document
// TODO update like this:
// this._weAreEditing = true
// await vscode.workspace.applyEdit(workspaceEdit)
// await vscode.workspace.save(docUri)
// this._weAreEditing = false
await editor.edit(editBuilder => {
editBuilder.replace(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER), m.code);
});
// rediff the changes based on the diffAreas
displayChangesProvider.refreshDiffAreas(editor.document.uri)
await applyDiffLazily({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffProvider, diffArea, abortRef: abortApplyRef })
}
else if (m.type === 'getPartialVoidConfig') {
const partialVoidConfig = context.globalState.get('partialVoidConfig') ?? {}
@@ -219,4 +207,3 @@ export function activate(context: vscode.ExtensionContext) {
// )
}
+107 -253
View File
@@ -1,277 +1,131 @@
import * as vscode from 'vscode';
// import { diffLines, Change } from 'diff';
import { Range } from 'vscode';
import { diffLines, Change } from 'diff';
import { BaseDiff } from './common/shared_types';
import { diff_match_patch } from 'diff-match-patch';
// class Range {
// range: any;
// constructor(startLine, startCol, endLine, endCol) {
// const range = {
// startLine,
// startCol,
// endLine,
// endCol,
// };
// this.range = range;
// }
// }
const diffLines = (text1: string, text2: string) => {
var dmp = new diff_match_patch();
var a = dmp.diff_linesToChars_(text1, text2);
var lineText1 = a.chars1;
var lineText2 = a.chars2;
var lineArray = a.lineArray;
var diffs = dmp.diff_main(lineText1, lineText2, false);
dmp.diff_charsToLines_(diffs, lineArray);
// dmp.diff_cleanupSemantic(diffs);
return diffs;
// Andrew diff algo:
export type SuggestedEdit = {
// start/end of current file
newRange: Range;
// start/end of original file
originalRange: Range;
type: 'insertion' | 'deletion' | 'edit',
originalContent: string, // original content (originalfile[originalStart...originalEnd])
newContent: string,
}
export function findDiffs(oldStr: string, newStr: string) {
// an ordered list of every original line, line added to the new file, and line removed from the old file (order is unambiguous, think about it)
const lineByLineChanges: Change[] = diffLines(oldStr, newStr);
lineByLineChanges.push({ value: '' }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed)
// TODO use a better diff algorithm
export const findDiffs = (oldText: string, newText: string): BaseDiff[] => {
let oldFileLineNum: number = 0;
let newFileLineNum: number = 0;
const diffs = diffLines(oldText, newText);
let streakStartInNewFile: number | undefined = undefined
let streakStartInOldFile: number | undefined = undefined
const blocks: BaseDiff[] = [];
let reprBlock: string[] = [];
let deletedBlock: string[] = [];
let insertedBlock: string[] = [];
let insertedLine = 0;
let deletedLine = 0;
let insertedStart = 0;
let deletedStart = 0;
let oldStrLines = oldStr.split('\n')
let newStrLines = newStr.split('\n')
diffs.forEach(([operation, text]) => {
const replacements: BaseDiff[] = []
for (let line of lineByLineChanges) {
const lines = text.split('\n');
// no change on this line
if (!line.added && !line.removed) {
switch (operation) {
// do nothing
// insertion
case 1:
if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
if (insertedBlock.length === 0) insertedStart = insertedLine;
insertedLine += lines.length - 1; // Update only the line count for new text
insertedBlock.push(text);
reprBlock.push(lines.map(line => `+ ${line}`).join('\n'));
break;
// if we were on a streak of +s and -s, end it
if (streakStartInNewFile !== undefined) {
let type: 'edit' | 'insertion' | 'deletion' = 'edit'
// deletion
case -1:
if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
if (deletedBlock.length === 0) deletedStart = deletedLine;
deletedLine += lines.length - 1; // Update only the line count for old text
deletedBlock.push(text);
reprBlock.push(lines.map(line => `- ${line}`).join('\n'));
break;
let startLine = streakStartInNewFile
let endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
let startCol = 0
let endCol = Number.MAX_SAFE_INTEGER
// no change
case 0:
// If we have a pending block, add it to the blocks array
if (insertedBlock.length > 0 || deletedBlock.length > 0) {
blocks.push({
code: reprBlock.join(''),
deletedCode: deletedBlock.join(''),
insertedCode: insertedBlock.join(''),
deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER),
insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER),
});
let originalStartLine = streakStartInOldFile!
let originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
let originalStartCol = 0
let originalEndCol = Number.MAX_SAFE_INTEGER
let newContent = newStrLines.slice(startLine, endLine + 1).join('\n')
let originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n')
// if the range is empty, mark it as a deletion / insertion (both won't be true at once)
// DELETION
if (endLine === startLine - 1) {
type = 'deletion'
endLine = startLine
startCol = 0
endCol = 0
newContent += '\n'
}
// Reset the block variables
reprBlock = [];
deletedBlock = [];
insertedBlock = [];
// INSERTION
else if (originalEndLine === originalStartLine - 1) {
type = 'insertion'
originalEndLine = originalStartLine
originalStartCol = 0
originalEndCol = 0
}
// Update line counts for unchanged text
insertedLine += lines.length - 1;
deletedLine += lines.length - 1;
const replacement: BaseDiff = {
type,
range: new Range(startLine, startCol, endLine, endCol),
code: newContent,
originalRange: new Range(originalStartLine, originalStartCol, originalEndLine, originalEndCol),
originalCode: originalContent,
}
break;
replacements.push(replacement)
streakStartInNewFile = undefined
streakStartInOldFile = undefined
}
oldFileLineNum += line.count ?? 0;
newFileLineNum += line.count ?? 0;
}
});
// Add any remaining blocks after the loop ends
if (insertedBlock.length > 0 || deletedBlock.length > 0) {
blocks.push({
code: reprBlock.join(''),
deletedCode: deletedBlock.join(''),
insertedCode: insertedBlock.join(''),
deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER),
insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER),
});
}
// line was removed from old file
else if (line.removed) {
// if we weren't on a streak, start one on this current line num
if (streakStartInNewFile === undefined) {
streakStartInNewFile = newFileLineNum
streakStartInOldFile = oldFileLineNum
}
oldFileLineNum += line.count ?? 0 // we processed the line so add 1
}
return blocks;
};
// line was added to new file
else if (line.added) {
// if we weren't on a streak, start one on this current line num
if (streakStartInNewFile === undefined) {
streakStartInNewFile = newFileLineNum
streakStartInOldFile = oldFileLineNum
}
newFileLineNum += line.count ?? 0; // we processed the line so add 1
}
} // end for
// export const findDiffs = (oldText: string, newText: string): DiffBlock[] => {
// const diffs = diffLines(oldText, newText);
// const blocks: DiffBlock[] = [];
// let reprBlock: string[] = [];
// let deletedBlock: string[] = [];
// let insertedBlock: string[] = [];
// let insertedEnd = 0;
// let deletedEnd = 0;
// let insertedStart = 0;
// let deletedStart = 0;
// diffs.forEach(part => {
// part.count = part.count ?? 0
// // if the part is an addition or deletion, add it to the current block
// if (part.added || part.removed) {
// if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
// if (part.added) {
// if (insertedBlock.length === 0) insertedStart = insertedEnd;
// insertedEnd += part.count
// insertedBlock.push(part.value);
// reprBlock.push(part.value.split('\n').map(line => `+ ${line}`).join('\n'));
// }
// if (part.removed) {
// if (deletedBlock.length === 0) deletedStart = deletedEnd;
// deletedEnd += part.count
// deletedBlock.push(part.value);
// reprBlock.push(part.value.split('\n').map(line => `- ${line}`).join('\n'));
// }
// }
// // if the part is unchanged, finalize the block and add it to the array
// else {
// // if the block is not null, add it to the array
// if (insertedBlock.length > 0 || deletedBlock.length > 0) {
// blocks.push({
// code: reprBlock.join('\n'),
// deletedCode: deletedBlock.join(''),
// insertedCode: insertedBlock.join(''),
// deletedRange: new vscode.Range(deletedStart, 0, deletedEnd, Number.MAX_SAFE_INTEGER),
// insertedRange: new vscode.Range(insertedStart, 0, insertedEnd, Number.MAX_SAFE_INTEGER),
// });
// }
// // update block variables
// reprBlock = [];
// deletedBlock = [];
// insertedBlock = [];
// insertedEnd += part.count;
// deletedEnd += part.count;
// }
// })
// // finally, add the last block to the array
// if (insertedBlock.length > 0 || deletedBlock.length > 0) {
// blocks.push({
// code: reprBlock.join('\n'),
// deletedCode: deletedBlock.join(''),
// insertedCode: insertedBlock.join(''),
// deletedRange: new vscode.Range(deletedStart, 0, deletedEnd, Number.MAX_SAFE_INTEGER),
// insertedRange: new vscode.Range(insertedStart, 0, insertedEnd, Number.MAX_SAFE_INTEGER),
// });
// }
// return blocks;
// }
// import { diffLines, Change } from 'diff';
// export type SuggestedEdit = {
// // start/end of current file
// startLine: number;
// endLine: number;
// // start/end of original file
// originalStartLine: number,
// originalEndLine: number,
// // original content (originalfile[originalStart...originalEnd])
// originalContent: string;
// newContent: string;
// }
// export function getDiffedLines(oldStr: string, newStr: string) {
// // an ordered list of every original line, line added to the new file, and line removed from the old file (order is unambiguous, think about it)
// const lineByLineChanges: Change[] = diffLines(oldStr, newStr);
// console.debug('Line by line changes', lineByLineChanges)
// lineByLineChanges.push({ value: '' }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed)
// let oldFileLineNum: number = 0;
// let newFileLineNum: number = 0;
// let streakStartInNewFile: number | undefined = undefined
// let streakStartInOldFile: number | undefined = undefined
// let oldStrLines = oldStr.split('\n')
// let newStrLines = newStr.split('\n')
// const replacements: SuggestedEdit[] = []
// for (let line of lineByLineChanges) {
// // no change on this line
// if (!line.added && !line.removed) {
// // if we were on a streak, add it
// if (streakStartInNewFile !== undefined) {
// const startLine = streakStartInNewFile
// const endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
// const newContent = newStrLines.slice(startLine, endLine + 1).join('\n')
// const originalStartLine = streakStartInOldFile!
// const originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
// const originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n')
// const replacement: SuggestedEdit = { startLine, endLine, newContent, originalStartLine, originalEndLine, originalContent }
// replacements.push(replacement)
// streakStartInNewFile = undefined
// streakStartInOldFile = undefined
// }
// oldFileLineNum += line.count ?? 0;
// newFileLineNum += line.count ?? 0;
// }
// // line was removed from old file
// else if (line.removed) {
// // if we weren't on a streak, start one on this current line num
// if (streakStartInNewFile === undefined) {
// streakStartInNewFile = newFileLineNum
// streakStartInOldFile = oldFileLineNum
// }
// oldFileLineNum += line.count ?? 0 // we processed the line so add 1
// }
// // line was added to new file
// else if (line.added) {
// // if we weren't on a streak, start one on this current line num
// if (streakStartInNewFile === undefined) {
// streakStartInNewFile = newFileLineNum
// streakStartInOldFile = oldFileLineNum
// }
// newFileLineNum += line.count ?? 0; // we processed the line so add 1
// }
// } // end for
// console.debug('Replacements', replacements)
// return replacements
// }
console.debug('Replacements', replacements)
return replacements
}
-1
View File
@@ -60,7 +60,6 @@ const Sidebar = () => {
</div>
</div>
</>
}
+18 -14
View File
@@ -10,6 +10,7 @@ import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMess
import { useThreads } from "./contextForThreads";
import { sendLLMMessage } from "../common/sendLLMMessage";
import { useVoidConfig } from "./contextForConfig";
import { generateDiffInstructions } from "../common/systemPrompts";
import { captureEvent } from "./metrics/posthog";
@@ -160,7 +161,8 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
const [latestError, setLatestError] = useState('')
// higher level state
const { allThreads, currentThread, addMessageToHistory, startNewThread, switchToThread } = useThreads()
const { getAllThreads, getCurrentThread, addMessageToHistory, startNewThread, switchToThread } = useThreads()
const { voidConfig } = useVoidConfig()
@@ -168,20 +170,21 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureChatEvent = useCallback((eventId: string, extras?: object) => {
const whichApi = voidConfig.default['whichApi']
const messages = currentThread?.messages
const messages = getCurrentThread()?.messages
captureEvent(eventId, {
whichApi: whichApi,
numMessages: messages?.length,
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.displayContent.length })),
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.displayContent?.length })),
version: '2024-10-19',
...extras,
})
}, [currentThread?.messages, voidConfig.default])
}, [getCurrentThread, voidConfig.default])
// if they pressed the + to add a new chat
useOnVSCodeMessage('startNewThread', (m) => {
const allThreads = getAllThreads()
// find a thread with 0 messages and switch to it
for (let threadId in allThreads) {
if (allThreads[threadId].messages.length === 0) {
@@ -224,19 +227,20 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
const relevantFiles = await awaitVSCodeResponse('files')
// add message to chat history
// add system message to chat history
const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions }
addMessageToHistory(systemPromptElt)
const userContent = userInstructionsStr(instructions, relevantFiles.files, selection)
// console.log('prompt:\n', content)
const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selection, files }
addMessageToHistory(newHistoryElt)
// send message to LLM
captureChatEvent('Chat - Sending Message', { messageLength: instructions.length })
const submit_time = new Date()
let { abort } = sendLLMMessage({
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })), { role: 'user', content: userContent }],
// send message to LLM
sendLLMMessage({
messages: [...(getCurrentThread()?.messages ?? []).map(m => ({ role: m.role, content: m.content })),],
onText: (newText, fullText) => setMessageStream(fullText),
onFinalMessage: (content) => {
captureChatEvent('Chat - Received Full Message', { messageLength: content.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
@@ -259,9 +263,10 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
setLatestError(error)
},
voidConfig: voidConfig
voidConfig,
abortRef: abortFnRef,
})
abortFnRef.current = abort
}
@@ -286,7 +291,7 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
return <>
<div className="overflow-x-hidden space-y-4">
{/* previous messages */}
{currentThread !== null && currentThread.messages.map((message, i) =>
{getCurrentThread() !== null && getCurrentThread()?.messages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} />
)}
{/* message stream */}
@@ -296,7 +301,6 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
<div className="shrink-0 py-4">
{/* selection */}
<div className="text-left">
<div className="relative">
<div className="input">
{/* selection */}
@@ -69,6 +69,10 @@ export const SidebarSettings = () => {
field='default'
param='whichApi'
/>
<SettingOfFieldAndParam
field='default'
param='maxTokens'
/>
</div>
<hr />
@@ -12,7 +12,9 @@ const truncate = (s: string) => {
export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
const { allThreads, currentThread, switchToThread } = useThreads()
const { getAllThreads, getCurrentThread, switchToThread } = useThreads()
const allThreads = getAllThreads()
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? 1 : -1)
@@ -62,7 +64,7 @@ export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
return (
<button
key={pastThread.id}
className={`btn btn-sm rounded-sm ${pastThread.id === currentThread?.id ? "btn-primary" : "btn-secondary"}`}
className={`btn btn-sm rounded-sm ${pastThread.id === getCurrentThread()?.id ? "btn-primary" : "btn-secondary"}`}
onClick={() => switchToThread(pastThread.id)}
title={new Date(pastThread.createdAt).toLocaleString()}
>
@@ -45,6 +45,18 @@ const voidConfigInfo: Record<
'anthropic',
configFields,
),
maxTokens: configEnum(
"Max number of tokens to output.",
'1024',
[
"1024",
"2048",
"4096",
"8192"
] as const,
),
},
anthropic: {
apikey: configString('Anthropic API key.', ''),
@@ -58,17 +70,6 @@ const voidConfigInfo: Record<
"claude-3-haiku-20240307"
] as const,
),
maxTokens: configEnum(
"Anthropic max number of tokens to output.",
'8192',
[
"1024",
"2048",
"4096",
"8192"
] as const,
),
},
openAI: {
apikey: configString('OpenAI API key.', ''),
@@ -184,7 +185,7 @@ export type VoidConfig = {
const getVoidConfig = (currentConfig: PartialVoidConfig): VoidConfig => {
export const getVoidConfig = (currentConfig: PartialVoidConfig): VoidConfig => {
const config = {} as PartialVoidConfig
for (let field of [...configFields, 'default'] as const) {
config[field] = {}
@@ -5,8 +5,8 @@ import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi"
// a "thread" means a chat message history
type ConfigForThreadsValueType = {
readonly allThreads: ChatThreads | null,
readonly currentThread: ChatThreads[string] | null;
readonly getAllThreads: () => ChatThreads;
readonly getCurrentThread: () => ChatThreads[string] | null;
addMessageToHistory: (message: ChatMessage) => void;
switchToThread: (threadId: string) => void;
startNewThread: () => void;
@@ -39,8 +39,8 @@ const useInstantState = <T,>(initVal: T) => {
export function ThreadsProvider({ children }: { children: ReactNode }) {
const [allThreads, setAllThreads] = useInstantState<ChatThreads>({})
const [currentThreadId, setCurrentThreadId] = useInstantState<string | null>(null)
const [allThreadsRef, setAllThreads] = useInstantState<ChatThreads>({})
const [currentThreadIdRef, setCurrentThreadId] = useInstantState<string | null>(null)
// this loads allThreads in on mount
useEffect(() => {
@@ -55,12 +55,12 @@ export function ThreadsProvider({ children }: { children: ReactNode }) {
return (
<ThreadsContext.Provider
value={{
allThreads: allThreads.current,
currentThread: currentThreadId.current === null || allThreads.current === null ? null : allThreads.current[currentThreadId.current],
getAllThreads: () => allThreadsRef.current ?? {},
getCurrentThread: () => currentThreadIdRef.current ? allThreadsRef.current?.[currentThreadIdRef.current] ?? null : null,
addMessageToHistory: (message: ChatMessage) => {
let currentThread: ChatThreads[string]
if (!(currentThreadId.current === null || allThreads.current === null)) {
currentThread = allThreads.current[currentThreadId.current]
if (!(currentThreadIdRef.current === null || allThreadsRef.current === null)) {
currentThread = allThreadsRef.current[currentThreadIdRef.current]
}
else {
currentThread = createNewThread()
@@ -68,7 +68,7 @@ export function ThreadsProvider({ children }: { children: ReactNode }) {
}
setAllThreads({
...allThreads.current,
...allThreadsRef.current,
[currentThread.id]: {
...currentThread,
lastModified: new Date().toISOString(),
@@ -84,7 +84,7 @@ export function ThreadsProvider({ children }: { children: ReactNode }) {
startNewThread: () => {
const newThread = createNewThread()
setAllThreads({
...allThreads.current,
...allThreadsRef.current,
[newThread.id]: newThread
})
setCurrentThreadId(newThread.id)
@@ -12,7 +12,7 @@ enum CopyButtonState {
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
const CodeButtonsOnHover = ({ text }: { text: string }) => {
const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
useEffect(() => {
@@ -44,7 +44,7 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={async () => {
getVSCodeAPI().postMessage({ type: "applyChanges", code: text })
getVSCodeAPI().postMessage({ type: "applyChanges", diffRepr: text })
}}
>
Apply
@@ -66,7 +66,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
return <BlockCode
text={t.text}
language={t.lang}
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
buttonsOnHover={<CodeButtonsOnHover diffRepr={t.text} />}
/>
}
+751 -100
View File
File diff suppressed because it is too large Load Diff