Merge branch 'main' into actual-editor-insets

This commit is contained in:
Andrew Pareles 2024-11-20 21:59:34 -08:00
commit 8508c2dd8e
13 changed files with 1111 additions and 52 deletions

48
.idx/dev.nix Normal file
View file

@ -0,0 +1,48 @@
# Created for Void
# To learn more about how to use Nix to configure your environment
# see: https://developers.google.com/idx/guides/customize-idx-env
{pkgs}: {
# Which nixpkgs channel to use.
channel = "stable-23.11"; # or "unstable"
# Use https://search.nixos.org/packages to find packages
packages = [
pkgs.nodejs_20
pkgs.yarn
pkgs.nodePackages.pnpm
pkgs.bun
pkgs.gh
];
# Sets environment variables in the workspace
env = {};
idx = {
# Search for the extensions you want on https://open-vsx.org/ and use "publisher.id"
extensions = [
# "vscodevim.vim"
];
workspace = {
# Runs when a workspace is first created with this `dev.nix` file
onCreate = {
npm-install = "npm ci --no-audit --prefer-offline --no-progress --timing";
# Open editors for the following files by default, if they exist:
default.openFiles = [
# Cover all the variations of language, src-dir, router (app/pages)
"pages/index.tsx" "pages/index.jsx"
"src/pages/index.tsx" "src/pages/index.jsx"
"app/page.tsx" "app/page.jsx"
"src/app/page.tsx" "src/app/page.jsx"
];
};
# To run something each time the workspace is (re)started, use the `onStart` hook
};
# Enable previews and customize configuration
previews = {
enable = true;
previews = {
web = {
command = ["npm" "run" "dev" "--" "--port" "$PORT" "--hostname" "0.0.0.0"];
manager = "web";
};
};
};
};
}

View file

