diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0125ac46..20fe049f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -97,7 +97,7 @@ 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! +1. 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! diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index 15226ca5..283fcb8c 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -7,6 +7,12 @@ "": { "name": "void", "version": "0.0.1", + "dependencies": { + "lru-cache": "^11.0.2", + "tree-sitter": "^0.21.1", + "tree-sitter-javascript": "^0.23.1", + "tree-sitter-python": "^0.23.4" + }, "devDependencies": { "@anthropic-ai/sdk": "^0.29.2", "@eslint/js": "^9.9.1", @@ -35,8 +41,8 @@ "eslint-plugin-react": "^7.35.1", "eslint-plugin-react-hooks": "^4.6.2", "globals": "^15.9.0", - "groq-sdk": "^0.8.0", "lodash": "^4.17.21", + "groq-sdk": "^0.8.0", "marked": "^14.1.0", "ollama": "^0.5.9", "openai": "^4.68.4", @@ -4977,12 +4983,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5047,11 +5047,12 @@ } }, "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "engines": { + "node": "20 || >=22" + } }, "node_modules/make-dir": { "version": "4.0.0", @@ -5970,7 +5971,6 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/ms": { @@ -6018,6 +6018,14 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.2.tgz", + "integrity": "sha512-9emqXAKhVoNrQ792nLI/wpzPpJ/bj/YXxW0CvAau1+RdGBcCRF1Dmz7719zgVsQNrzHl9Tzn3ImZ4qWFarWL0A==", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -6059,6 +6067,16 @@ } } }, + "node_modules/node-gyp-build": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz", + "integrity": "sha512-EMS95CMJzdoSKoIiXo8pxKoL8DYxwIZXYlLmgPb8KUv794abpnLK6ynsCAWNliOjREKruYKdzbh76HHYUHX7nw==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -6560,6 +6578,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, "node_modules/picocolors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", @@ -7400,16 +7424,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/lru-cache": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", - "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/rimraf/node_modules/minimatch": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", @@ -8208,6 +8222,52 @@ "dev": true, "license": "MIT" }, + "node_modules/tree-sitter": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", + "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" + } + }, + "node_modules/tree-sitter-javascript": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/tree-sitter-javascript/-/tree-sitter-javascript-0.23.1.tgz", + "integrity": "sha512-/bnhbrTD9frUYHQTiYnPcxyHORIw157ERBa6dqzaKxvR/x3PC4Yzd+D1pZIMS6zNg2v3a8BZ0oK7jHqsQo9fWA==", + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-python": { + "version": "0.23.4", + "resolved": "https://registry.npmjs.org/tree-sitter-python/-/tree-sitter-python-0.23.4.tgz", + "integrity": "sha512-MbmUAl7y5UCUWqHscHke7DdRDwQnVNMNKQYQc4Gq2p09j+fgPxaU8JVsuOI/0HD3BSEEe5k9j3xmdtIWbDtDgw==", + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^8.2.1", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", diff --git a/extensions/void/package.json b/extensions/void/package.json index 4584f9eb..2cc8e3a7 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -21,6 +21,10 @@ "properties": {} }, "commands": [ + { + "command": "typeInspector.inspect", + "title": "Inspect Types of All Variables" + }, { "command": "void.ctrl+l", "title": "Show Sidebar" @@ -156,5 +160,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/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/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index f305ff3f..a9f883c6 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -3,8 +3,12 @@ import OpenAI from 'openai'; import { Ollama } from 'ollama/browser' import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai'; import { VoidConfig } from '../webviews/common/contextForConfig' + import Groq, { GroqError } from 'groq-sdk'; +import { getFIMPrompt, getFIMSystem } from './getPrompt'; + + export type AbortRef = { current: (() => void) | null } export type OnText = (newText: string, fullText: string) => void @@ -22,23 +26,32 @@ export type LLMMessage = { } type SendLLMMessageFnTypeInternal = (params: { + mode: 'chat' | 'fim', messages: LLMMessage[], onText: OnText, onFinalMessage: OnFinalMessage, onError: (error: string) => void, - voidConfig: VoidConfig, abortRef: AbortRef, + voidConfig: VoidConfig, }) => void -type SendLLMMessageFnTypeExternal = (params: { - messages: LLMMessage[], + +type SendLLMMessageFnTypeExternal = (params: ( + | { mode?: 'chat', messages: LLMMessage[], fimInfo?: undefined, } + | { mode: 'fim', fimInfo: FimInfo, messages?: undefined, } +) & { onText: OnText, - onFinalMessage: (fullText: string) => void, + onFinalMessage: OnFinalMessage, onError: (error: string) => void, - voidConfig: VoidConfig | null, abortRef: AbortRef, + voidConfig: VoidConfig | null, // these may be absent }) => 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 let int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr) @@ -233,7 +246,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal }; // Ollama -export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { +export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { let didAbort = false let fullText = "" @@ -244,6 +257,10 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, const ollama = new Ollama({ host: voidConfig.ollama.endpoint }) + type GenerateResponse = Awaited> + type ChatResponse = Awaited> + + // First check if model exists ollama.list() .then(async models => { @@ -257,6 +274,18 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, 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, @@ -272,7 +301,11 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, } for await (const chunk of stream) { if (didAbort) return; - const newText = chunk.message.content; + + const newText = (mode === 'fim' + ? (chunk as GenerateResponse).response + : (chunk as ChatResponse).message.content + ) fullText += newText; onText(newText, fullText); } @@ -358,6 +391,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin } + // Groq const sendGroqMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { let didAbort = false; @@ -397,26 +431,54 @@ const sendGroqMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onF export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { 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() !== '') + } // 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.'); switch (voidConfig.default.whichApi) { case 'anthropic': - return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); + return sendAnthropicMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }); case 'openAI': case 'openRouter': case 'openAICompatible': - return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); + return sendOpenAIMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }); case 'gemini': - return sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); + return sendGeminiMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }); case 'ollama': - return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); + return sendOllamaMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }); case 'greptile': + return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); case 'groq': return sendGroqMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); + + return sendGreptileMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }); default: onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`) } + } diff --git a/extensions/void/src/common/systemPrompts.ts b/extensions/void/src/common/systemPrompts.ts index 7e443053..276c8570 100644 --- a/extensions/void/src/common/systemPrompts.ts +++ b/extensions/void/src/common/systemPrompts.ts @@ -1,4 +1,14 @@ + + +// used for ctrl+l +const partialGenerationInstructions = `` + + +// used for ctrl+k, autocomplete +const fimInstructions = `` + + 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\`. @@ -399,6 +409,7 @@ export default Sidebar;\`\`\` + export { generateDiffInstructions, searchDiffChunkInstructions, diff --git a/extensions/void/src/extension/AutcompleteProvider.ts b/extensions/void/src/extension/AutcompleteProvider.ts new file mode 100644 index 00000000..5ca8fdac --- /dev/null +++ b/extensions/void/src/extension/AutcompleteProvider.ts @@ -0,0 +1,276 @@ +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 }: { prefix: string, autocompletion: Autocompletion }): 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) + +} + +// 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 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, }) + return [inlineCompletion] + + } else if (cachedAutocompletion.status === 'pending') { + console.log('AAA2') + + try { + await cachedAutocompletion.promise; + const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, }) + 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, }) + return [inlineCompletion] + + } catch (e) { + console.error('Error creating autocompletion (2): ' + e) + return [] + } + + } + + + + +} diff --git a/extensions/void/src/extension/DiffProvider.ts b/extensions/void/src/extension/DiffProvider.ts index 9836dfde..1b7d8ce8 100644 --- a/extensions/void/src/extension/DiffProvider.ts +++ b/extensions/void/src/extension/DiffProvider.ts @@ -48,8 +48,6 @@ export class DiffProvider implements vscode.CodeLensProvider { constructor(context: vscode.ExtensionContext) { this._extensionUri = context.extensionUri - console.log('Creating DisplayChangesProvider') - // this acts as a useEffect every time text changes vscode.workspace.onDidChangeTextDocument((e) => { @@ -167,7 +165,9 @@ export class DiffProvider implements vscode.CodeLensProvider { } const originalFile = this._originalFileOfDocument[docUriStr] if (!originalFile) { - console.log('Error: No original file!') + if (this._diffAreasOfDocument[docUriStr]?.length > 0) { + console.log('Error: More than one diff area exists, but no original file was set.') + } return; } @@ -189,7 +189,6 @@ export class DiffProvider implements vscode.CodeLensProvider { // add the diffs to `this._diffsOfDocument[docUriStr]` this.createDiffs(editor.document.uri, diffs, diffArea) - // // print diffs // console.log('!ORIGINAL FILE:', JSON.stringify(originalFile)) // console.log('!NEW FILE :', JSON.stringify(editor.document.getText().replace(/\r\n/g, '\n'))) @@ -451,6 +450,7 @@ export class DiffProvider implements vscode.CodeLensProvider { const diffareaRange = new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER) workspaceEdit.replace(editor.document.uri, diffareaRange, newCode) await vscode.workspace.applyEdit(workspaceEdit) + }, THROTTLE_TIME) } diff --git a/extensions/void/src/extension/ctrlK.ts b/extensions/void/src/extension/ctrlK.ts index 63aaf200..8b3f2ab6 100644 --- a/extensions/void/src/extension/ctrlK.ts +++ b/extensions/void/src/extension/ctrlK.ts @@ -1,35 +1,8 @@ import * as vscode from 'vscode'; import { AbortRef, OnFinalMessage, OnText, sendLLMMessage } from "../common/sendLLMMessage" import { VoidConfig } from '../webviews/common/contextForConfig'; -import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from '../common/systemPrompts'; -import { throttle } from 'lodash'; import { readFileContentOfUri } from './extensionLib/readFileContentOfUri'; -type Res = ((value: T) => void) - -const THRTOTLE_TIME = 100 // minimum time between edits -const LINES_PER_CHUNK = 20 // number of lines to search at a time - -const applyCtrlLChangesToFile = throttle( - ({ fileUri, newCurrentLine, oldCurrentLine, fullCompletedStr, oldFileStr, debug }: { fileUri: vscode.Uri, newCurrentLine: number, oldCurrentLine: number, fullCompletedStr: string, oldFileStr: string, debug?: string }) => { - - // write the change to the file - const WRITE_TO_FILE = ( - fullCompletedStr.split('\n').slice(0, newCurrentLine + 1).join('\n') // newFile[:newCurrentLine+1] - + oldFileStr.split('\n').slice(oldCurrentLine + 1).join('\n') // oldFile[oldCurrentLine+1:] - ) - const workspaceEdit = new vscode.WorkspaceEdit() - workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), WRITE_TO_FILE) - vscode.workspace.applyEdit(workspaceEdit) - - // highlight the `newCurrentLine` in white - // highlight the remaining part of the file in gray - - }, - THRTOTLE_TIME, { trailing: true } -) - - const applyCtrlK = async ({ fileUri, startLine, endLine, instructions, voidConfig, abortRef }: { fileUri: vscode.Uri, startLine: number, endLine: number, instructions: string, voidConfig: VoidConfig, abortRef: AbortRef }) => { const fileStr = await readFileContentOfUri(fileUri) @@ -47,24 +20,18 @@ const applyCtrlK = async ({ fileUri, startLine, endLine, instructions, voidConfi The user wants to apply the following instructions to the selection: ${instructions} -Please rewrite the selection following the user's instructions. - -Instructions to follow: +Instructions: 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 +4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake -Complete the following: +Please rewrite the complete the following code, following the instructions. \`\`\`
${prefix}
${suffix} `; - - // TODO initialize stream - - // update stream sendLLMMessage({ messages: [{ role: 'user', content: promptContent, }], onText: async (tokenStr, completionStr) => { @@ -97,4 +64,4 @@ Complete the following: -export { applyCtrlK } \ No newline at end of file +export { applyCtrlK } diff --git a/extensions/void/src/extension/extension.ts b/extensions/void/src/extension/extension.ts index cc7cbed6..700667cc 100644 --- a/extensions/void/src/extension/extension.ts +++ b/extensions/void/src/extension/extension.ts @@ -9,21 +9,24 @@ import { DiffProvider } from './DiffProvider'; import { readFileContentOfUri } from './extensionLib/readFileContentOfUri'; import { SidebarWebviewProvider } from './providers/SidebarWebviewProvider'; import { CtrlKWebviewProvider } from './providers/CtrlKWebviewProvider'; +import { AutocompleteProvider } from './AutcompleteProvider'; +import { runTreeSitter } from '../common/LangaugeServer/createJsProgramGraph'; -// this comes from vscode.proposed.editorInsets.d.ts -declare module 'vscode' { - export interface WebviewEditorInset { - readonly editor: vscode.TextEditor; - readonly line: number; - readonly height: number; - readonly webview: vscode.Webview; - readonly onDidDispose: Event; - dispose(): void; - } - export namespace window { - export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset; - } -} + +// // this comes from vscode.proposed.editorInsets.d.ts +// declare module 'vscode' { +// export interface WebviewEditorInset { +// readonly editor: vscode.TextEditor; +// readonly line: number; +// readonly height: number; +// readonly webview: vscode.Webview; +// readonly onDidDispose: Event; +// dispose(): void; +// } +// export namespace window { +// export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset; +// } +// } const roundRangeToLines = (selection: vscode.Selection) => { 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 @@ -112,7 +115,7 @@ export function activate(context: vscode.ExtensionContext) { // Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`) webview.onDidReceiveMessage(async (m: MessageFromSidebar) => { - const abortApplyRef: AbortRef = { current: null } + const abortRef: AbortRef = { current: null } if (m.type === 'requestFiles') { @@ -146,7 +149,7 @@ export function activate(context: vscode.ExtensionContext) { 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 }) + await applyDiffLazily({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffProvider, diffArea, abortRef: abortRef }) } else if (m.type === 'getPartialVoidConfig') { const partialVoidConfig = context.globalState.get('partialVoidConfig') ?? {} @@ -180,7 +183,21 @@ export function activate(context: vscode.ExtensionContext) { } ) + // 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); // Gets called when user presses ctrl + k (mounts ctrl+k-style codelens) diff --git a/extensions/void/src/webviews/common/contextForConfig.tsx b/extensions/void/src/webviews/common/contextForConfig.tsx index f2d02543..5288e51d 100644 --- a/extensions/void/src/webviews/common/contextForConfig.tsx +++ b/extensions/void/src/webviews/common/contextForConfig.tsx @@ -122,8 +122,8 @@ const voidConfigInfo: Record< // 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.2", "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 + '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.2", "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: { diff --git a/extensions/void/src/webviews/common/posthog.tsx b/extensions/void/src/webviews/common/posthog.tsx index 10690728..496ab598 100644 --- a/extensions/void/src/webviews/common/posthog.tsx +++ b/extensions/void/src/webviews/common/posthog.tsx @@ -1,12 +1,29 @@ import posthog from '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, +} + + export const identifyUser = (id: string) => { posthog.identify(id) } + + + export const captureEvent = (eventId: string, properties: object) => { - posthog.capture(eventId, properties) + posthog.capture(eventId, { ...properties, systemInfo }) } export const initPosthog = () => { @@ -17,4 +34,4 @@ export const initPosthog = () => { person_profiles: 'identified_only' // we only track events from identified users. We identify them in Sidebar } ) -} \ No newline at end of file +} diff --git a/extensions/void/tsconfig.json b/extensions/void/tsconfig.json index 63e3afa9..79aef824 100644 --- a/extensions/void/tsconfig.json +++ b/extensions/void/tsconfig.json @@ -1,7 +1,7 @@ { "include": [ "src/**/*" - ], +, "../../src/vs/workbench/contrib/welcomeGettingStarted/common/AutcompleteProvider.tsx" ], "exclude": [ "node_modules" ], @@ -27,4 +27,4 @@ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ } -} \ No newline at end of file +}