diff --git a/extensions/void/build-css.js b/extensions/void/build-css.js deleted file mode 100644 index f5face5c..00000000 --- a/extensions/void/build-css.js +++ /dev/null @@ -1,19 +0,0 @@ -const tailwindcss = require('tailwindcss') -const autoprefixer = require('autoprefixer') -const postcss = require('postcss') -const fs = require('fs') - -const from = 'src/sidebar/styles.css' -const to = 'dist/sidebar/styles.css' - -const original_css_contents = fs.readFileSync(from, 'utf8') - -postcss([ - tailwindcss, // this compiles tailwind of all the files specified in tailwind.config.json - autoprefixer, -]) - .process(original_css_contents, { from, to }) - .then(processed_css_contents => { fs.writeFileSync(to, processed_css_contents.css) }) - .catch(error => { - console.error('Error in build-css:', error) - }) diff --git a/extensions/void/build-tsx.js b/extensions/void/build-tsx.js deleted file mode 100644 index 8ca3eae8..00000000 --- a/extensions/void/build-tsx.js +++ /dev/null @@ -1,13 +0,0 @@ -const esbuild = require('esbuild') - -// Build JS -esbuild.build({ - entryPoints: ['src/sidebar/index.tsx'], - bundle: true, - minify: true, - sourcemap: true, - outfile: 'dist/sidebar/index.js', - format: 'iife', // apparently iife is safe for browsers (safer than cjs) - platform: 'browser', - external: ['vscode'], -}).catch(() => process.exit(1)); diff --git a/extensions/void/build/build.js b/extensions/void/build/build.js new file mode 100644 index 00000000..8b2ab8be --- /dev/null +++ b/extensions/void/build/build.js @@ -0,0 +1,59 @@ +const tailwindcss = require('tailwindcss') +const autoprefixer = require('autoprefixer') +const postcss = require('postcss') +const fs = require('fs') + +const convertTailwindToCSS = ({ from, to }) => { + console.log('converting ', from, ' --> ', to) + + const original_css_contents = fs.readFileSync(from, 'utf8') + + return postcss([ + tailwindcss, // this compiles tailwind of all the files specified in tailwind.config.json + autoprefixer, + ]) + .process(original_css_contents, { from, to }) + .then(processed_css_contents => { fs.writeFileSync(to, processed_css_contents.css) }) + .catch(error => { + console.error('Error in build-css:', error) + }) +} + + +const esbuild = require('esbuild') + +const convertTSXtoJS = async ({ from, to }) => { + console.log('converting ', from, ' --> ', to) + + return esbuild.build({ + entryPoints: [from], + bundle: true, + minify: true, + sourcemap: true, + outfile: to, + format: 'iife', // apparently iife is safe for browsers (safer than cjs) + platform: 'browser', + external: ['vscode'], + }).catch(() => process.exit(1)); +} + +(async () => { + // convert tsx to js + await convertTSXtoJS({ + from: 'src/webviews/sidebar/index.tsx', + to: 'dist/webviews/sidebar/index.js', + }) + + await convertTSXtoJS({ + from: 'src/webviews/ctrlk/index.tsx', + to: 'dist/webviews/ctrlk/index.js', + }) + + // convert tailwind to css + await convertTailwindToCSS({ + from: 'src/webviews/styles.css', + to: 'dist/webviews/styles.css', + }) + +})() + diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index e9bbd5a5..62449b32 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -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", @@ -6265,9 +6277,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 +6601,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 +6832,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 +6849,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 +8665,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" }, diff --git a/extensions/void/package.json b/extensions/void/package.json index 90b3fb6d..16553535 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -11,7 +11,7 @@ "Other" ], "activationEvents": [], - "main": "./out/extension.js", + "main": "./out/extension/extension.js", "contributes": { "configuration": { "title": "Void", @@ -104,7 +104,7 @@ "vscode:prepublish": "npm run compile", "compile": "tsc -p ./", "watch": "tsc -watch -p ./", - "build": "rimraf dist && node build-tsx.js && node build-css.js", + "build": "rimraf dist && node build/build.js", "pretest": "tsc -p ./ && eslint src --ext ts", "test": "vscode-test" }, @@ -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", diff --git a/extensions/void/src/DisplayChangesProvider.ts b/extensions/void/src/DisplayChangesProvider.ts deleted file mode 100644 index 5130a16b..00000000 --- a/extensions/void/src/DisplayChangesProvider.ts +++ /dev/null @@ -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 = new vscode.EventEmitter(); // signals a UI refresh on .fire() events - public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event; - - // used internally by vscode - public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult { - 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, run this - 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) - } -} diff --git a/extensions/void/src/SidebarWebviewProvider.ts b/extensions/void/src/SidebarWebviewProvider.ts deleted file mode 100644 index 0c2ed5a5..00000000 --- a/extensions/void/src/SidebarWebviewProvider.ts +++ /dev/null @@ -1,85 +0,0 @@ -// renders the code from `src/sidebar` - -import * as vscode from 'vscode'; - -function generateNonce() { - let text = ""; - const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} - -export class SidebarWebviewProvider implements vscode.WebviewViewProvider { - public static readonly viewId = 'void.viewnumberone'; - - public webview: Promise // used to send messages to the webview, resolved by _res in resolveWebviewView - private _res: (c: vscode.Webview) => void // used to resolve the webview - - private readonly _extensionUri: vscode.Uri - - // private _webviewView?: vscode.WebviewView; - private _webviewDeps: string[] = []; - - constructor(context: vscode.ExtensionContext) { - // const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later... was included in webviewProvider code - this._extensionUri = context.extensionUri - - let temp_res: typeof this._res | undefined = undefined - this.webview = new Promise((res, rej) => { temp_res = res }) - if (!temp_res) throw new Error("Void sidebar provider: resolver was undefined") - this._res = temp_res - } - - // called by us - updateWebviewHTML(webview: vscode.Webview) { - this._webviewDeps = [] - - const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/index.js')); - const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/styles.css')); - const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri)); - const nonce = generateNonce(); - - const webviewHTML = ` - - - - - Custom View - - - - - -
-
- - - `; - - webview.html = webviewHTML; - } - - - // called internally by vscode - resolveWebviewView( - webviewView: vscode.WebviewView, - context: vscode.WebviewViewResolveContext, - token: vscode.CancellationToken, - ) { - - const webview = webviewView.webview; - - webview.options = { - enableScripts: true, - localResourceRoots: [this._extensionUri] - }; - - this.updateWebviewHTML(webview); - - // resolve webview and _webviewView - this._res(webview); - // this._webviewView = webviewView; - } -} diff --git a/extensions/void/src/common/ctrlK.ts b/extensions/void/src/common/ctrlK.ts new file mode 100644 index 00000000..ab4feea8 --- /dev/null +++ b/extensions/void/src/common/ctrlK.ts @@ -0,0 +1,100 @@ +import * as vscode from 'vscode'; +import { AbortRef, OnFinalMessage, OnText, sendLLMMessage } from "./sendLLMMessage" +import { VoidConfig } from '../webviews/common/contextForConfig'; +import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from './systemPrompts'; +import { throttle } from 'lodash'; +import { readFileContentOfUri } from '../extension/extensionLib/readFileContentOfUri'; + +type Res = ((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: +\`\`\` +${selection} +\`\`\` + +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: +\`\`\` +
${prefix}
+${suffix} +`; + + + // 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 } \ No newline at end of file diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index a0ab0df3..43800a27 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -1,39 +1,41 @@ import Anthropic from '@anthropic-ai/sdk'; import OpenAI from 'openai'; import { Ollama } from 'ollama/browser' -import { VoidConfig } from '../sidebar/contextForConfig'; +import { VoidConfig } from '../webviews/common/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: () => { } } } } diff --git a/extensions/void/src/common/shared_types.ts b/extensions/void/src/common/shared_types.ts index e7373266..dfe72b46 100644 --- a/extensions/void/src/common/shared_types.ts +++ b/extensions/void/src/common/shared_types.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; -import { PartialVoidConfig } from '../sidebar/contextForConfig'; +import { PartialVoidConfig } from '../webviews/common/contextForConfig' @@ -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, diff --git a/extensions/void/src/common/systemPrompts.ts b/extensions/void/src/common/systemPrompts.ts new file mode 100644 index 00000000..7e443053 --- /dev/null +++ b/extensions/void/src/common/systemPrompts.ts @@ -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 = ({ items, onItemSelect, onExtraButtonClick }) => { + return ( +
+
    + {items.map((item, index) => ( +
  • + {{selection}} + className={styles.sidebarButton} + onClick={() => onItemSelect?.(item.label)} + > + {item.label} + +
  • + ))} +
+ +
+ ); +}; + +export default Sidebar; +\`\`\` + +SELECTION +\`\`\` +-
    +- {items.map((item, index) => ( +-
  • +- +-
  • +- ))} +-
+- +- ++
++
    ++ {items.map((item, index) => ( ++
  • ++
    onItemSelect?.(item.label)} ++ > ++ {item.label} ++
    ++
  • ++ ))} ++
++
++ Extra Action ++
++
+\`\`\` +`; + + +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 = ({ items, onItemSelect, onExtraButtonClick }) => { + return ( +
+
    + {items.map((item, index) => ( +
  • + +
  • + ))} +
+ +
+ ); +}; + +export default Sidebar; +\`\`\` + +DIFF +\`\`\` +@@ ... @@ +-
+-
    +- {items.map((item, index) => ( +-
  • +- +-
  • +- ))} +-
+- +-
++
++
    ++ {items.map((item, index) => ( ++
  • ++
    onItemSelect?.(item.label)} ++ > ++ {item.label} ++
    ++
  • ++ ))} ++
++
++ Extra Action ++
++
+\`\`\` + +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 = ({ items, onItemSelect, onExtraButtonClick }) => { + return ( +
+
    + {items.map((item, index) => ( +\`\`\` + +RESULT +The output should be \`true\` because the diff begins on the line with \`
    \` 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 = ({ items, onItemSelect, onExtraButtonClick }) => { + return ( +
    +
      + {items.map((item, index) => ( +
    • + +
    • + ))} +
    + +
    + ); +}; + +export default Sidebar; +\`\`\` + +DIFF +\`\`\` +@@ ... @@ +-
    +-
      +- {items.map((item, index) => ( +-
    • +- +-
    • +- ))} +-
    +- +-
    ++
    ++
      ++ {items.map((item, index) => ( ++
    • ++
      onItemSelect?.(item.label)} ++ > ++ {item.label} ++
      ++
    • ++ ))} ++
    ++
    ++ Extra Action ++
    ++
    +\`\`\` + +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 = ({ items, onItemSelect, onExtraButtonClick }) => { + return ( +\`\`\` + +COMPLETION +\`\`\` +
    +
      + {items.map((item, index) => ( +
    • +
      onItemSelect?.(item.label)} + > + {item.label} +
      +
    • + ))} +
    +
    + Extra Action +
    +
    + ); +}; + +export default Sidebar;\`\`\` +` + + + +export { + generateDiffInstructions, + searchDiffChunkInstructions, + writeFileWithDiffInstructions, +}; \ No newline at end of file diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index bebc3bf0..989bf4b9 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -1,8 +1,13 @@ import * as vscode from 'vscode'; -import { DisplayChangesProvider } from './DisplayChangesProvider'; -import { BaseDiffArea, ChatThreads, MessageFromSidebar, MessageToSidebar } from './common/shared_types'; -import { SidebarWebviewProvider } from './SidebarWebviewProvider'; + +import { DiffArea, ChatThreads, MessageFromSidebar, MessageToSidebar } from './common/shared_types'; import { v4 as uuidv4 } from 'uuid' +import { AbortRef } from './common/sendLLMMessage'; +import { DiffProvider } from './extension/DiffProvider'; +import { SidebarWebviewProvider } from './extension/providers/SidebarWebviewProvider'; +import { getVoidConfig } from './webviews/common/contextForConfig'; +import { applyDiffLazily } from './extension/ctrlL'; +import { readFileContentOfUri } from './extension/extensionLib/readFileContentOfUri'; // this comes from vscode.proposed.editorInsets.d.ts declare module 'vscode' { @@ -19,13 +24,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 +93,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 +122,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 +142,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 = { 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 +208,3 @@ export function activate(context: vscode.ExtensionContext) { // ) } - diff --git a/extensions/void/src/extension/DiffProvider.ts b/extensions/void/src/extension/DiffProvider.ts new file mode 100644 index 00000000..ca567b4c --- /dev/null +++ b/extensions/void/src/extension/DiffProvider.ts @@ -0,0 +1,560 @@ +import * as vscode from 'vscode'; +import { findDiffs } from './findDiffs'; +import { throttle } from 'lodash'; +import { DiffArea, BaseDiff, Diff } from '../common/shared_types'; +import { readFileContentOfUri } from './extensionLib/readFileContentOfUri'; + + +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 = new vscode.EventEmitter(); // signals a UI refresh on .fire() events + public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event; + + // used internally by vscode + public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult { + 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, 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 = ` + + Hello World! + + `; + + } + + // 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() { + + } + + + +} + + +*/ diff --git a/extensions/void/src/extension/ctrlL.ts b/extensions/void/src/extension/ctrlL.ts new file mode 100644 index 00000000..2837638f --- /dev/null +++ b/extensions/void/src/extension/ctrlL.ts @@ -0,0 +1,185 @@ +import * as vscode from 'vscode'; +import { AbortRef, sendLLMMessage } from '../common/sendLLMMessage'; +import { DiffArea } from '../common/shared_types'; +import { writeFileWithDiffInstructions, searchDiffChunkInstructions } from '../common/systemPrompts'; +import { VoidConfig } from '../webviews/common/contextForConfig'; +import { DiffProvider } from './DiffProvider'; +import { readFileContentOfUri } from './extensionLib/readFileContentOfUri'; + +const LINES_PER_CHUNK = 20 // number of lines to search at a time + + +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((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((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 } diff --git a/extensions/void/src/extension/extension.ts b/extensions/void/src/extension/extension.ts new file mode 100644 index 00000000..c3463365 --- /dev/null +++ b/extensions/void/src/extension/extension.ts @@ -0,0 +1,203 @@ +import * as vscode from 'vscode'; +import { applyDiffLazily } from './ctrlL'; +import { readFileContentOfUri } from './extensionLib/readFileContentOfUri'; +import { MessageToSidebar, MessageFromSidebar, DiffArea, ChatThreads } from '../common/shared_types'; +import { DiffProvider } from './DiffProvider'; +import { getVoidConfig } from '../webviews/common/contextForConfig'; +import { CtrlKWebviewProvider } from './providers/CtrlKWebviewProvider'; +import { SidebarWebviewProvider } from './providers/SidebarWebviewProvider'; +import { v4 as uuidv4 } from 'uuid' +import { AbortRef } from '../common/sendLLMMessage'; + +// this comes from vscode.proposed.editorInsets.d.ts +declare module 'vscode' { + export interface WebviewEditorInset { + readonly editor: vscode.TextEditor; + readonly line: number; + readonly height: number; + readonly webview: vscode.Webview; + readonly onDidDispose: Event; + dispose(): void; + } + export namespace window { + export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset; + } +} + +const roundRangeToLines = (selection: vscode.Selection) => { + return new vscode.Range(selection.start.line, 0, selection.end.line, Number.MAX_SAFE_INTEGER) +} + +export function activate(context: vscode.ExtensionContext) { + + // 1. Mount the chat sidebar + const sidebarWebviewProvider = new SidebarWebviewProvider(context); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider(SidebarWebviewProvider.viewId, sidebarWebviewProvider, { webviewOptions: { retainContextWhenHidden: true } }) + ); + + // 1.5 + const ctrlKWebviewProvider = new CtrlKWebviewProvider(context) + + + // 2. ctrl+l + context.subscriptions.push( + vscode.commands.registerCommand('void.ctrl+l', () => { + const editor = vscode.window.activeTextEditor + if (!editor) return + + // show the sidebar + vscode.commands.executeCommand('workbench.view.extension.voidViewContainer'); + // vscode.commands.executeCommand('vscode.moveViewToPanel', CustomViewProvider.viewId); // move to aux bar + + // get the range of the selection + const selectionRange = roundRangeToLines(editor.selection); + + // get the text the user is selecting + const selectionStr = editor.document.getText(selectionRange); + + // get the file the user is in + const filePath = editor.document.uri; + + // send message to the webview (Sidebar.tsx) + sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, selectionRange, filePath } } satisfies MessageToSidebar)); + }) + ); + + // 2.5: ctrl+k + context.subscriptions.push( + vscode.commands.registerCommand('void.ctrl+k', () => { + console.log('CTRLK PRESSED') + const editor = vscode.window.activeTextEditor + if (!editor) return + + // get the range of the selection + const selectionRange = roundRangeToLines(editor.selection); + + // get the text the user is selecting + const selectionStr = editor.document.getText(selectionRange); + + // get the file the user is in + const filePath = editor.document.uri; + + // send message to the webview (Sidebar.tsx) + ctrlKWebviewProvider.onPressCtrlK() + }) + ); + + // 3. Show an approve/reject codelens above each change + 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) => { + diffProvider.acceptDiff(params) + })); + context.subscriptions.push(vscode.commands.registerCommand('void.rejectDiff', async (params) => { + diffProvider.rejectDiff(params) + })); + + // 5. Receive messages from sidebar + sidebarWebviewProvider.webview.then( + webview => { + + // top navigation bar commands + context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => { + webview.postMessage({ type: 'startNewThread' } satisfies MessageToSidebar) + })) + context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => { + webview.postMessage({ type: 'toggleThreadSelector' } satisfies MessageToSidebar) + })) + context.subscriptions.push(vscode.commands.registerCommand('void.toggleSettings', async () => { + webview.postMessage({ type: 'toggleSettings' } satisfies MessageToSidebar) + })); + + // 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 + const files = await Promise.all( + m.filepaths.map(async (filepath) => ({ filepath, content: await readFileContentOfUri(filepath) })) + ) + + // send contents to webview + webview.postMessage({ type: 'files', files, } satisfies MessageToSidebar) + + } + else if (m.type === 'applyChanges') { + + const editor = vscode.window.activeTextEditor + if (!editor) { + vscode.window.showInformationMessage('No active editor!') + return + } + // create an area to show diffs + const partialDiffArea: Omit = { + startLine: 0, // in ctrl+L the start and end lines are the full document + endLine: editor.document.lineCount, + originalStartLine: 0, + originalEndLine: editor.document.lineCount, + sweepIndex: null, + } + 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') ?? {}) + + await applyDiffLazily({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffProvider, diffArea, abortRef: abortApplyRef }) + } + else if (m.type === 'getPartialVoidConfig') { + const partialVoidConfig = context.globalState.get('partialVoidConfig') ?? {} + webview.postMessage({ type: 'partialVoidConfig', partialVoidConfig } satisfies MessageToSidebar) + } + else if (m.type === 'persistPartialVoidConfig') { + const partialVoidConfig = m.partialVoidConfig + context.globalState.update('partialVoidConfig', partialVoidConfig) + } + else if (m.type === 'getAllThreads') { + const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {} + webview.postMessage({ type: 'allThreads', threads } satisfies MessageToSidebar) + } + else if (m.type === 'persistThread') { + const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {} + const updatedThreads: ChatThreads = { ...threads, [m.thread.id]: m.thread } + context.workspaceState.update('allThreads', updatedThreads) + } + else if (m.type === 'getDeviceId') { + let deviceId = context.globalState.get('void_deviceid') + if (!deviceId || typeof deviceId !== 'string') { + deviceId = uuidv4() + context.globalState.update('void_deviceid', deviceId) + } + webview.postMessage({ type: 'deviceId', deviceId: deviceId as string } satisfies MessageToSidebar) + } + else { + console.error('unrecognized command', m) + } + }) + } + ) + + + + + // Gets called when user presses ctrl + k (mounts ctrl+k-style codelens) + // TODO need to build this + // const ctrlKCodeLensProvider = new CtrlKCodeLensProvider(); + // context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', ctrlKCodeLensProvider)); + // context.subscriptions.push( + // vscode.commands.registerCommand('void.ctrl+k', () => { + // const editor = vscode.window.activeTextEditor; + // if (!editor) + // return + // ctrlKCodeLensProvider.addNewCodeLens(editor.document, editor.selection); + // // vscode.commands.executeCommand('editor.action.showHover'); // apparently this refreshes the codelenses by having the internals call provideCodeLenses + // }) + // ) + +} diff --git a/extensions/void/src/extension/extensionLib/readFileContentOfUri.ts b/extensions/void/src/extension/extensionLib/readFileContentOfUri.ts new file mode 100644 index 00000000..8e7e947e --- /dev/null +++ b/extensions/void/src/extension/extensionLib/readFileContentOfUri.ts @@ -0,0 +1,14 @@ +import * as vscode from 'vscode' + + +export const readFileContentOfUri = async (uri: vscode.Uri): Promise => { + 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 +// } diff --git a/extensions/void/src/extension/extensionLib/updateWebviewHTML.ts b/extensions/void/src/extension/extensionLib/updateWebviewHTML.ts new file mode 100644 index 00000000..1957ac75 --- /dev/null +++ b/extensions/void/src/extension/extensionLib/updateWebviewHTML.ts @@ -0,0 +1,46 @@ +import * as vscode from 'vscode' + +function generateNonce() { + let text = ""; + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + + +// call this when you have access to the webview to set its html +export const updateWebviewHTML = (webview: vscode.Webview, extensionUri: vscode.Uri, { jsOutLocation, cssOutLocation }: { jsOutLocation: string, cssOutLocation: string }, props?: object) => { + + // 'dist/sidebar/index.js' + // 'dist/sidebar/styles.css' + + const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, jsOutLocation)); + const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, cssOutLocation)); + const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri)); + const nonce = generateNonce(); + + const webviewHTML = ` + + + + + Custom View + + + + + +
    + + + `; + + webview.html = webviewHTML + + webview.options = { + enableScripts: true, + localResourceRoots: [extensionUri] + }; +} diff --git a/extensions/void/src/extension/findDiffs.ts b/extensions/void/src/extension/findDiffs.ts new file mode 100644 index 00000000..02d073a7 --- /dev/null +++ b/extensions/void/src/extension/findDiffs.ts @@ -0,0 +1,132 @@ + +import { Range } from 'vscode'; +import { diffLines, Change } from 'diff'; +import { BaseDiff } from '../common/shared_types'; + + + +// class Range { +// range: any; +// constructor(startLine, startCol, endLine, endCol) { +// const range = { +// startLine, +// startCol, +// endLine, +// endCol, +// }; +// this.range = range; +// } +// } + + + +// 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) + + 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: BaseDiff[] = [] + for (let line of lineByLineChanges) { + + // no change on this line + if (!line.added && !line.removed) { + + // do nothing + + // if we were on a streak of +s and -s, end it + if (streakStartInNewFile !== undefined) { + let type: 'edit' | 'insertion' | 'deletion' = 'edit' + + 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 + + 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' + } + + // INSERTION + else if (originalEndLine === originalStartLine - 1) { + type = 'insertion' + originalEndLine = originalStartLine + originalStartCol = 0 + originalEndCol = 0 + } + + const replacement: BaseDiff = { + type, + range: new Range(startLine, startCol, endLine, endCol), + code: newContent, + originalRange: new Range(originalStartLine, originalStartCol, originalEndLine, originalEndCol), + originalCode: 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 +} diff --git a/extensions/void/src/extension/providers/CtrlKWebviewProvider.ts b/extensions/void/src/extension/providers/CtrlKWebviewProvider.ts new file mode 100644 index 00000000..8ef55198 --- /dev/null +++ b/extensions/void/src/extension/providers/CtrlKWebviewProvider.ts @@ -0,0 +1,57 @@ +// renders the code from `src/sidebar` + +import * as vscode from 'vscode'; +import { updateWebviewHTML as _updateWebviewHTML, updateWebviewHTML } from '../extensionLib/updateWebviewHTML'; + +// this comes from vscode.proposed.editorInsets.d.ts +declare module 'vscode' { + export interface WebviewEditorInset { + readonly editor: vscode.TextEditor; + readonly line: number; + readonly height: number; + readonly webview: vscode.Webview; + readonly onDidDispose: Event; + dispose(): void; + } + export namespace window { + export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset; + } +} + + + +export class CtrlKWebviewProvider { + + private readonly _extensionUri: vscode.Uri + + private _idPool = 0 + + + + constructor(context: vscode.ExtensionContext) { + this._extensionUri = context.extensionUri + } + + onPressCtrlK() { + + // // TODO if currently selecting a ctrl k element, just focus it and do nothing + + + // const inset = vscode.window.createWebviewTextEditorInset(editor, line, height); + + + // const newCtrlKId = this._idPool++ + // updateWebviewHTML(inset.webview, this._extensionUri, { jsOutLocation: 'dist/webviews/ctrlk/index.js', cssOutLocation: 'dist/webviews/styles.css' }, + // { id: newCtrlKId } + // ) + + // ctrlKWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+k', selection: { selectionStr, selectionRange, filePath } } satisfies MessageToSidebar)); + + + } + + onDisposeCtrlK() { + + } + +} diff --git a/extensions/void/src/extension/providers/SidebarWebviewProvider.ts b/extensions/void/src/extension/providers/SidebarWebviewProvider.ts new file mode 100644 index 00000000..0ca1d895 --- /dev/null +++ b/extensions/void/src/extension/providers/SidebarWebviewProvider.ts @@ -0,0 +1,30 @@ +// renders the code from `src/sidebar` + +import * as vscode from 'vscode'; +import { updateWebviewHTML as _updateWebviewHTML } from '../extensionLib/updateWebviewHTML'; + +export class SidebarWebviewProvider implements vscode.WebviewViewProvider { + public static readonly viewId = 'void.viewnumberone'; + + public webview: Promise // used to send messages to the webview, resolved by _res in resolveWebviewView + private _res: (c: vscode.Webview) => void // used to resolve the webview + + private readonly _extensionUri: vscode.Uri + + constructor(context: vscode.ExtensionContext) { + // const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later... was included in webviewProvider code + this._extensionUri = context.extensionUri + + let temp_res: typeof this._res | undefined = undefined + this.webview = new Promise((res, rej) => { temp_res = res }) + if (!temp_res) throw new Error("Void sidebar provider: resolver was undefined") + this._res = temp_res + } + + // called internally by vscode + resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken,) { + const webview = webviewView.webview; + _updateWebviewHTML(webview, this._extensionUri, { jsOutLocation: 'dist/webviews/sidebar/index.js', cssOutLocation: 'dist/webviews/styles.css' }) + this._res(webview); // resolve webview and _webviewView + } +} diff --git a/extensions/void/src/findDiffs.ts b/extensions/void/src/findDiffs.ts deleted file mode 100644 index e95b533a..00000000 --- a/extensions/void/src/findDiffs.ts +++ /dev/null @@ -1,277 +0,0 @@ - -import * as vscode from 'vscode'; -// import { diffLines, Change } from 'diff'; -import { BaseDiff } from './common/shared_types'; - -import { diff_match_patch } from 'diff-match-patch'; - - -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; -} - - -// TODO use a better diff algorithm -export const findDiffs = (oldText: string, newText: string): BaseDiff[] => { - - const diffs = diffLines(oldText, newText); - - const blocks: BaseDiff[] = []; - let reprBlock: string[] = []; - let deletedBlock: string[] = []; - let insertedBlock: string[] = []; - let insertedLine = 0; - let deletedLine = 0; - let insertedStart = 0; - let deletedStart = 0; - - diffs.forEach(([operation, text]) => { - - const lines = text.split('\n'); - - switch (operation) { - - // 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; - - // 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; - - // 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), - }); - } - - // Reset the block variables - reprBlock = []; - deletedBlock = []; - insertedBlock = []; - - // Update line counts for unchanged text - insertedLine += lines.length - 1; - deletedLine += lines.length - 1; - - break; - } - }); - - // 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), - }); - } - - return blocks; -}; - - - -// 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 - -// } \ No newline at end of file diff --git a/extensions/void/src/sidebar/index.tsx b/extensions/void/src/sidebar/index.tsx deleted file mode 100644 index 3d1c17d2..00000000 --- a/extensions/void/src/sidebar/index.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import * as React from "react" -import { useEffect } from "react" -import * as ReactDOM from "react-dom/client" -import Sidebar from "./Sidebar" -import { CtrlK } from "./CtrlK" -import { ThreadsProvider } from "./contextForThreads" -import { ConfigProvider } from "./contextForConfig" -import { MessageToSidebar } from "../common/shared_types" -import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode } from "./getVscodeApi" -import { identifyUser, initPosthog } from "./metrics/posthog" - -if (typeof document === "undefined") { - console.log("index.tsx error: document was undefined") -} - - -const CommonEffects = () => { - // initialize posthog - useEffect(() => { - initPosthog() - }, []) - - // when we get the deviceid, identify the user - useEffect(() => { - getVSCodeAPI().postMessage({ type: 'getDeviceId' }); - awaitVSCodeResponse('deviceId').then((m => { - identifyUser(m.deviceId) - })) - }, []) - - // Receive messages from the VSCode extension - useEffect(() => { - const listener = (event: MessageEvent) => { - const m = event.data as MessageToSidebar; - onMessageFromVSCode(m) - } - window.addEventListener('message', listener); - return () => window.removeEventListener('message', listener) - }, []) - - return null -} - -(() => { - // mount the sidebar on the id="root" element - const rootElement = document.getElementById("root")! - console.log("Void root Element:", rootElement) - - const sidebar = (<> - - - - - - - - - - - - - ) - const root = ReactDOM.createRoot(rootElement) - root.render(sidebar) -})(); - diff --git a/extensions/void/src/sidebar/contextForConfig.tsx b/extensions/void/src/webviews/common/contextForConfig.tsx similarity index 98% rename from extensions/void/src/sidebar/contextForConfig.tsx rename to extensions/void/src/webviews/common/contextForConfig.tsx index c933e341..e5b5d6b9 100644 --- a/extensions/void/src/sidebar/contextForConfig.tsx +++ b/extensions/void/src/webviews/common/contextForConfig.tsx @@ -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] = {} diff --git a/extensions/void/src/webviews/common/contextForProps.tsx b/extensions/void/src/webviews/common/contextForProps.tsx new file mode 100644 index 00000000..25f73abf --- /dev/null +++ b/extensions/void/src/webviews/common/contextForProps.tsx @@ -0,0 +1,25 @@ +import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react" + +type PropsType = { [s: string]: any } | null + +type PropsValue = { props: PropsType } + +const PropsContext = createContext(undefined as unknown as PropsValue) + +// provider for whatever came in data-void-props +export function PropsProvider({ children, props }: { children: ReactNode, props: PropsType }) { + return ( + + {children} + + ) +} + +export function useVoidProps(): PropsValue { + const context = useContext(PropsContext) + if (context === undefined) { + throw new Error("useVoidProps missing Provider") + } + return context +} + diff --git a/extensions/void/src/sidebar/contextForThreads.tsx b/extensions/void/src/webviews/common/contextForThreads.tsx similarity index 77% rename from extensions/void/src/sidebar/contextForThreads.tsx rename to extensions/void/src/webviews/common/contextForThreads.tsx index b8b1bfe9..d2ab97be 100644 --- a/extensions/void/src/sidebar/contextForThreads.tsx +++ b/extensions/void/src/webviews/common/contextForThreads.tsx @@ -1,12 +1,12 @@ import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react" -import { ChatMessage, ChatThreads } from "../common/shared_types" +import { ChatMessage, ChatThreads } from "../../common/shared_types" 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 = (initVal: T) => { export function ThreadsProvider({ children }: { children: ReactNode }) { - const [allThreads, setAllThreads] = useInstantState({}) - const [currentThreadId, setCurrentThreadId] = useInstantState(null) + const [allThreadsRef, setAllThreads] = useInstantState({}) + const [currentThreadIdRef, setCurrentThreadId] = useInstantState(null) // this loads allThreads in on mount useEffect(() => { @@ -55,12 +55,12 @@ export function ThreadsProvider({ children }: { children: ReactNode }) { return ( 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) diff --git a/extensions/void/src/sidebar/getVscodeApi.ts b/extensions/void/src/webviews/common/getVscodeApi.ts similarity index 96% rename from extensions/void/src/sidebar/getVscodeApi.ts rename to extensions/void/src/webviews/common/getVscodeApi.ts index c24ce3ea..cf9d53ea 100644 --- a/extensions/void/src/sidebar/getVscodeApi.ts +++ b/extensions/void/src/webviews/common/getVscodeApi.ts @@ -1,5 +1,5 @@ import { useEffect } from "react"; -import { MessageFromSidebar, MessageToSidebar, } from "../common/shared_types"; +import { MessageFromSidebar, MessageToSidebar, } from "../../common/shared_types"; import { v4 as uuidv4 } from 'uuid'; diff --git a/extensions/void/src/webviews/common/mount.tsx b/extensions/void/src/webviews/common/mount.tsx new file mode 100644 index 00000000..70ef66f6 --- /dev/null +++ b/extensions/void/src/webviews/common/mount.tsx @@ -0,0 +1,72 @@ +import React, { useEffect } from "react"; +import * as ReactDOM from "react-dom/client" +import { MessageToSidebar } from "../../common/shared_types"; +import { getVSCodeAPI, awaitVSCodeResponse, onMessageFromVSCode } from "./getVscodeApi"; +import { initPosthog, identifyUser } from "./posthog"; +import { ThreadsProvider } from "./contextForThreads"; +import { ConfigProvider } from "./contextForConfig"; +import { PropsProvider } from "./contextForProps"; + +const ListenersAndTracking = () => { + // initialize posthog + useEffect(() => { + initPosthog() + }, []) + + // when we get the deviceid, identify the user + useEffect(() => { + getVSCodeAPI().postMessage({ type: 'getDeviceId' }); + awaitVSCodeResponse('deviceId').then((m => { + identifyUser(m.deviceId) + })) + }, []) + + // Receive messages from the VSCode extension + useEffect(() => { + const listener = (event: MessageEvent) => { + const m = event.data as MessageToSidebar; + onMessageFromVSCode(m) + } + window.addEventListener('message', listener); + return () => window.removeEventListener('message', listener) + }, []) + + return null +} + + + + +export const mount = (children: React.ReactNode) => { + + if (typeof document === "undefined") { + console.error("index.tsx error: document was undefined") + return + } + + // mount the sidebar on the id="root" element + const rootElement = document.getElementById("root")! + console.log("Void root Element:", rootElement) + + let props = rootElement.getAttribute("data-void-props") + let propsObj: object | null = null + if (props !== null) { + propsObj = JSON.parse(decodeURIComponent(props)) + } + + const content = (<> + + + + + + {children} + + + + ) + + const root = ReactDOM.createRoot(rootElement) + root.render(content); + +} \ No newline at end of file diff --git a/extensions/void/src/sidebar/metrics/posthog.tsx b/extensions/void/src/webviews/common/posthog.tsx similarity index 100% rename from extensions/void/src/sidebar/metrics/posthog.tsx rename to extensions/void/src/webviews/common/posthog.tsx diff --git a/extensions/void/src/sidebar/CtrlK.tsx b/extensions/void/src/webviews/ctrlk/CtrlK.tsx similarity index 80% rename from extensions/void/src/sidebar/CtrlK.tsx rename to extensions/void/src/webviews/ctrlk/CtrlK.tsx index 89797e3e..09250fab 100644 --- a/extensions/void/src/sidebar/CtrlK.tsx +++ b/extensions/void/src/webviews/ctrlk/CtrlK.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { useOnVSCodeMessage } from './getVscodeApi'; +import { useOnVSCodeMessage } from '../common/getVscodeApi'; export const CtrlK = () => { diff --git a/extensions/void/src/webviews/ctrlk/index.tsx b/extensions/void/src/webviews/ctrlk/index.tsx new file mode 100644 index 00000000..9141b713 --- /dev/null +++ b/extensions/void/src/webviews/ctrlk/index.tsx @@ -0,0 +1,7 @@ +import React from "react" +import { mount } from "../common/mount" +import { CtrlK } from "./CtrlK" + +// this is the entry point that mounts ctrlk +mount() + diff --git a/extensions/void/src/sidebar/Sidebar.tsx b/extensions/void/src/webviews/sidebar/Sidebar.tsx similarity index 90% rename from extensions/void/src/sidebar/Sidebar.tsx rename to extensions/void/src/webviews/sidebar/Sidebar.tsx index e0ed5906..70bf1c3b 100644 --- a/extensions/void/src/sidebar/Sidebar.tsx +++ b/extensions/void/src/webviews/sidebar/Sidebar.tsx @@ -1,11 +1,11 @@ import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react" -import { CodeSelection, ChatMessage, MessageToSidebar } from "../common/shared_types" -import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi" +import { CodeSelection, ChatMessage, MessageToSidebar } from "../../common/shared_types" +import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "../common/getVscodeApi" import { SidebarThreadSelector } from "./SidebarThreadSelector"; import { SidebarChat } from "./SidebarChat"; -import { SidebarSettings } from './SidebarSettings'; -import { identifyUser } from "./metrics/posthog"; +import { SidebarSettings } from "./SidebarSettings"; +import { identifyUser } from "../common/posthog"; const Sidebar = () => { @@ -60,7 +60,6 @@ const Sidebar = () => {
- } diff --git a/extensions/void/src/sidebar/SidebarChat.tsx b/extensions/void/src/webviews/sidebar/SidebarChat.tsx similarity index 90% rename from extensions/void/src/sidebar/SidebarChat.tsx rename to extensions/void/src/webviews/sidebar/SidebarChat.tsx index b7280b33..038fedf7 100644 --- a/extensions/void/src/sidebar/SidebarChat.tsx +++ b/extensions/void/src/webviews/sidebar/SidebarChat.tsx @@ -4,13 +4,14 @@ import React, { FormEvent, useCallback, useEffect, useRef, useState } from "reac import { marked } from 'marked'; import MarkdownRender from "./markdown/MarkdownRender"; import BlockCode from "./markdown/BlockCode"; -import { File, ChatMessage, CodeSelection } from "../common/shared_types"; +import { File, ChatMessage, CodeSelection } from "../../common/shared_types"; import * as vscode from 'vscode' -import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi"; -import { useThreads } from "./contextForThreads"; -import { sendLLMMessage } from "../common/sendLLMMessage"; -import { useVoidConfig } from "./contextForConfig"; -import { captureEvent } from "./metrics/posthog"; +import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "../common/getVscodeApi"; +import { useThreads } from "../common/contextForThreads"; +import { sendLLMMessage } from "../../common/sendLLMMessage"; +import { useVoidConfig } from "../common/contextForConfig"; +import { captureEvent } from "../common/posthog"; +import { generateDiffInstructions } from "../../common/systemPrompts"; @@ -160,7 +161,8 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject { 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 ({ 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
{/* previous messages */} - {currentThread !== null && currentThread.messages.map((message, i) => + {getCurrentThread() !== null && getCurrentThread()?.messages.map((message, i) => )} {/* message stream */} @@ -296,7 +301,6 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject {/* selection */}
-
{/* selection */} diff --git a/extensions/void/src/sidebar/SidebarSettings.tsx b/extensions/void/src/webviews/sidebar/SidebarSettings.tsx similarity index 94% rename from extensions/void/src/sidebar/SidebarSettings.tsx rename to extensions/void/src/webviews/sidebar/SidebarSettings.tsx index 209b1232..f144fd11 100644 --- a/extensions/void/src/sidebar/SidebarSettings.tsx +++ b/extensions/void/src/webviews/sidebar/SidebarSettings.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { configFields, useVoidConfig, VoidConfigField } from "./contextForConfig"; +import { configFields, useVoidConfig, VoidConfigField } from "../common/contextForConfig"; const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, param: string }) => { @@ -69,6 +69,10 @@ export const SidebarSettings = () => { field='default' param='whichApi' /> +

diff --git a/extensions/void/src/sidebar/SidebarThreadSelector.tsx b/extensions/void/src/webviews/sidebar/SidebarThreadSelector.tsx similarity index 85% rename from extensions/void/src/sidebar/SidebarThreadSelector.tsx rename to extensions/void/src/webviews/sidebar/SidebarThreadSelector.tsx index 5f88eac8..3d785d1b 100644 --- a/extensions/void/src/sidebar/SidebarThreadSelector.tsx +++ b/extensions/void/src/webviews/sidebar/SidebarThreadSelector.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { ThreadsProvider, useThreads } from "./contextForThreads"; +import { ThreadsProvider, useThreads } from "../common/contextForThreads"; const truncate = (s: string) => { @@ -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 (