Compare commits

..

24 Commits

Author SHA1 Message Date
Andrew df646b34e2 amend 2024-10-31 20:31:45 -07:00
Andrew 5e41a555dc add inset-update test 2024-10-31 20:31:07 -07:00
mp 4a9962a7fc add native updateEditorInset() function 2024-10-31 19:24:44 -07:00
mp dd48db0ed3 minor changes 2024-10-31 19:23:17 -07:00
mp d7c00addd5 update inline diffs 2024-10-31 17:10:39 -07:00
Andrew Pareles b34119c4df Merge pull request #134 from Piyu-Pika/Gemini
Gemini added #new feature
2024-10-30 23:19:52 -07:00
Andrew Pareles d098291c90 minor note 2024-10-30 23:19:34 -07:00
Andrew ed9e126a4a remove duplicate extension.ts 2024-10-30 23:16:36 -07:00
Andrew a8010eec15 simplify gemini 2024-10-30 22:46:47 -07:00
Piyu-Pika 8cd1ea305c Gemini 2024-10-31 10:41:04 +05:30
Piyush Bhardwaj 762264b030 Merge branch 'main' into Gemini 2024-10-31 10:28:17 +05:30
Andrew Pareles 48d5369196 amend 2024-10-30 19:09:43 -07:00
Andrew Pareles aab840db2d amend 2024-10-30 19:02:48 -07:00
Andrew Pareles 3526005e1c amend 2024-10-30 19:02:22 -07:00
Andrew Pareles 0ef8c66d54 simplify readme 2024-10-30 19:01:56 -07:00
Andrew cd9173c802 move ctrlk 2024-10-30 18:59:17 -07:00
Piyu-Pika 0c8a185df7 Gemini added 2024-10-29 18:05:07 +05:30
Andrew 0f0719c596 fix type 2024-10-29 00:26:46 -07:00
Andrew d9397d870f change CodeSelection type 2024-10-28 17:17:26 -07:00
Andrew 1704a7de7a improve line rounding 2024-10-28 16:34:19 -07:00
Andrew 616cd2e39b update back to enum 2024-10-28 15:58:48 -07:00
Andrew Pareles 222f90bbea Merge pull request #133 from voideditor/multiple-webviews
Max tokens is optional + other config updates - Fixes #128
2024-10-28 15:50:40 -07:00
Andrew Pareles a7a4488848 minor Contributing changes 2024-10-28 04:22:19 -07:00
Andrew Pareles 8e72c1392a Merge pull request #125 from voideditor/multiple-webviews
Speculative Edits
2024-10-28 03:59:53 -07:00
25 changed files with 299 additions and 334 deletions
+6 -7
View File
@@ -71,7 +71,7 @@ We haven't created prerequisite steps for building on Linux yet, but you can fol
Before building Void, please follow the prerequisite steps above for your operating system. Also make sure you've already built the Void extension (or just run `cd ./extensions/void && npm install && npm run build && npm run compile && cd ../..`).
First, open `void/` in VSCode. Then:
To build Void, first open `void/` in VSCode. Then:
1. Install all dependencies.
@@ -97,8 +97,7 @@ If you ran `npm run watch`, the build is done when you see something like this:
<!-- 3. Press <kbd>Ctrl+Shift+B</kbd> to start the build process. -->
1. In a new terminal, run `./scripts/code.sh` (Mac/Linux) or `/.scripts/code.bat` (Windows). This should open up the built IDE!
To see new changes without restarting the build, press <kbd>Ctrl+Shift+P</kbd> and run "Reload Window" inside the new window.
You can always press <kbd>Ctrl+Shift+P</kbd> and run "Reload Window" inside the new window to see changes without re-building.
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page!
@@ -119,9 +118,7 @@ Here are the full options: `vscode-{win32-ia32 | win32-x64 | darwin-x64 | darwin
## Roadmap
Here are the most important topics on our Roadmap. More ⭐'s = more important.
These sometimes get outdated - please refer to our [Issues](https://github.com/voideditor/void/issues) page for the latest issues.
Here are the most important topics on our Roadmap. More ⭐'s = more important. Please refer to our [Issues](https://github.com/voideditor/void/issues) page for the latest issues.
## ⭐⭐⭐ Make History work well.
@@ -149,7 +146,9 @@ We're always glad to talk about new ideas, help you get set up, and make sure yo
## Submitting a Pull Request
Please submit a pull request once you've made a change. You don't need to submit an issue.
Please submit a pull request once you've made a change. You don't need to submit an issue.
Please don't use AI to write your PR 🙂.
# Relevant files
+4 -10
View File
@@ -1,22 +1,16 @@
# Welcome to Void.
Void is the open-source Cursor alternative. This repo contains the full sourcecode for Void. We have a [waitlist](https://voideditor.com/email) for downloading the official release, but you can build and develop Void right now.
Void is the open-source Cursor alternative.
If you're new, welcome! Feel free to check out our [Project board](https://github.com/orgs/voideditor/projects/2/views/3) for the most pressing Issues to work on, and see [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md) for instructions on building and running Void.
This repo contains the full sourcecode for Void. We have a [waitlist](https://voideditor.com/email) for downloading the official release, but you can build and develop Void right now.
If you're new, welcome!
## Contributing
To build and run Void, see [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
To build and run Void, follow the steps in [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
## Reference
Void is a fork of the of [vscode](https://github.com/microsoft/vscode) repository.
For some useful links we've compiled see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
Void is a fork of the of [vscode](https://github.com/microsoft/vscode) repository. For some useful links on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
## Support
Feel free to reach out in our [Discord](https://discord.gg/RSNjgaugJs) or contact us via email.
+3 -2
View File
@@ -10,7 +10,8 @@
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
"--extensionDevelopmentPath=${workspaceFolder}",
"--enable-proposed-api=void.void",
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
@@ -18,4 +19,4 @@
"preLaunchTask": "${defaultBuildTask}"
}
]
}
}
+5
View File
@@ -49,6 +49,11 @@ const convertTSXtoJS = async ({ from, to }) => {
to: 'dist/webviews/ctrlk/index.js',
})
await convertTSXtoJS({
from: 'src/webviews/diffline/index.tsx',
to: 'dist/webviews/diffline/index.js',
})
// convert tailwind to css
await convertTailwindToCSS({
from: 'src/webviews/styles.css',
+11
View File
@@ -10,6 +10,7 @@
"devDependencies": {
"@anthropic-ai/sdk": "^0.29.2",
"@eslint/js": "^9.9.1",
"@google/generative-ai": "^0.21.0",
"@monaco-editor/react": "^4.6.0",
"@rrweb/types": "^2.0.0-alpha.17",
"@types/diff": "^5.2.2",
@@ -737,6 +738,16 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@google/generative-ai": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz",
"integrity": "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+4
View File
@@ -10,6 +10,9 @@
"categories": [
"Other"
],
"enabledApiProposals": [
"editorInsets"
],
"activationEvents": [],
"main": "./out/extension/extension.js",
"contributes": {
@@ -111,6 +114,7 @@
"devDependencies": {
"@anthropic-ai/sdk": "^0.29.2",
"@eslint/js": "^9.9.1",
"@google/generative-ai": "^0.21.0",
"@monaco-editor/react": "^4.6.0",
"@rrweb/types": "^2.0.0-alpha.17",
"@types/diff": "^5.2.2",
+60 -11
View File
@@ -1,6 +1,7 @@
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { Ollama } from 'ollama/browser'
import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai';
import { VoidConfig } from '../webviews/common/contextForConfig'
export type AbortRef = { current: (() => void) | null }
@@ -37,8 +38,6 @@ type SendLLMMessageFnTypeExternal = (params: {
abortRef: AbortRef,
}) => void
const parseMaxTokensStr = (maxTokensStr: string) => {
// parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
let int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
@@ -65,7 +64,7 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi
system: systemMessage,
messages: anthropicMessages,
model: voidConfig.anthropic.model,
max_tokens: parseInt(voidConfig.default.maxTokens),
max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user
});
let did_abort = false
@@ -103,8 +102,62 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi
return { abort }
};
// Gemini
const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let didAbort = false
let fullText = ''
abortRef.current = () => {
didAbort = true
}
const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey);
const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model });
// remove system messages that get sent to Gemini
// str of all system messages
let systemMessage = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n');
// Convert messages to Gemini format
const geminiMessages: Content[] = messages
.filter(msg => msg.role !== 'system')
.map((msg, i) => ({
parts: [{ text: msg.content }],
role: msg.role === 'assistant' ? 'model' : 'user'
}))
model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
.then(async response => {
abortRef.current = () => {
// response.stream.return(fullText)
didAbort = true;
}
for await (const chunk of response.stream) {
if (didAbort) return;
const newText = chunk.text();
fullText += newText;
onText(newText, fullText);
}
onFinalMessage(fullText);
})
.catch((error) => {
if (error instanceof GoogleGenerativeAIFetchError) {
if (error.status === 400) {
onError('Invalid API key.');
}
else {
onError(`${error.name}:\n${error.message}`);
}
}
else {
onError(error);
}
})
}
// OpenAI, OpenRouter, OpenAICompatible
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
@@ -168,7 +221,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
onError('Invalid API key.');
}
else {
onError(error.message);
onError(`${error.name}:\n${error.message}`);
}
}
else {
@@ -178,7 +231,6 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
};
// Ollama
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
@@ -196,7 +248,7 @@ 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
options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens
})
.then(async stream => {
abortRef.current = () => {
@@ -220,8 +272,6 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
};
// Greptile
// https://docs.greptile.com/api-reference/query
// https://docs.greptile.com/quickstart#sample-response-streamed
@@ -236,7 +286,6 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
didAbort = true
}
fetch('https://api.greptile.com/v2/query', {
method: 'POST',
headers: {
@@ -291,8 +340,6 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
}
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
if (!voidConfig) return;
@@ -306,6 +353,8 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText,
case 'openRouter':
case 'openAICompatible':
return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'gemini':
return sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'ollama':
return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'greptile':
+2 -5
View File
@@ -2,10 +2,7 @@
import * as vscode from 'vscode';
import { PartialVoidConfig } from '../webviews/common/contextForConfig'
// a selection is a frozen snapshot
type CodeSelection = { selectionStr: string, selectionRange: vscode.Range, filePath: vscode.Uri }
type CodeSelection = { selectionStr: string, filePath: vscode.Uri }
type File = { filepath: vscode.Uri, content: string }
@@ -37,7 +34,7 @@ type Diff = {
// editor -> sidebar
type MessageToSidebar = (
| { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor
| { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor. selection and path are frozen snapshots
| { type: 'ctrl+k', selection: CodeSelection }
| { type: 'files', files: { filepath: vscode.Uri, content: string }[] }
| { type: 'partialVoidConfig', partialVoidConfig: PartialVoidConfig }
-210
View File
@@ -1,210 +0,0 @@
import * as vscode from 'vscode';
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 { getVoidConfigFromPartial } 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' {
export interface WebviewEditorInset {
readonly editor: vscode.TextEditor;
readonly line: number;
readonly height: number;
readonly webview: vscode.Webview;
readonly onDidDispose: Event<void>;
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 } })
);
// 2. ctrl+l
context.subscriptions.push(
vscode.commands.registerCommand('void.ctrl+l', () => {
const editor = vscode.window.activeTextEditor
if (!editor) return
// const inset = vscode.window.createWebviewTextEditorInset(editor, 10, 10, {})
// inset.webview.html = `
// <html>
// <body style="pointer-events:none;">Hello World!</body>
// </html>
// `;
// 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)
sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+k', selection: { selectionStr, selectionRange, filePath } } satisfies MessageToSidebar));
})
);
// 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<DiffArea, 'diffareaid'> = {
startLine: 0, // in ctrl+L the start and end lines are the full document
endLine: editor.document.lineCount,
originalStartLine: 0,
originalEndLine: editor.document.lineCount,
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 = getVoidConfigFromPartial(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
// })
// )
}
+34 -4
View File
@@ -3,6 +3,7 @@ import { findDiffs } from './findDiffs';
import { throttle } from 'lodash';
import { DiffArea, BaseDiff, Diff } from '../common/shared_types';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
import { updateWebviewHTML } from './extensionLib/updateWebviewHTML';
const THROTTLE_TIME = 100
@@ -27,10 +28,14 @@ export class DiffProvider implements vscode.CodeLensProvider {
private _originalFileOfDocument: { [docUriStr: string]: string } = {}
private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {}
private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {}
private _insetsOfDocument: { [docUriStr: string]: vscode.WebviewEditorInset[] } = {}
private _diffareaidPool = 0
private _diffidPool = 0
private _extensionUri: vscode.Uri
// used internally by vscode
private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
@@ -42,7 +47,8 @@ export class DiffProvider implements vscode.CodeLensProvider {
}
// declared by us, registered with vscode.languages.registerCodeLensProvider()
constructor() {
constructor(context: vscode.ExtensionContext) {
this._extensionUri = context.extensionUri
console.log('Creating DisplayChangesProvider')
@@ -172,6 +178,10 @@ export class DiffProvider implements vscode.CodeLensProvider {
// reset all diffs (we update them below)
this._diffsOfDocument[docUriStr] = []
// reset all insets (we update them below)
this._insetsOfDocument[docUriStr]?.forEach(inset => inset.dispose())
this._insetsOfDocument[docUriStr] = []
// for each diffArea
for (const diffArea of diffAreas) {
@@ -210,6 +220,28 @@ export class DiffProvider implements vscode.CodeLensProvider {
)
);
// update red highlighting
this._insetsOfDocument[docUriStr] = this._diffsOfDocument[docUriStr]
.filter(diff => diff.originalCode !== '')
.map(diff => {
// create new inset
const text = originalFile.split('\n').slice(diff.originalRange.start.line, diff.originalRange.end.line + 1).join('\n')
const height = text.split('\n').length
const line = diff.range.start.line - 1
const inset = vscode.window.createWebviewTextEditorInset(editor, line, height);
updateWebviewHTML(inset.webview, this._extensionUri, { jsOutLocation: 'dist/webviews/diffline/index.js', cssOutLocation: 'dist/webviews/styles.css' },
{ text }
)
setTimeout(() => {
console.log('updating!')
vscode.window.updateWebviewTextEditorInset(inset.handle, line + 2, height)
}, 2000)
return inset
})
// for each diffArea, highlight its sweepIndex in dark gray
editor.setDecorations(
@@ -234,8 +266,6 @@ export class DiffProvider implements vscode.CodeLensProvider {
)
)
// TODO update red highlighting
// this._diffsOfDocument[docUriStr].map(diff => diff.deletedCode)
// update code lenses
this._onDidChangeCodeLenses.fire()
@@ -392,7 +422,7 @@ export class DiffProvider implements vscode.CodeLensProvider {
// 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
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
@@ -134,7 +134,7 @@ Return \`true\` if ANY part of the chunk should be modified, and \`false\` if it
// 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 }) => {
export 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
@@ -179,7 +179,3 @@ const applyDiffLazily = async ({ docUri, oldFileStr, voidConfig, abortRef, diffR
}
export { applyDiffLazily }
@@ -1,9 +1,9 @@
import * as vscode from 'vscode';
import { AbortRef, OnFinalMessage, OnText, sendLLMMessage } from "./sendLLMMessage"
import { AbortRef, OnFinalMessage, OnText, sendLLMMessage } from "../common/sendLLMMessage"
import { VoidConfig } from '../webviews/common/contextForConfig';
import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from './systemPrompts';
import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from '../common/systemPrompts';
import { throttle } from 'lodash';
import { readFileContentOfUri } from '../extension/extensionLib/readFileContentOfUri';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
type Res<T> = ((value: T) => void)
+30 -27
View File
@@ -1,31 +1,47 @@
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 { getVoidConfigFromPartial } 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';
import { MessageToSidebar, MessageFromSidebar, DiffArea, ChatThreads } from '../common/shared_types';
import { getVoidConfigFromPartial } from '../webviews/common/contextForConfig';
import { applyDiffLazily } from './applyDiffLazily';
import { DiffProvider } from './DiffProvider';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
import { SidebarWebviewProvider } from './providers/SidebarWebviewProvider';
import { CtrlKWebviewProvider } from './providers/CtrlKWebviewProvider';
// 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 handle: number;
readonly onDidDispose: Event<void>;
dispose(): void;
}
export namespace window {
export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset;
export function updateWebviewTextEditorInset(handle: number, line: number, height: number): WebviewEditorInset;
}
}
const roundRangeToLines = (selection: vscode.Selection) => {
return new vscode.Range(selection.start.line, 0, selection.end.line, Number.MAX_SAFE_INTEGER)
let endLine = selection.end.character === 0 ? selection.end.line - 1 : selection.end.line // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1
return new vscode.Range(selection.start.line, 0, endLine, Number.MAX_SAFE_INTEGER)
}
const getSelection = (editor: vscode.TextEditor) => {
// get the range of the selection and the file the user is in
const selectionRange = roundRangeToLines(editor.selection);
const selectionStr = editor.document.getText(selectionRange).trim();
const filePath = editor.document.uri;
return { selectionStr, filePath }
}
export function activate(context: vscode.ExtensionContext) {
@@ -50,17 +66,10 @@ export function activate(context: vscode.ExtensionContext) {
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;
const { selectionStr, filePath } = getSelection(editor)
// send message to the webview (Sidebar.tsx)
sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, selectionRange, filePath } } satisfies MessageToSidebar));
sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, filePath } } satisfies MessageToSidebar));
})
);
@@ -71,22 +80,16 @@ export function activate(context: vscode.ExtensionContext) {
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;
const { selectionStr, filePath } = getSelection(editor)
// send message to the webview (Sidebar.tsx)
ctrlKWebviewProvider.onPressCtrlK()
// ctrlKWebviewProvider.onPressCtrlK()
// sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+k', selection: { selectionStr, filePath } } satisfies MessageToSidebar));
})
);
// 3. Show an approve/reject codelens above each change
const diffProvider = new DiffProvider();
const diffProvider = new DiffProvider(context);
context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', diffProvider));
// 4. Add approve/reject commands
@@ -1,14 +1,13 @@
import * as vscode from 'vscode'
export const readFileContentOfUri = async (uri: vscode.Uri): Promise<string> => {
const document = await vscode.workspace.openTextDocument(uri);
return document.getText().replace(/\r\n/g, '\n') ?? '' // Normalize line endings
// export const readFileContentOfUri = async (uri: vscode.Uri): Promise<string> => {
// const document = await vscode.workspace.openTextDocument(uri.fsPath);
// 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
// }
// TODO this only accesses the most recently saved version; make it instead access the most recent version in the vscode editor
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
}
@@ -32,7 +32,7 @@ export const updateWebviewHTML = (webview: vscode.Webview, extensionUri: vscode.
<link href="${stylesUri}" rel="stylesheet">
</head>
<body>
<div id="root" ${props ? `data-void-props="${encodeURIComponent(JSON.stringify(props))}"` : ''}></div>
<div id="root"${props ? ` data-void-props="${encodeURIComponent(JSON.stringify(props))}"` : ''}></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
@@ -3,22 +3,6 @@
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<void>;
dispose(): void;
}
export namespace window {
export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset;
}
}
export class CtrlKWebviewProvider {
@@ -21,11 +21,12 @@ const configString = (description: string, defaultVal: string) => {
export const configFields = [
'anthropic',
'openAI',
'gemini',
'greptile',
'ollama',
'openRouter',
'openAICompatible',
'azure'
'azure',
] as const
@@ -46,8 +47,16 @@ const voidConfigInfo: Record<
configFields,
),
maxTokens: configString(
"Max number of tokens to output. Must be a number.", ''
maxTokens: configEnum(
"Max number of tokens to output.",
'1024',
[
"default", // this will be parseInt'd into NaN and ignored by the API. Anything that's not a number has this behavior.
"1024",
"2048",
"4096",
"8192"
] as const,
),
},
@@ -156,6 +165,19 @@ const voidConfigInfo: Record<
// }
// },
},
gemini: {
apikey: configString('Google API key.', ''),
model: configEnum(
'Gemini model to use.',
'gemini-1.5-flash',
[
"gemini-1.5-flash",
"gemini-1.5-pro",
"gemini-1.5-flash-8b",
"gemini-1.0-pro"
] as const
),
},
}
@@ -265,4 +287,3 @@ export function useVoidConfig(): ConfigValueType {
}
return context
}
@@ -1,22 +1,33 @@
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
type PropsType = { [s: string]: any } | null
type PropsValue = { props: PropsType }
const PropsContext = createContext<PropsValue>(undefined as unknown as PropsValue)
const PropsContext = createContext<any>(undefined as unknown as any)
// provider for whatever came in data-void-props
export function PropsProvider({ children, props }: { children: ReactNode, props: PropsType }) {
export function PropsProvider({ children, rootElement }: { children: ReactNode, rootElement: HTMLElement }) {
const [props, setProps] = useState<object | null>(null)
// update props when rootElement changes
useEffect(() => {
let props = rootElement.getAttribute("data-void-props")
let propsObj: object | null = null
if (props !== null) {
propsObj = JSON.parse(decodeURIComponent(props))
}
setProps(propsObj)
}, [rootElement])
return (
<PropsContext.Provider value={{ props }}>
<PropsContext.Provider value={props}>
{children}
</PropsContext.Provider>
)
}
export function useVoidProps(): PropsValue {
const context = useContext<PropsValue>(PropsContext)
export function useVoidProps<T extends {}>(): T | null {
// context is the "value" from above
const context: T | null | undefined = useContext<T>(PropsContext)
// only undefined if has no provider
if (context === undefined) {
throw new Error("useVoidProps missing Provider")
}
@@ -46,18 +46,12 @@ export const mount = (children: React.ReactNode) => {
// 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))
}
// console.log("Void root Element:", rootElement)
const content = (<>
<ListenersAndTracking />
<PropsProvider props={propsObj}>
<PropsProvider rootElement={rootElement}>
<ThreadsProvider>
<ConfigProvider>
{children}
@@ -69,4 +63,4 @@ export const mount = (children: React.ReactNode) => {
const root = ReactDOM.createRoot(rootElement)
root.render(content);
}
}
@@ -11,6 +11,13 @@ export const CtrlK = () => {
sx('Pressed ctrl+k')
})
// const inset = vscode.window.createWebviewTextEditorInset(editor, 10, 10, {})
// inset.webview.html = `
// <html>
// <body style="pointer-events:none;">Hello World!</body>
// </html>
// `;
return <>
<div>
{x}
@@ -0,0 +1,29 @@
import React, { useState } from 'react';
import { useOnVSCodeMessage } from '../common/getVscodeApi';
import { useVoidProps } from '../common/contextForProps';
type props = {
text: string
}
export const DiffLine = () => {
const props = useVoidProps<props>()
console.log('props!', props)
if (!props) {
return null
}
// eslint-disable-next-line react/prop-types
const text = props.text
return <>
<pre>
{text}
</pre>
</>
};
@@ -0,0 +1,7 @@
import React from "react"
import { mount } from "../common/mount"
import { DiffLine } from "./DiffLine"
// this is the entry point that mounts diffline
mount(<DiffLine />)
@@ -826,6 +826,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
// checkProposedApiEnabled(extension, 'editorInsets'); // Void commented this out
return extHostEditorInsets.createWebviewEditorInset(editor, line, height, options, extension);
},
// Void created this function:
updateWebviewTextEditorInset(handle: number, line: number, height: number): vscode.WebviewEditorInset {
return extHostEditorInsets.updateEditorInset(handle, line, height);
},
createTerminal(nameOrOptions?: vscode.TerminalOptions | vscode.ExtensionTerminalOptions | string, shellPath?: string, shellArgs?: readonly string[] | string): vscode.Terminal {
if (typeof nameOrOptions === 'object') {
if ('pty' in nameOrOptions) {
@@ -99,11 +99,11 @@ export class ExtHostEditorInsets implements ExtHostEditorInsetsShape {
};
const inset = new class implements vscode.WebviewEditorInset {
readonly editor: vscode.TextEditor = editor;
readonly line: number = line;
readonly height: number = height;
readonly webview: vscode.Webview = webview;
readonly handle: number = handle;
readonly onDidDispose: vscode.Event<void> = onDidDispose.event;
dispose(): void {
@@ -125,6 +125,34 @@ export class ExtHostEditorInsets implements ExtHostEditorInsetsShape {
return inset;
}
updateEditorInset(handle: number, line: number, height: number): vscode.WebviewEditorInset {
const _inset = this._insets.get(handle);
if (!_inset) {
throw new Error(`Inset with handle ${handle} not found`);
}
const { editor, onDidReceiveMessage, inset: { webview, onDidDispose, dispose } } = _inset
const inset = new class implements vscode.WebviewEditorInset {
readonly editor: vscode.TextEditor = editor;
readonly line: number = line;
readonly height: number = height;
readonly webview: vscode.Webview = webview;
readonly handle: number = handle;
readonly onDidDispose: vscode.Event<void> = onDidDispose;
dispose = dispose
};
this._insets.set(handle, { editor, inset: inset, onDidReceiveMessage });
// _inset?.onDidReceiveMessage.fire({});
return inset
}
$onDidDispose(handle: number): void {
const value = this._insets.get(handle);
if (value) {
+2
View File
@@ -12,11 +12,13 @@ declare module 'vscode' {
readonly line: number;
readonly height: number;
readonly webview: Webview;
readonly handle: number;
readonly onDidDispose: Event<void>;
dispose(): void;
}
export namespace window {
export function createWebviewTextEditorInset(editor: TextEditor, line: number, height: number, options?: WebviewOptions): WebviewEditorInset;
export function updateWebviewTextEditorInset(handle: number, line: number, height: number): WebviewEditorInset;
}
}