@ -9,37 +9,9 @@ There are a few ways to contribute:
- Submit Issues/Docs/Bugs ([Issues](https://github.com/voideditor/void/issues))
## 1. Building the Extension
## 2. Building the IDE
Here's how you can start contributing to the Void extension. This is where you should get started if you're new.
1. Clone the repository:
```
git clone https://github.com/voideditor/void
```
2. Open the folder `/extensions/void` in VSCode (open it in a new workspace, _don't_ just cd into it).
3. Install dependencies:
```
npm install
```
1. Compile the React files by running `npm run build`. This build command converts all the Tailwind/React entrypoint files into raw .css and .js files in `dist/`.
```
npm run build
```
5. Run the extension in a new window by pressing <kbd>F5</kbd>.
This will start a new instance of VSCode with the extension enabled. If this doesn't work, you can press <kbd>Ctrl+Shift+P</kbd>, select "Debug: Start Debugging", and select "VSCode Extension Development".
## 2. Building the full IDE
If you want to work on the full IDE, please follow the steps below. If you have any questions/issues, you can refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page. Also feel free to submit an issue or get in touch with us with any build errors.
Please follow the steps below to build the IDE. If you have any questions/issues, you can refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page. Also feel free to [submit an issue](https://github.com/voideditor/void/issues/new) or get in touch with us with any build errors.
<!-- TODO say whether you can build each distribution on any Operating System, or if you need to build Windows on Windows, etc -->
@ -70,7 +42,7 @@ We haven't created prerequisite steps for building on Linux yet, but you can fol
### Build instructions
Before building Void, please follow the prerequisite steps above for your operating system. Also, make sure you've already built and compiled the Void extension (or just run `cd ./extensions/void && npm install && npm run build && npm run compile && cd ../..`).
Before building Void, please follow the prerequisite steps above for your operating system. Also, make sure you've already built and compiled the Void React components by running `cd ./` (or just run `cd ./extensions/void && npm install && npm run build && npm run compile && cd ../..`).
To build Void, first open `void/` in VSCode. Then:
@ -80,7 +52,9 @@ To build Void, first open `void/` in VSCode. Then:
npm install
```
2. Press <kbd>Ctrl+Shift+B</kbd>, or if you prefer using the terminal run `npm run watch`.
2. Build Void's React components by running `cd ./src/vs/workbench/contrib/void/browser/react/`, and executing the build script with `node ./build.js`. You might need to run `npm i -g tsup` if this doesn't work.
3. Press <kbd>Ctrl+Shift+B</kbd>, or if you prefer using the terminal run `npm run watch`.
This can take ~5 min.
@ -97,11 +71,10 @@ 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!
4. In a new terminal, run `./scripts/code.sh` (Mac/Linux) or `./scripts/code.bat` (Windows). This should open up the built IDE!
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!
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page.
### Common Fixes

View file

@ -0,0 +1,333 @@
import * as vscode from 'vscode';
import Parser from 'tree-sitter';
import JavaScript from 'tree-sitter-javascript';
interface Definition {
file: string;
node: Parser.SyntaxNode;
}
interface DefnUse {
parent: Parser.SyntaxNode;
file: string;
}
interface ImportInfo {
source: string;
imported: string;
}
class ProjectAnalyzer {
private parser: Parser;
private graph: Map<string, Set<string>>;
private visited: Set<string>;
private parsedFiles: Map<string, Parser.Tree>;
private imports: Map<string, Map<string, ImportInfo>>;
private definitions: Map<string, Definition>;
private fileStack: Set<string>;
constructor() {
this.parser = new Parser();
this.parser.setLanguage(JavaScript);
this.graph = new Map();
this.visited = new Set();
this.parsedFiles = new Map();
this.imports = new Map();
this.definitions = new Map();
this.fileStack = new Set();
}
async parseFile(filePath: string): Promise<Parser.Tree | null> {
if (this.parsedFiles.has(filePath)) {
return this.parsedFiles.get(filePath)!;
}
if (this.fileStack.has(filePath)) {
return null; // Circular import
}
this.fileStack.add(filePath);
try {
const uri = vscode.Uri.file(filePath);
const document = await vscode.workspace.openTextDocument(uri);
const code = document.getText();
const tree = this.parser.parse(code);
this.parsedFiles.set(filePath, tree);
this.collectImports(filePath, tree);
this.collectDefinitions(filePath, tree);
return tree;
} catch (error) {
console.error(`Error parsing ${filePath}:`, error);
return null;
} finally {
this.fileStack.delete(filePath);
}
}
private collectImports(filePath: string, tree: Parser.Tree): void {
const fileImports = new Map<string, ImportInfo>();
const visit = (node: Parser.SyntaxNode): void => {
if (node.type === 'import_declaration') {
const source = node.childForFieldName('source')?.text.slice(1, -1) ?? '';
const specifiers = node.childForFieldName('specifiers');
specifiers?.children.forEach(spec => {
if (spec.type === 'import_specifier') {
const local = spec.childForFieldName('local')?.text ?? '';
const imported = spec.childForFieldName('imported')?.text ?? '';
fileImports.set(local, { source, imported });
}
});
}
node.children.forEach(visit);
};
visit(tree.rootNode);
this.imports.set(filePath, fileImports);
}
private collectDefinitions(filePath: string, tree: Parser.Tree): void {
const visit = (node: Parser.SyntaxNode): void => {
if (node.type === 'function_declaration') {
const name = node.childForFieldName('name')?.text ?? '';
this.definitions.set(name, { file: filePath, node });
}
else if (node.type === 'variable_declarator') {
const name = node.childForFieldName('name')?.text;
const value = node.childForFieldName('value');
if (name && (value?.type === 'arrow_function' || value?.type === 'function')) {
this.definitions.set(name, { file: filePath, node: value });
}
}
node.children.forEach(visit);
};
visit(tree.rootNode);
}
private async getTypeFromPosition(uri: vscode.Uri, position: vscode.Position): Promise<string | null> {
const hover = await vscode.commands.executeCommand<vscode.Hover[]>(
'vscode.executeHoverProvider',
uri,
position
);
if (hover?.[0]?.contents.length) {
for (const content of hover[0].contents) {
let hoverText = typeof content === 'string' ?
content :
('value' in content ? content.value : '');
// Remove typescript backticks if present
hoverText = hoverText.replace(/```typescript\s*/, '').replace(/```\s*$/, '');
console.log('Processing hover text:', hoverText);
// Extract the type information - look for the type after the colon
const typeMatches = [
/:\s*([\w<>]+)(?:\[\])?/, // matches "foo: Type" or "foo: Type[]"
/var\s+\w+:\s*([\w<>]+)/, // matches "var foo: Type"
/\(type\)\s+[\w<>]+:\s*([\w<>]+)/, // matches "(type) foo: Type"
/\(method\)\s*([\w<>]+)\./ // matches "(method) Type.method"
];
for (const pattern of typeMatches) {
const match = pattern.exec(hoverText);
if (match) {
let type = match[1];
// Handle array types
if (hoverText.includes('[]')) {
return 'Array';
}
// Extract base type from generics
if (type.includes('<')) {
type = type.split('<')[0];
}
return type;
}
}
}
}
return null;
}
private async getCallsInDefn(defnNode: Parser.SyntaxNode, currentFile: string): Promise<Set<string>> {
const calls = new Set<string>();
const fileImports = this.imports.get(currentFile) ?? new Map();
const uri = vscode.Uri.file(currentFile);
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
if (node.type === 'call_expression') {
const callee = node.childForFieldName('function');
if (callee?.type === 'identifier') {
const name = callee.text;
const importInfo = fileImports.get(name);
if (importInfo) {
calls.add(`${importInfo.source}:${importInfo.imported}`);
} else {
calls.add(name);
}
}
else if (callee?.type === 'member_expression') {
const method = callee.childForFieldName('property')?.text;
const object = callee.childForFieldName('object');
if (method && object) {
const position = new vscode.Position(
object.startPosition.row,
object.startPosition.column
);
const type = await this.getTypeFromPosition(uri, position);
if (type) {
calls.add(`${type}.${method}`);
} else {
calls.add(`method:${method}`);
}
}
}
}
for (const child of node.children) {
await visit(child);
}
};
await visit(defnNode);
return calls;
}
private gotoDefn(name: string): Definition | null {
if (name.includes(':')) {
const [file, funcName] = name.split(':');
const def = this.definitions.get(funcName);
return def ?? null;
}
return this.definitions.get(name) ?? null;
}
private getUses(defnNode: Parser.SyntaxNode, currentFile: string): DefnUse[] {
const uses: DefnUse[] = [];
let fnName: string | undefined;
if (defnNode.type === 'function_declaration') {
fnName = defnNode.childForFieldName('name')?.text;
} else if (defnNode.type === 'arrow_function' || defnNode.type === 'function') {
const parent = defnNode.parent;
if (parent?.type === 'variable_declarator') {
fnName = parent.childForFieldName('name')?.text;
}
}
if (!fnName) return uses;
for (const [file, tree] of this.parsedFiles) {
const visit = (node: Parser.SyntaxNode): void => {
if (node.type === 'call_expression') {
const callee = node.childForFieldName('function');
if (callee?.type === 'identifier' && callee.text === fnName) {
let current: Parser.SyntaxNode | null = node;
while (current) {
if (current.type === 'function_declaration' ||
current.type === 'arrow_function' ||
current.type === 'function') {
uses.push({ parent: current, file });
break;
}
current = current.parent;
}
}
}
node.children.forEach(visit);
};
visit(tree.rootNode);
}
return uses;
}
private async visitAllNodesInGraphFromDefinition(defn: Parser.SyntaxNode, currentFile: string): Promise<void> {
let defnName: string | undefined;
if (defn.type === 'function_declaration') {
defnName = defn.childForFieldName('name')?.text;
} else if (defn.type === 'arrow_function' || defn.type === 'function') {
const parent = defn.parent;
if (parent?.type === 'variable_declarator') {
defnName = parent.childForFieldName('name')?.text;
}
}
if (!defnName) return;
const fullName = `${currentFile}:${defnName}`;
if (this.visited.has(fullName)) return;
const calls = await this.getCallsInDefn(defn, currentFile);
this.graph.set(fullName, calls);
this.visited.add(fullName);
const callDefns = Array.from(calls).map(call => this.gotoDefn(call));
for (const callDefn of callDefns) {
if (callDefn) {
await this.visitAllNodesInGraphFromDefinition(callDefn.node, callDefn.file);
}
}
const defnUses = this.getUses(defn, currentFile);
for (const defnUse of defnUses) {
await this.visitAllNodesInGraphFromDefinition(defnUse.parent, defnUse.file);
}
}
async analyze(entryFile: string): Promise<Map<string, Set<string>>> {
const tree = await this.parseFile(entryFile);
if (!tree) return new Map();
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
if (node.type === 'function_declaration') {
await this.visitAllNodesInGraphFromDefinition(node, entryFile);
}
else if (node.type === 'variable_declarator') {
const value = node.childForFieldName('value');
if (value?.type === 'arrow_function' || value?.type === 'function') {
await this.visitAllNodesInGraphFromDefinition(value, entryFile);
}
}
for (const child of node.children) {
await visit(child);
}
};
await visit(tree.rootNode);
return this.graph;
}
}
export async function runTreeSitter(filePath?: string): Promise<Map<string, Set<string>> | null> {
const editor = vscode.window.activeTextEditor;
if (!editor && !filePath) {
vscode.window.showWarningMessage('No active editor found');
return null;
}
try {
const targetPath = filePath ?? editor!.document.uri.fsPath;
const analyzer = new ProjectAnalyzer();
const graph = await analyzer.analyze(targetPath);
for (const [defn, calls] of graph) {
console.log(`${defn} calls: ${[...calls].join(', ')}`);
}
return graph;
} catch (error) {
console.error('Error analyzing file:', error);
vscode.window.showErrorMessage('Error analyzing file');
return null;
}
}

View file

@ -0,0 +1,62 @@
import * as vscode from 'vscode';
const legend = new vscode.SemanticTokensLegend([], []);
export async function findFunctions() {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const document = editor.document;
const tokens = await vscode.commands.executeCommand<vscode.SemanticTokens>(
'vscode.provideDocumentSemanticTokens',
document.uri
);
if (!tokens) {
console.error('No tokens found');
return [];
}
const allTokens = decodeTokens(tokens, document);
return allTokens;
}
function decodeTokens(tokens: vscode.SemanticTokens, document: vscode.TextDocument) {
const data = tokens.data;
const decodedTokens = [];
let line = 0;
let character = 0;
for (let i = 0; i < data.length; i += 5) {
const deltaLine = data[i];
const deltaStartChar = data[i + 1];
const length = data[i + 2];
const tokenTypeIdx = data[i + 3];
const tokenModifierIdx = data[i + 4];
line += deltaLine;
character = deltaLine === 0 ? character + deltaStartChar : deltaStartChar;
const type = legend.tokenTypes[tokenTypeIdx] || `(${tokenTypeIdx})`;
const modifier = legend.tokenModifiers[tokenModifierIdx] || `(${tokenModifierIdx})`;
const tokenRange = new vscode.Range(line, character, line, character + length);
const tokenText = document.getText(tokenRange);
decodedTokens.push({
line,
startCharacter: character,
length,
type,
modifier,
text: tokenText,
});
console.log(`Token: '${tokenText}' | Type: ${type} | Modifier: ${modifier} | Line: ${line}, Character: ${character}`);
}
return decodedTokens;
}

View file

@ -0,0 +1,32 @@
import { LRUCache } from 'lru-cache';
const DEFAULT_MAX_SIZE = 20
export class SimpleLRUCache<T extends {}> {
private cache: LRUCache<number, T>;
private maxSize: number
public length: number
constructor(maxSize?: number) {
maxSize = maxSize ?? DEFAULT_MAX_SIZE
this.cache = new LRUCache<number, T>({ max: maxSize });
this.length = 0
this.maxSize = maxSize
}
push(value: T): void {
const key = this.cache.size;
this.cache.set(key, value);
this.length++
this.length = Math.min(this.length, this.maxSize)
}
values() {
return this.cache.values()
}
}

View file

@ -0,0 +1,105 @@
import { configFields, VoidConfig } from "../webviews/common/contextForConfig"
import { FimInfo } from "./sendLLMMessage"
type GetFIMPrompt = ({ voidConfig, fimInfo }: { voidConfig: VoidConfig, fimInfo: FimInfo, }) => string
export const getFIMSystem: GetFIMPrompt = ({ voidConfig, fimInfo }) => {
switch (voidConfig.default.whichApi) {
case 'ollama':
return ''
case 'anthropic':
case 'openAI':
case 'gemini':
case 'greptile':
case 'openRouter':
case 'openAICompatible':
case 'azure':
default:
return `You are given the START and END to a piece of code. Please FILL IN THE MIDDLE between the START and END.
Instruction summary:
1. Return the MIDDLE of the code between the START and END.
2. Do not give an explanation, description, or any other code besides the middle.
2. Do not return duplicate code from either START or END.
3. Make sure the MIDDLE piece of code has balanced brackets that match the START and END.
4. The MIDDLE begins on the same line as START. Please include a newline character if you want to begin on the next line.
# EXAMPLE
## START:
\`\`\` python
def add(a,b):
return a + b
def subtract(a,b):
return a - b
\`\`\`
## END:
\`\`\` python
def divide(a,b):
return a / b
\`\`\`
## EXPECTED OUTPUT:
\`\`\` python
def multiply(a,b):
return a * b
\`\`\`
# EXAMPLE
## START:
\`\`\` javascript
const x = 1
const y
\`\`\`
## END:
\`\`\` javascript
const z = 3
\`\`\`
## EXPECTED OUTPUT:
\`\`\` javascript
= 2
\`\`\`
`
}
}
export const getFIMPrompt: GetFIMPrompt = ({ voidConfig, fimInfo }) => {
// if no prefix or suffix, return empty string
if (!fimInfo.prefix.trim() && !fimInfo.suffix.trim()) return ''
// TODO may want to trim the prefix and suffix
switch (voidConfig.default.whichApi) {
case 'ollama':
if (voidConfig.ollama.model === 'codestral') {
return `[SUFFIX]${fimInfo.suffix}[PREFIX] ${fimInfo.prefix}`
}
return ''
case 'anthropic':
case 'openAI':
case 'gemini':
case 'greptile':
case 'openRouter':
case 'openAICompatible':
case 'azure':
default:
return `## START:
\`\`\`
${fimInfo.prefix}
\`\`\`
## END:
\`\`\`
${fimInfo.suffix}
\`\`\`
`
}
}

View file

@ -0,0 +1,282 @@
import * as vscode from 'vscode';
import { AbortRef, LLMMessage, sendLLMMessage } from '../common/sendLLMMessage';
import { getVoidConfigFromPartial, VoidConfig } from '../webviews/common/contextForConfig';
import { LRUCache } from 'lru-cache';
import { SimpleLRUCache } from '../common/SimpleLruCache';
type AutocompletionStatus = 'pending' | 'finished' | 'error';
type Autocompletion = {
prefix: string,
suffix: string,
startTime: number,
endTime: number | undefined,
abortRef: AbortRef,
status: AutocompletionStatus,
promise: Promise<string> | undefined,
result: string,
}
const DEBOUNCE_TIME = 300
const TIMEOUT_TIME = 60000
// postprocesses the result
const postprocessResult = (result: string) => {
// remove leading whitespace from result
return result.trimStart()
}
const extractCodeFromResult = (result: string) => {
// extract the code between triple backticks
const parts = result.split(/```/);
// if there is no ``` then return the raw result
if (parts.length === 1) {
return result;
}
// else return the code between the triple backticks
return parts[1]
}
// trims the end of the prefix to improve cache hit rate
const trimPrefix = (prefix: string) => {
const trimmedPrefix = prefix.trimEnd()
const trailingEnd = prefix.substring(trimmedPrefix.length)
// keep only a single trailing newline
if (trailingEnd.includes('\n')) {
return trimmedPrefix + '\n'
}
// else ignore all spaces and return the trimmed prefix
return trimmedPrefix
}
// finds the text in the autocompletion to display, assuming the prefix is already matched
// example:
// originalPrefix = abcd
// generatedMiddle = efgh
// originalSuffix = ijkl
// the user has typed "ef" so prefix = abcdef
// we want to return the rest of the generatedMiddle, which is "gh"
const toInlineCompletion = ({ prefix, autocompletion, position }: { prefix: string, autocompletion: Autocompletion, position: vscode.Position }): vscode.InlineCompletionItem => {
const originalPrefix = autocompletion.prefix
const generatedMiddle = autocompletion.result
const trimmedOriginalPrefix = trimPrefix(originalPrefix)
const trimmedCurrentPrefix = trimPrefix(prefix)
const lastMatchupIndex = trimmedCurrentPrefix.length - trimmedOriginalPrefix.length
console.log('generatedMiddle ', generatedMiddle)
console.log('trimmedOriginalPrefix ', trimmedOriginalPrefix)
console.log('trimmedCurrentPrefix ', trimmedCurrentPrefix)
console.log('index: ', lastMatchupIndex)
if (lastMatchupIndex < 0) {
return new vscode.InlineCompletionItem('')
}
const completionStr = generatedMiddle.substring(lastMatchupIndex)
console.log('completionStr: ', completionStr)
return new vscode.InlineCompletionItem(
completionStr,
new vscode.Range(position, position)
)
}
// returns whether we can use this autocompletion to complete the prefix
const doesPrefixMatchAutocompletion = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): boolean => {
const originalPrefix = autocompletion.prefix
const generatedMiddle = autocompletion.result
const trimmedOriginalPrefix = trimPrefix(originalPrefix)
const trimmedCurrentPrefix = trimPrefix(prefix)
if (trimmedCurrentPrefix.length < trimmedOriginalPrefix.length) {
return false
}
const isMatch = (trimmedOriginalPrefix + generatedMiddle).startsWith(trimmedCurrentPrefix)
return isMatch
}
export class AutocompleteProvider implements vscode.InlineCompletionItemProvider {
private _extensionContext: vscode.ExtensionContext;
private _autocompletionsOfDocument: { [docUriStr: string]: SimpleLRUCache<Autocompletion> } = {}
private _lastTime = 0
constructor(context: vscode.ExtensionContext) {
this._extensionContext = context
}
// used internally by vscode
// fires after every keystroke
async provideInlineCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
context: vscode.InlineCompletionContext,
token: vscode.CancellationToken,
): Promise<vscode.InlineCompletionItem[]> {
const disabled = true
if (disabled) { return []; }
const docUriStr = document.uri.toString()
const fullText = document.getText();
const cursorOffset = document.offsetAt(position);
const prefix = fullText.substring(0, cursorOffset)
const suffix = fullText.substring(cursorOffset)
if (!this._autocompletionsOfDocument[docUriStr]) {
this._autocompletionsOfDocument[docUriStr] = new SimpleLRUCache()
}
const voidConfig = getVoidConfigFromPartial(this._extensionContext.globalState.get('partialVoidConfig') ?? {})
// get autocompletion from cache
let cachedAutocompletion: Autocompletion | undefined = undefined
loop: for (const autocompletion of this._autocompletionsOfDocument[docUriStr].values()) {
// if the user's change matches up with the generated text
if (doesPrefixMatchAutocompletion({ prefix, autocompletion })) {
cachedAutocompletion = autocompletion
break loop;
}
}
// if there is a cached autocompletion, return it
if (cachedAutocompletion) {
if (cachedAutocompletion.status === 'finished') {
console.log('AAA1')
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, position })
return [inlineCompletion]
} else if (cachedAutocompletion.status === 'pending') {
console.log('AAA2')
try {
await cachedAutocompletion.promise;
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, position })
return [inlineCompletion]
} catch (e) {
console.error('Error creating autocompletion (1): ' + e)
}
} else if (cachedAutocompletion.status === 'error') {
console.log('AAA3')
}
return []
}
// if there is no cached autocompletion, create it and add it to cache
// wait DEBOUNCE_TIME for the user to stop typing
const thisTime = Date.now()
this._lastTime = thisTime
const didTypingHappenDuringDebounce = await new Promise((resolve, reject) =>
setTimeout(() => {
if (this._lastTime === thisTime) {
resolve(false)
} else {
resolve(true)
}
}, DEBOUNCE_TIME)
)
// if more typing happened, then do not go forwards with the request
if (didTypingHappenDuringDebounce) {
return []
}
console.log('BBB')
// else if no more typing happens, then go forwards with the request
const newAutocompletion: Autocompletion = {
prefix: prefix,
suffix: suffix,
startTime: Date.now(),
endTime: undefined,
abortRef: { current: () => { } },
status: 'pending',
promise: undefined,
result: '',
}
// set parameters of `newAutocompletion` appropriately
newAutocompletion.promise = new Promise((resolve, reject) => {
sendLLMMessage({
mode: 'fim',
fimInfo: { prefix, suffix },
onText: async (tokenStr, completionStr) => {
// TODO filter out bad responses here
newAutocompletion.result = completionStr
},
onFinalMessage: (finalMessage) => {
// newAutocompletion.prefix = prefix
// newAutocompletion.suffix = suffix
// newAutocompletion.startTime = Date.now()
newAutocompletion.endTime = Date.now()
// newAutocompletion.abortRef = { current: () => { } }
newAutocompletion.status = 'finished'
// newAutocompletion.promise = undefined
newAutocompletion.result = postprocessResult(extractCodeFromResult(finalMessage))
resolve(newAutocompletion.result)
},
onError: (e) => {
newAutocompletion.endTime = Date.now()
newAutocompletion.status = 'error'
reject(e)
},
voidConfig,
abortRef: newAutocompletion.abortRef,
})
setTimeout(() => { // if the request hasnt resolved in TIMEOUT_TIME seconds, reject it
if (newAutocompletion.status === 'pending') {
reject('Timeout')
}
}, TIMEOUT_TIME)
})
// add autocompletion to cache
this._autocompletionsOfDocument[docUriStr].push(newAutocompletion)
// show autocompletion
try {
await newAutocompletion.promise;
const inlineCompletion = toInlineCompletion({ autocompletion: newAutocompletion, prefix, position })
return [inlineCompletion]
} catch (e) {
console.error('Error creating autocompletion (2): ' + e)
return []
}
}
}

