mirror of
https://github.com/voideditor/void
synced 2026-05-23 17:38:23 +00:00
Merge branch 'autocomplete'
This commit is contained in:
commit
bdd224990d
14 changed files with 1022 additions and 99 deletions
105
extensions/void/package-lock.json
generated
105
extensions/void/package-lock.json
generated
|
|
@ -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,7 +41,6 @@
|
|||
"eslint-plugin-react": "^7.35.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"globals": "^15.9.0",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^14.1.0",
|
||||
"ollama": "^0.5.9",
|
||||
"openai": "^4.68.4",
|
||||
|
|
@ -5036,12 +5041,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",
|
||||
|
|
@ -5106,11 +5105,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",
|
||||
|
|
@ -6029,7 +6029,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": {
|
||||
|
|
@ -6077,6 +6076,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",
|
||||
|
|
@ -6118,6 +6125,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",
|
||||
|
|
@ -6619,6 +6636,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",
|
||||
|
|
@ -7459,16 +7482,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",
|
||||
|
|
@ -8267,6 +8280,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",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@
|
|||
"properties": {}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "typeInspector.inspect",
|
||||
"title": "Inspect Types of All Variables"
|
||||
},
|
||||
{
|
||||
"command": "void.ctrl+l",
|
||||
"title": "Show Sidebar"
|
||||
|
|
@ -139,7 +143,6 @@
|
|||
"eslint-plugin-react": "^7.35.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"globals": "^15.9.0",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^14.1.0",
|
||||
"ollama": "^0.5.9",
|
||||
"openai": "^4.68.4",
|
||||
|
|
@ -155,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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,333 @@
|
|||
import * as vscode from 'vscode';
|
||||
import Parser from 'tree-sitter';
|
||||
import JavaScript from 'tree-sitter-javascript';
|
||||
|
||||
interface Definition {
|
||||
file: string;
|
||||
node: Parser.SyntaxNode;
|
||||
}
|
||||
|
||||
interface DefnUse {
|
||||
parent: Parser.SyntaxNode;
|
||||
file: string;
|
||||
}
|
||||
|
||||
interface ImportInfo {
|
||||
source: string;
|
||||
imported: string;
|
||||
}
|
||||
|
||||
class ProjectAnalyzer {
|
||||
private parser: Parser;
|
||||
private graph: Map<string, Set<string>>;
|
||||
private visited: Set<string>;
|
||||
private parsedFiles: Map<string, Parser.Tree>;
|
||||
private imports: Map<string, Map<string, ImportInfo>>;
|
||||
private definitions: Map<string, Definition>;
|
||||
private fileStack: Set<string>;
|
||||
|
||||
constructor() {
|
||||
this.parser = new Parser();
|
||||
this.parser.setLanguage(JavaScript);
|
||||
this.graph = new Map();
|
||||
this.visited = new Set();
|
||||
this.parsedFiles = new Map();
|
||||
this.imports = new Map();
|
||||
this.definitions = new Map();
|
||||
this.fileStack = new Set();
|
||||
}
|
||||
|
||||
async parseFile(filePath: string): Promise<Parser.Tree | null> {
|
||||
if (this.parsedFiles.has(filePath)) {
|
||||
return this.parsedFiles.get(filePath)!;
|
||||
}
|
||||
|
||||
if (this.fileStack.has(filePath)) {
|
||||
return null; // Circular import
|
||||
}
|
||||
|
||||
this.fileStack.add(filePath);
|
||||
|
||||
try {
|
||||
const uri = vscode.Uri.file(filePath);
|
||||
const document = await vscode.workspace.openTextDocument(uri);
|
||||
const code = document.getText();
|
||||
const tree = this.parser.parse(code);
|
||||
|
||||
this.parsedFiles.set(filePath, tree);
|
||||
this.collectImports(filePath, tree);
|
||||
this.collectDefinitions(filePath, tree);
|
||||
|
||||
return tree;
|
||||
} catch (error) {
|
||||
console.error(`Error parsing ${filePath}:`, error);
|
||||
return null;
|
||||
} finally {
|
||||
this.fileStack.delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private collectImports(filePath: string, tree: Parser.Tree): void {
|
||||
const fileImports = new Map<string, ImportInfo>();
|
||||
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'import_declaration') {
|
||||
const source = node.childForFieldName('source')?.text.slice(1, -1) ?? '';
|
||||
const specifiers = node.childForFieldName('specifiers');
|
||||
|
||||
specifiers?.children.forEach(spec => {
|
||||
if (spec.type === 'import_specifier') {
|
||||
const local = spec.childForFieldName('local')?.text ?? '';
|
||||
const imported = spec.childForFieldName('imported')?.text ?? '';
|
||||
fileImports.set(local, { source, imported });
|
||||
}
|
||||
});
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
visit(tree.rootNode);
|
||||
this.imports.set(filePath, fileImports);
|
||||
}
|
||||
|
||||
private collectDefinitions(filePath: string, tree: Parser.Tree): void {
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'function_declaration') {
|
||||
const name = node.childForFieldName('name')?.text ?? '';
|
||||
this.definitions.set(name, { file: filePath, node });
|
||||
}
|
||||
else if (node.type === 'variable_declarator') {
|
||||
const name = node.childForFieldName('name')?.text;
|
||||
const value = node.childForFieldName('value');
|
||||
if (name && (value?.type === 'arrow_function' || value?.type === 'function')) {
|
||||
this.definitions.set(name, { file: filePath, node: value });
|
||||
}
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
visit(tree.rootNode);
|
||||
}
|
||||
|
||||
private async getTypeFromPosition(uri: vscode.Uri, position: vscode.Position): Promise<string | null> {
|
||||
const hover = await vscode.commands.executeCommand<vscode.Hover[]>(
|
||||
'vscode.executeHoverProvider',
|
||||
uri,
|
||||
position
|
||||
);
|
||||
|
||||
if (hover?.[0]?.contents.length) {
|
||||
for (const content of hover[0].contents) {
|
||||
let hoverText = typeof content === 'string' ?
|
||||
content :
|
||||
('value' in content ? content.value : '');
|
||||
|
||||
// Remove typescript backticks if present
|
||||
hoverText = hoverText.replace(/```typescript\s*/, '').replace(/```\s*$/, '');
|
||||
console.log('Processing hover text:', hoverText);
|
||||
|
||||
// Extract the type information - look for the type after the colon
|
||||
const typeMatches = [
|
||||
/:\s*([\w<>]+)(?:\[\])?/, // matches "foo: Type" or "foo: Type[]"
|
||||
/var\s+\w+:\s*([\w<>]+)/, // matches "var foo: Type"
|
||||
/\(type\)\s+[\w<>]+:\s*([\w<>]+)/, // matches "(type) foo: Type"
|
||||
/\(method\)\s*([\w<>]+)\./ // matches "(method) Type.method"
|
||||
];
|
||||
|
||||
for (const pattern of typeMatches) {
|
||||
const match = pattern.exec(hoverText);
|
||||
if (match) {
|
||||
let type = match[1];
|
||||
// Handle array types
|
||||
if (hoverText.includes('[]')) {
|
||||
return 'Array';
|
||||
}
|
||||
// Extract base type from generics
|
||||
if (type.includes('<')) {
|
||||
type = type.split('<')[0];
|
||||
}
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getCallsInDefn(defnNode: Parser.SyntaxNode, currentFile: string): Promise<Set<string>> {
|
||||
const calls = new Set<string>();
|
||||
const fileImports = this.imports.get(currentFile) ?? new Map();
|
||||
const uri = vscode.Uri.file(currentFile);
|
||||
|
||||
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
|
||||
if (node.type === 'call_expression') {
|
||||
const callee = node.childForFieldName('function');
|
||||
if (callee?.type === 'identifier') {
|
||||
const name = callee.text;
|
||||
const importInfo = fileImports.get(name);
|
||||
if (importInfo) {
|
||||
calls.add(`${importInfo.source}:${importInfo.imported}`);
|
||||
} else {
|
||||
calls.add(name);
|
||||
}
|
||||
}
|
||||
else if (callee?.type === 'member_expression') {
|
||||
const method = callee.childForFieldName('property')?.text;
|
||||
const object = callee.childForFieldName('object');
|
||||
|
||||
if (method && object) {
|
||||
const position = new vscode.Position(
|
||||
object.startPosition.row,
|
||||
object.startPosition.column
|
||||
);
|
||||
|
||||
const type = await this.getTypeFromPosition(uri, position);
|
||||
if (type) {
|
||||
calls.add(`${type}.${method}`);
|
||||
} else {
|
||||
calls.add(`method:${method}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of node.children) {
|
||||
await visit(child);
|
||||
}
|
||||
};
|
||||
|
||||
await visit(defnNode);
|
||||
return calls;
|
||||
}
|
||||
|
||||
private gotoDefn(name: string): Definition | null {
|
||||
if (name.includes(':')) {
|
||||
const [file, funcName] = name.split(':');
|
||||
const def = this.definitions.get(funcName);
|
||||
return def ?? null;
|
||||
}
|
||||
|
||||
return this.definitions.get(name) ?? null;
|
||||
}
|
||||
|
||||
private getUses(defnNode: Parser.SyntaxNode, currentFile: string): DefnUse[] {
|
||||
const uses: DefnUse[] = [];
|
||||
|
||||
let fnName: string | undefined;
|
||||
if (defnNode.type === 'function_declaration') {
|
||||
fnName = defnNode.childForFieldName('name')?.text;
|
||||
} else if (defnNode.type === 'arrow_function' || defnNode.type === 'function') {
|
||||
const parent = defnNode.parent;
|
||||
if (parent?.type === 'variable_declarator') {
|
||||
fnName = parent.childForFieldName('name')?.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fnName) return uses;
|
||||
|
||||
for (const [file, tree] of this.parsedFiles) {
|
||||
const visit = (node: Parser.SyntaxNode): void => {
|
||||
if (node.type === 'call_expression') {
|
||||
const callee = node.childForFieldName('function');
|
||||
if (callee?.type === 'identifier' && callee.text === fnName) {
|
||||
let current: Parser.SyntaxNode | null = node;
|
||||
while (current) {
|
||||
if (current.type === 'function_declaration' ||
|
||||
current.type === 'arrow_function' ||
|
||||
current.type === 'function') {
|
||||
uses.push({ parent: current, file });
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
visit(tree.rootNode);
|
||||
}
|
||||
|
||||
return uses;
|
||||
}
|
||||
|
||||
private async visitAllNodesInGraphFromDefinition(defn: Parser.SyntaxNode, currentFile: string): Promise<void> {
|
||||
let defnName: string | undefined;
|
||||
if (defn.type === 'function_declaration') {
|
||||
defnName = defn.childForFieldName('name')?.text;
|
||||
} else if (defn.type === 'arrow_function' || defn.type === 'function') {
|
||||
const parent = defn.parent;
|
||||
if (parent?.type === 'variable_declarator') {
|
||||
defnName = parent.childForFieldName('name')?.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defnName) return;
|
||||
|
||||
const fullName = `${currentFile}:${defnName}`;
|
||||
if (this.visited.has(fullName)) return;
|
||||
|
||||
const calls = await this.getCallsInDefn(defn, currentFile);
|
||||
this.graph.set(fullName, calls);
|
||||
this.visited.add(fullName);
|
||||
|
||||
const callDefns = Array.from(calls).map(call => this.gotoDefn(call));
|
||||
for (const callDefn of callDefns) {
|
||||
if (callDefn) {
|
||||
await this.visitAllNodesInGraphFromDefinition(callDefn.node, callDefn.file);
|
||||
}
|
||||
}
|
||||
|
||||
const defnUses = this.getUses(defn, currentFile);
|
||||
for (const defnUse of defnUses) {
|
||||
await this.visitAllNodesInGraphFromDefinition(defnUse.parent, defnUse.file);
|
||||
}
|
||||
}
|
||||
|
||||
async analyze(entryFile: string): Promise<Map<string, Set<string>>> {
|
||||
const tree = await this.parseFile(entryFile);
|
||||
if (!tree) return new Map();
|
||||
|
||||
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
|
||||
if (node.type === 'function_declaration') {
|
||||
await this.visitAllNodesInGraphFromDefinition(node, entryFile);
|
||||
}
|
||||
else if (node.type === 'variable_declarator') {
|
||||
const value = node.childForFieldName('value');
|
||||
if (value?.type === 'arrow_function' || value?.type === 'function') {
|
||||
await this.visitAllNodesInGraphFromDefinition(value, entryFile);
|
||||
}
|
||||
}
|
||||
for (const child of node.children) {
|
||||
await visit(child);
|
||||
}
|
||||
};
|
||||
|
||||
await visit(tree.rootNode);
|
||||
return this.graph;
|
||||
}
|
||||
}
|
||||
|
||||
export async function runTreeSitter(filePath?: string): Promise<Map<string, Set<string>> | null> {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor && !filePath) {
|
||||
vscode.window.showWarningMessage('No active editor found');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const targetPath = filePath ?? editor!.document.uri.fsPath;
|
||||
const analyzer = new ProjectAnalyzer();
|
||||
const graph = await analyzer.analyze(targetPath);
|
||||
|
||||
for (const [defn, calls] of graph) {
|
||||
console.log(`${defn} calls: ${[...calls].join(', ')}`);
|
||||
}
|
||||
|
||||
return graph;
|
||||
} catch (error) {
|
||||
console.error('Error analyzing file:', error);
|
||||
vscode.window.showErrorMessage('Error analyzing file');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
62
extensions/void/src/common/LangaugeServer/findFunctions.ts
Normal file
62
extensions/void/src/common/LangaugeServer/findFunctions.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import * as vscode from 'vscode';
|
||||
|
||||
const legend = new vscode.SemanticTokensLegend([], []);
|
||||
|
||||
export async function findFunctions() {
|
||||
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
const document = editor.document;
|
||||
|
||||
const tokens = await vscode.commands.executeCommand<vscode.SemanticTokens>(
|
||||
'vscode.provideDocumentSemanticTokens',
|
||||
document.uri
|
||||
);
|
||||
|
||||
if (!tokens) {
|
||||
console.error('No tokens found');
|
||||
return [];
|
||||
}
|
||||
|
||||
const allTokens = decodeTokens(tokens, document);
|
||||
|
||||
|
||||
return allTokens;
|
||||
}
|
||||
|
||||
function decodeTokens(tokens: vscode.SemanticTokens, document: vscode.TextDocument) {
|
||||
const data = tokens.data;
|
||||
const decodedTokens = [];
|
||||
let line = 0;
|
||||
let character = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i += 5) {
|
||||
const deltaLine = data[i];
|
||||
const deltaStartChar = data[i + 1];
|
||||
const length = data[i + 2];
|
||||
const tokenTypeIdx = data[i + 3];
|
||||
const tokenModifierIdx = data[i + 4];
|
||||
|
||||
line += deltaLine;
|
||||
character = deltaLine === 0 ? character + deltaStartChar : deltaStartChar;
|
||||
|
||||
const type = legend.tokenTypes[tokenTypeIdx] || `(${tokenTypeIdx})`;
|
||||
const modifier = legend.tokenModifiers[tokenModifierIdx] || `(${tokenModifierIdx})`;
|
||||
|
||||
const tokenRange = new vscode.Range(line, character, line, character + length);
|
||||
const tokenText = document.getText(tokenRange);
|
||||
|
||||
decodedTokens.push({
|
||||
line,
|
||||
startCharacter: character,
|
||||
length,
|
||||
type,
|
||||
modifier,
|
||||
text: tokenText,
|
||||
});
|
||||
|
||||
console.log(`Token: '${tokenText}' | Type: ${type} | Modifier: ${modifier} | Line: ${line}, Character: ${character}`);
|
||||
}
|
||||
|
||||
return decodedTokens;
|
||||
}
|
||||
32
extensions/void/src/common/SimpleLruCache.ts
Normal file
32
extensions/void/src/common/SimpleLruCache.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { LRUCache } from 'lru-cache';
|
||||
|
||||
const DEFAULT_MAX_SIZE = 20
|
||||
|
||||
|
||||
export class SimpleLRUCache<T extends {}> {
|
||||
private cache: LRUCache<number, T>;
|
||||
private maxSize: number
|
||||
public length: number
|
||||
|
||||
constructor(maxSize?: number) {
|
||||
|
||||
maxSize = maxSize ?? DEFAULT_MAX_SIZE
|
||||
|
||||
this.cache = new LRUCache<number, T>({ max: maxSize });
|
||||
this.length = 0
|
||||
this.maxSize = maxSize
|
||||
}
|
||||
|
||||
push(value: T): void {
|
||||
const key = this.cache.size;
|
||||
this.cache.set(key, value);
|
||||
this.length++
|
||||
this.length = Math.min(this.length, this.maxSize)
|
||||
}
|
||||
|
||||
values() {
|
||||
return this.cache.values()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
105
extensions/void/src/common/getPrompt.ts
Normal file
105
extensions/void/src/common/getPrompt.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { configFields, VoidConfig } from "../webviews/common/contextForConfig"
|
||||
import { FimInfo } from "./sendLLMMessage"
|
||||
|
||||
|
||||
type GetFIMPrompt = ({ voidConfig, fimInfo }: { voidConfig: VoidConfig, fimInfo: FimInfo, }) => string
|
||||
|
||||
export const getFIMSystem: GetFIMPrompt = ({ voidConfig, fimInfo }) => {
|
||||
|
||||
switch (voidConfig.default.whichApi) {
|
||||
case 'ollama':
|
||||
return ''
|
||||
case 'anthropic':
|
||||
case 'openAI':
|
||||
case 'gemini':
|
||||
case 'greptile':
|
||||
case 'openRouter':
|
||||
case 'openAICompatible':
|
||||
case 'azure':
|
||||
default:
|
||||
return `You are given the START and END to a piece of code. Please FILL IN THE MIDDLE between the START and END.
|
||||
|
||||
Instruction summary:
|
||||
1. Return the MIDDLE of the code between the START and END.
|
||||
2. Do not give an explanation, description, or any other code besides the middle.
|
||||
2. Do not return duplicate code from either START or END.
|
||||
3. Make sure the MIDDLE piece of code has balanced brackets that match the START and END.
|
||||
4. The MIDDLE begins on the same line as START. Please include a newline character if you want to begin on the next line.
|
||||
|
||||
# EXAMPLE
|
||||
|
||||
## START:
|
||||
\`\`\` python
|
||||
def add(a,b):
|
||||
return a + b
|
||||
def subtract(a,b):
|
||||
return a - b
|
||||
\`\`\`
|
||||
## END:
|
||||
\`\`\` python
|
||||
def divide(a,b):
|
||||
return a / b
|
||||
\`\`\`
|
||||
## EXPECTED OUTPUT:
|
||||
\`\`\` python
|
||||
|
||||
def multiply(a,b):
|
||||
return a * b
|
||||
\`\`\`
|
||||
|
||||
# EXAMPLE
|
||||
## START:
|
||||
\`\`\` javascript
|
||||
const x = 1
|
||||
|
||||
const y
|
||||
\`\`\`
|
||||
## END:
|
||||
\`\`\` javascript
|
||||
|
||||
const z = 3
|
||||
\`\`\`
|
||||
## EXPECTED OUTPUT:
|
||||
\`\`\` javascript
|
||||
= 2
|
||||
\`\`\`
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const getFIMPrompt: GetFIMPrompt = ({ voidConfig, fimInfo }) => {
|
||||
|
||||
// if no prefix or suffix, return empty string
|
||||
if (!fimInfo.prefix.trim() && !fimInfo.suffix.trim()) return ''
|
||||
|
||||
// TODO may want to trim the prefix and suffix
|
||||
switch (voidConfig.default.whichApi) {
|
||||
case 'ollama':
|
||||
if (voidConfig.ollama.model === 'codestral') {
|
||||
return `[SUFFIX]${fimInfo.suffix}[PREFIX] ${fimInfo.prefix}`
|
||||
}
|
||||
return ''
|
||||
case 'anthropic':
|
||||
case 'openAI':
|
||||
case 'gemini':
|
||||
case 'greptile':
|
||||
case 'openRouter':
|
||||
case 'openAICompatible':
|
||||
case 'azure':
|
||||
default:
|
||||
return `## START:
|
||||
\`\`\`
|
||||
${fimInfo.prefix}
|
||||
\`\`\`
|
||||
## END:
|
||||
\`\`\`
|
||||
${fimInfo.suffix}
|
||||
\`\`\`
|
||||
`
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3,6 +3,7 @@ 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 { getFIMPrompt, getFIMSystem } from './getPrompt';
|
||||
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
||||
|
|
@ -21,23 +22,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)
|
||||
|
|
@ -232,7 +242,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 = ""
|
||||
|
|
@ -243,6 +253,10 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
|
|||
|
||||
const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
|
||||
|
||||
type GenerateResponse = Awaited<ReturnType<(typeof ollama.generate)>>
|
||||
type ChatResponse = Awaited<ReturnType<(typeof ollama.chat)>>
|
||||
|
||||
|
||||
// First check if model exists
|
||||
ollama.list()
|
||||
.then(async models => {
|
||||
|
|
@ -256,6 +270,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,
|
||||
|
|
@ -271,7 +297,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);
|
||||
}
|
||||
|
|
@ -357,26 +387,49 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
|
|||
|
||||
}
|
||||
|
||||
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 });
|
||||
return sendGreptileMsg({ mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
|
||||
default:
|
||||
onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
276
extensions/void/src/extension/AutcompleteProvider.ts
Normal file
276
extensions/void/src/extension/AutcompleteProvider.ts
Normal file
|
|
@ -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<string> | undefined,
|
||||
result: string,
|
||||
}
|
||||
|
||||
const DEBOUNCE_TIME = 300
|
||||
const TIMEOUT_TIME = 60000
|
||||
|
||||
// postprocesses the result
|
||||
const postprocessResult = (result: string) => {
|
||||
|
||||
// remove leading whitespace from result
|
||||
return result.trimStart()
|
||||
|
||||
}
|
||||
|
||||
const extractCodeFromResult = (result: string) => {
|
||||
|
||||
// extract the code between triple backticks
|
||||
const parts = result.split(/```/);
|
||||
|
||||
// if there is no ``` then return the raw result
|
||||
if (parts.length === 1) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// else return the code between the triple backticks
|
||||
return parts[1]
|
||||
|
||||
}
|
||||
|
||||
// trims the end of the prefix to improve cache hit rate
|
||||
const trimPrefix = (prefix: string) => {
|
||||
const trimmedPrefix = prefix.trimEnd()
|
||||
const trailingEnd = prefix.substring(trimmedPrefix.length)
|
||||
|
||||
// keep only a single trailing newline
|
||||
if (trailingEnd.includes('\n')) {
|
||||
return trimmedPrefix + '\n'
|
||||
}
|
||||
|
||||
// else ignore all spaces and return the trimmed prefix
|
||||
return trimmedPrefix
|
||||
}
|
||||
|
||||
// finds the text in the autocompletion to display, assuming the prefix is already matched
|
||||
// example:
|
||||
// originalPrefix = abcd
|
||||
// generatedMiddle = efgh
|
||||
// originalSuffix = ijkl
|
||||
// the user has typed "ef" so prefix = abcdef
|
||||
// we want to return the rest of the generatedMiddle, which is "gh"
|
||||
const toInlineCompletion = ({ prefix, autocompletion }: { 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<Autocompletion> } = {}
|
||||
|
||||
private _lastTime = 0
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this._extensionContext = context
|
||||
}
|
||||
|
||||
// used internally by vscode
|
||||
// fires after every keystroke
|
||||
async provideInlineCompletionItems(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
context: vscode.InlineCompletionContext,
|
||||
token: vscode.CancellationToken,
|
||||
): Promise<vscode.InlineCompletionItem[]> {
|
||||
|
||||
const 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 []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T> = ((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.
|
||||
\`\`\`
|
||||
<PRE>${prefix}</PRE>
|
||||
<SUF>${suffix}</SUF>
|
||||
<MID>`;
|
||||
|
||||
|
||||
// TODO initialize stream
|
||||
|
||||
// update stream
|
||||
sendLLMMessage({
|
||||
messages: [{ role: 'user', content: promptContent, }],
|
||||
onText: async (tokenStr, completionStr) => {
|
||||
|
|
@ -97,4 +64,4 @@ Complete the following:
|
|||
|
||||
|
||||
|
||||
export { applyCtrlK }
|
||||
export { applyCtrlK }
|
||||
|
|
|
|||
|
|
@ -9,21 +9,23 @@ 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<void>;
|
||||
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<void>;
|
||||
// dispose(): void;
|
||||
// }
|
||||
// export namespace window {
|
||||
// export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset;
|
||||
// }
|
||||
// }
|
||||
|
||||
const roundRangeToLines = (selection: vscode.Selection) => {
|
||||
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 +114,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 +148,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 +182,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)
|
||||
|
|
|
|||
|
|
@ -121,8 +121,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: {
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue