diff --git a/.idx/dev.nix b/.idx/dev.nix new file mode 100644 index 00000000..83eb293f --- /dev/null +++ b/.idx/dev.nix @@ -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"; + }; + }; + }; + }; +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b27e119..f06d13d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 F5. - -This will start a new instance of VSCode with the extension enabled. If this doesn't work, you can press Ctrl+Shift+P, 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. @@ -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 Ctrl+Shift+B, 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 Ctrl+Shift+B, 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: -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 Ctrl+Shift+P 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 diff --git a/extensions/void/src/common/LangaugeServer/createJsProgramGraph.ts b/extensions/void/src/common/LangaugeServer/createJsProgramGraph.ts new file mode 100644 index 00000000..4fb9bcaf --- /dev/null +++ b/extensions/void/src/common/LangaugeServer/createJsProgramGraph.ts @@ -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>; + private visited: Set; + private parsedFiles: Map; + private imports: Map>; + private definitions: Map; + private fileStack: Set; + + 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 { + 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(); + + 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 { + const hover = await vscode.commands.executeCommand( + '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> { + const calls = new Set(); + const fileImports = this.imports.get(currentFile) ?? new Map(); + const uri = vscode.Uri.file(currentFile); + + const visit = async (node: Parser.SyntaxNode): Promise => { + 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 { + 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>> { + const tree = await this.parseFile(entryFile); + if (!tree) return new Map(); + + const visit = async (node: Parser.SyntaxNode): Promise => { + 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> | 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; + } +} \ No newline at end of file diff --git a/extensions/void/src/common/LangaugeServer/findFunctions.ts b/extensions/void/src/common/LangaugeServer/findFunctions.ts new file mode 100644 index 00000000..570b4369 --- /dev/null +++ b/extensions/void/src/common/LangaugeServer/findFunctions.ts @@ -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.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; +} diff --git a/extensions/void/src/common/SimpleLruCache.ts b/extensions/void/src/common/SimpleLruCache.ts new file mode 100644 index 00000000..7118bc8f --- /dev/null +++ b/extensions/void/src/common/SimpleLruCache.ts @@ -0,0 +1,32 @@ +import { LRUCache } from 'lru-cache'; + +const DEFAULT_MAX_SIZE = 20 + + +export class SimpleLRUCache { + private cache: LRUCache; + private maxSize: number + public length: number + + constructor(maxSize?: number) { + + maxSize = maxSize ?? DEFAULT_MAX_SIZE + + this.cache = new LRUCache({ 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() + } + + +} \ No newline at end of file diff --git a/extensions/void/src/common/getPrompt.ts b/extensions/void/src/common/getPrompt.ts new file mode 100644 index 00000000..f1c7567b --- /dev/null +++ b/extensions/void/src/common/getPrompt.ts @@ -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} +\`\`\` +` + + } +} + diff --git a/extensions/void/src/extension/AutcompleteProvider.ts b/extensions/void/src/extension/AutcompleteProvider.ts new file mode 100644 index 00000000..2aad378a --- /dev/null +++ b/extensions/void/src/extension/AutcompleteProvider.ts @@ -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 | 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 } = {} + + 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 { + + 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 [] + } + + } + + + + +} diff --git a/src/vs/workbench/contrib/void/browser/misc/oldpackage.json b/src/vs/workbench/contrib/void/browser/misc/oldpackage.json index 855e2190..f1eedb08 100644 --- a/src/vs/workbench/contrib/void/browser/misc/oldpackage.json +++ b/src/vs/workbench/contrib/void/browser/misc/oldpackage.json @@ -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" } } diff --git a/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts b/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts index c18a70c9..2faa8909 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts @@ -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;\`\`\` ` + diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx index 9a797442..5736c9f0 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx @@ -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> + type ChatResponse = Awaited> + + + // 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 } diff --git a/src/vs/workbench/contrib/void/browser/registerConfig.ts b/src/vs/workbench/contrib/void/browser/registerConfig.ts index 7164c644..bf7dc86c 100644 --- a/src/vs/workbench/contrib/void/browser/registerConfig.ts +++ b/src/vs/workbench/contrib/void/browser/registerConfig.ts @@ -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( diff --git a/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts b/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts index 0e77ca08..70df9f16 100644 --- a/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts +++ b/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts @@ -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: + // \`\`\` + // ${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} + // `; + const abortRef = { current: null } as { current: null | (() => void) } await new Promise((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); + diff --git a/src/vs/workbench/contrib/void/browser/registerMetrics.ts b/src/vs/workbench/contrib/void/browser/registerMetrics.ts index 4a302c36..14a52b10 100644 --- a/src/vs/workbench/contrib/void/browser/registerMetrics.ts +++ b/src/vs/workbench/contrib/void/browser/registerMetrics.ts @@ -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; }