View file

@ -21,6 +21,10 @@
"properties": {}
},
"commands": [
{
"command": "typeInspector.inspect",
"title": "Inspect Types of All Variables"
},
{
"command": "void.ctrl+l",
"title": "Show Sidebar"
@ -154,5 +158,11 @@
"typescript": "5.5.4",
"typescript-eslint": "^8.3.0",
"uuid": "^10.0.0"
},
"dependencies": {
"lru-cache": "^11.0.2",
"tree-sitter": "^0.21.1",
"tree-sitter-javascript": "^0.23.1",
"tree-sitter-python": "^0.23.4"
}
}

View file

@ -1,4 +1,14 @@
// // used for ctrl+l
// const partialGenerationInstructions = ``
// // used for ctrl+k, autocomplete
// const fimInstructions = ``
export 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\`.
@ -398,3 +408,4 @@ export default Sidebar;\`\`\`
`

View file

@ -1,9 +1,15 @@
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { Ollama } from 'ollama/browser'
<<<<<<< HEAD:src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
import { posthog } from 'posthog-js'
import type { VoidConfig } from '../../../registerConfig.js';
=======
import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai';
import { VoidConfig } from '../webviews/common/contextForConfig'
import { getFIMPrompt, getFIMSystem } from './getPrompt';
>>>>>>> origin/main:extensions/void/src/common/sendLLMMessage.ts
export type AbortRef = { current: (() => void) | null }
@ -22,6 +28,7 @@ export type LLMMessage = {
}
type SendLLMMessageFnTypeInternal = (params: {
<<<<<<< HEAD:src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx
messages: LLMMessage[];
onText: OnText;
onFinalMessage: OnFinalMessage;
@ -42,8 +49,34 @@ type SendLLMMessageFnTypeExternal = (params: {
logging: {
loggingName: string,
};
=======
mode: 'chat' | 'fim',
messages: LLMMessage[],
onText: OnText,
onFinalMessage: OnFinalMessage,
onError: (error: string) => void,
abortRef: AbortRef,
voidConfig: VoidConfig,
}) => void
type SendLLMMessageFnTypeExternal = (params: (
| { mode?: 'chat', messages: LLMMessage[], fimInfo?: undefined, }
| { mode: 'fim', fimInfo: FimInfo, messages?: undefined, }
) & {
onText: OnText,
onFinalMessage: OnFinalMessage,
onError: (error: string) => void,
abortRef: AbortRef,
voidConfig: VoidConfig | null, // these may be absent
>>>>>>> origin/main:extensions/void/src/common/sendLLMMessage.ts
}) => void
export type FimInfo = {
prefix: string,
suffix: string,
}
const parseMaxTokensStr = (maxTokensStr: string) => {
// parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
@ -213,34 +246,94 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
};
// Ollama
<<<<<<< HEAD:src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
let fullText = ''
=======
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let didAbort = false
let fullText = ""
abortRef.current = () => {
didAbort = true;
};
>>>>>>> origin/main:extensions/void/src/common/sendLLMMessage.ts
const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
ollama.chat({
model: voidConfig.ollama.model,
messages: messages,
stream: true,
options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens
})
type GenerateResponse = Awaited<ReturnType<(typeof ollama.generate)>>
type ChatResponse = Awaited<ReturnType<(typeof ollama.chat)>>
// First check if model exists
ollama.list()
.then(async models => {
const installedModels = models.models.map(m => m.name.replace(/:latest$/, ''))
const modelExists = installedModels.some(m => m.startsWith(voidConfig.ollama.model));
if (!modelExists) {
const errorMessage = `The model "${voidConfig.ollama.model}" is not available locally. Please run 'ollama pull ${voidConfig.ollama.model}' to download it first or
try selecting one from the Installed models: ${installedModels.join(', ')}`;
onText(errorMessage, errorMessage);
onFinalMessage(errorMessage);
return Promise.reject();
}
if (mode === 'fim') {
// the fim prompt is the last message
let prompt = messages[messages.length - 1].content
return ollama.generate({
model: voidConfig.ollama.model,
prompt: prompt,
stream: true,
raw: true,
})
}
return ollama.chat({
model: voidConfig.ollama.model,
messages: messages,
stream: true,
options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) }
});
})
.then(async stream => {
<<<<<<< HEAD:src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx
_setAborter(() => stream.abort())
// iterate through the stream
for await (const chunk of stream) {
const newText = chunk.message.content;
=======
if (!stream) return;
abortRef.current = () => {
didAbort = true
}
for await (const chunk of stream) {
if (didAbort) return;
const newText = (mode === 'fim'
? (chunk as GenerateResponse).response
: (chunk as ChatResponse).message.content
)
>>>>>>> origin/main:extensions/void/src/common/sendLLMMessage.ts
fullText += newText;
onText(newText, fullText);
}
onFinalMessage(fullText);
})
// when error/fail
.catch(error => {
onError(error)
})
// Check if the error is a connection error
if (error instanceof Error && error.message.includes('Failed to fetch')) {
const errorMessage = 'Ollama service is not running. Please start the Ollama service and try again.';
onText(errorMessage, errorMessage);
onFinalMessage(errorMessage);
} else if (error) {
onError(error);
}
});
};
// Greptile
@ -303,6 +396,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
}
<<<<<<< HEAD:src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx
@ -317,10 +411,37 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({
logging: { loggingName }
}) => {
if (!voidConfig) return;
=======
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ mode, messages, fimInfo, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
if (!voidConfig)
return onError('No config file found for LLM.');
// handle defaults
if (!mode) mode = 'chat'
if (!messages) messages = []
// build messages
if (mode === 'chat') {
// nothing needed
} else if (mode === 'fim') {
fimInfo = fimInfo!
const system = getFIMSystem({ voidConfig, fimInfo })
const prompt = getFIMPrompt({ voidConfig, fimInfo })
messages = ([
{ role: 'system', content: system },
{ role: 'user', content: prompt }
] as const)
.filter(m => m.content.trim() !== '')
}
>>>>>>> origin/main:extensions/void/src/common/sendLLMMessage.ts
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
if (messages.length === 0)
return onError('No messages provided to LLM.');
<<<<<<< HEAD:src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureChatEvent = (eventId: string, extras?: object) => {
posthog.capture(eventId, {
@ -397,5 +518,23 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({
}
=======
switch (voidConfig.default.whichApi) {
case 'anthropic':
return sendAnthropicMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'openAI':
case 'openRouter':
case 'openAICompatible':
return sendOpenAIMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'gemini':
return sendGeminiMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'ollama':
return sendOllamaMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'greptile':
return sendGreptileMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
default:
onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
}
>>>>>>> origin/main:extensions/void/src/common/sendLLMMessage.ts
}

View file

@ -127,12 +127,11 @@ const voidConfigInfo: Record<
'The endpoint of your Ollama instance. Start Ollama by running `OLLAMA_ORIGINS="vscode - webview://*" ollama serve`.',
'http://127.0.0.1:11434'
),
// TODO we should allow user to select model inside Void, but for now we'll just let them handle the Ollama setup on their own
// model: configEnum(
// 'Ollama model to use.',
// 'llama3.1',
// ['codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b'] as const
// ),
model: configEnum(
'Ollama model to use.',
'codestral',
['codestral', 'codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b'] as const
),
},
openRouter: {
model: configString(

View file

@ -704,6 +704,30 @@ INSTRUCTIONS
Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation.
`
// CTRL+K prompt:
// const promptContent = `Here is the user's original selection:
// \`\`\`
// <MID>${selection}</MID>
// \`\`\`
// The user wants to apply the following instructions to the selection:
// ${instructions}
// Please rewrite the selection following the user's instructions.
// Instructions to follow:
// 1. Follow the user's instructions
// 2. You may ONLY CHANGE the selection, and nothing else in the file
// 3. Make sure all brackets in the new selection are balanced the same was as in the original selection
// 3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake
// Complete the following:
// \`\`\`
// <PRE>${prefix}</PRE>
// <SUF>${suffix}</SUF>
// <MID>`;
const abortRef = { current: null } as { current: null | (() => void) }
await new Promise<void>((resolve, reject) => {
sendLLMMessage({
@ -1000,3 +1024,29 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget {
}
// // 6. Autocomplete
// const autocompleteProvider = new AutocompleteProvider(context);
// context.subscriptions.push(vscode.languages.registerInlineCompletionItemProvider('*', autocompleteProvider));
// const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {})
// const abortRef: AbortRef = { current: null }
// // setupAutocomplete({ voidConfig, abortRef })
// // 7. Language Server
// console.log('run lsp')
// let disposable = vscode.commands.registerCommand('typeInspector.inspect', runTreeSitter);
// context.subscriptions.push(disposable);

View file

@ -10,6 +10,21 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet
import { posthog } from './react/out/util/posthog.js'
const buildEnv = 'development';
const buildNumber = '1.0.0';
const isMac = process.platform === 'darwin';
// TODO use commandKey
const commandKey = isMac ? '⌘' : 'Ctrl';
const systemInfo = {
buildEnv,
buildNumber,
isMac,
}
interface IMetricsService {
readonly _serviceBrand: undefined;
}