Compare commits

..

14 Commits

Author SHA1 Message Date
mp 53d19d819c autocomplete UX 2024-11-21 05:58:43 -08:00
mp cd77542a9e Autocomplete (max number of pending requests, filter for matchup with user's text, better prompt) 2024-11-19 03:57:06 -08:00
mp 6df0093786 ui 2024-11-18 17:13:55 -08:00
mp 0ca2ad4930 Create basic language server functionality 2024-11-17 16:48:23 -08:00
mp a765d738e1 Basic LSP Usage 2024-11-14 18:08:52 -08:00
mp 19a8d36e07 Add LRU cache 2024-11-12 02:31:26 -08:00
mp 339aff5d31 Refactor sendLLMMessage and add FIM mode 2024-11-12 01:36:32 -08:00
mp aab065139e Trim prefixes to increase cache hit rate 2024-11-10 23:52:25 -08:00
mp 652f64cd14 Add basic system prompt 2024-11-08 21:57:33 -08:00
mp 9b878a9fc0 Merge remote-tracking branch 'origin/main' into autocomplete 2024-11-08 01:28:19 -08:00
mp 268d03fad5 Add working autocomplete with caching 2024-11-08 01:23:09 -08:00
Andrew Pareles 791f6b1151 Merge pull request #140 from Madhav160804/ollama-model-selection
Added Ollama Model Check for installed models
2024-11-04 14:44:29 -08:00
Madhav160804 1c739e521d ollama error handling 2024-11-05 02:49:24 +05:30
Madhav Dhingra 794f35fc37 added ollama model check 2024-11-05 02:30:30 +05:30
32 changed files with 1775 additions and 971 deletions
+1 -1
View File
@@ -147,7 +147,7 @@ We're always glad to talk about new ideas, help you get set up, and make sure yo
## Submitting a Pull Request
Please submit a pull request once you've made a change. You don't need to submit an issue.
Please submit a pull request once you've made a change. You don't need to submit an issue.
Please don't use AI to write your PR 🙂.
+112 -37
View File
@@ -7,8 +7,14 @@
"": {
"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.31.0",
"@anthropic-ai/sdk": "^0.29.2",
"@eslint/js": "^9.9.1",
"@google/generative-ai": "^0.21.0",
"@monaco-editor/react": "^4.6.0",
@@ -23,6 +29,7 @@
"@types/react-dom": "^18.3.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/uuid": "^10.0.0",
"@types/vscode": "1.92.0",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"@vscode/test-cli": "^0.0.10",
@@ -34,10 +41,9 @@
"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.70.2",
"openai": "^4.68.4",
"postcss": "^8.4.41",
"posthog-js": "^1.176.0",
"react": "^18.3.1",
@@ -52,7 +58,7 @@
"uuid": "^10.0.0"
},
"engines": {
"vscode": "*"
"vscode": "^1.92.0"
}
},
"node_modules/@alloc/quick-lru": {
@@ -69,9 +75,9 @@
}
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.31.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.31.0.tgz",
"integrity": "sha512-9EX90YMUtj0d1aHPsnjgurUWAUoNQA/kMaN+UUN7eL3jhl1cijBIGKHQPrR4/ctvD9A065QnzzJDy5Oxb/Bk8A==",
"version": "0.29.2",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.29.2.tgz",
"integrity": "sha512-5dwiOPO/AZvhY4bJIG9vjFKU9Kza3hA6VEsbIQg6L9vny2RQIpCFhV50nB9IrG2edZaHZb4HuQ9Wmsn5zgWyZg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -85,9 +91,9 @@
}
},
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
"version": "18.19.64",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.64.tgz",
"integrity": "sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ==",
"version": "18.19.59",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.59.tgz",
"integrity": "sha512-vizm2EqwV/7Zay+A6J3tGl9Lhr7CjZe2HmWS988sefiEmsyP9CeXEleho6i4hJk/8UtZAo0bWN4QPZZr83RxvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1256,6 +1262,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/vscode": {
"version": "1.92.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.92.0.tgz",
"integrity": "sha512-DcZoCj17RXlzB4XJ7IfKdPTcTGDLYvTOcTNkvtjXWF+K2TlKzHHkBEXNWQRpBIXixNEUgx39cQeTFunY0E2msw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/yargs": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
@@ -5028,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",
@@ -5098,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",
@@ -6016,6 +6024,13 @@
"node": ">=10"
}
},
"node_modules/monaco-editor": {
"version": "0.52.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz",
"integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==",
"dev": true,
"peer": true
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -6061,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",
@@ -6102,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",
@@ -6280,9 +6313,9 @@
}
},
"node_modules/openai": {
"version": "4.70.2",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.70.2.tgz",
"integrity": "sha512-Q2ymi/KPUYv+LJ9rFxeYxpkVAhcrZFTVvnJbdF1pUHg9eMC6lY8PU4TO1XOK5UZzOZuuVicouRwVMi1iDrT4qw==",
"version": "4.68.4",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.68.4.tgz",
"integrity": "sha512-LRinV8iU9VQplkr25oZlyrsYGPGasIwYN8KFMAAFTHHLHjHhejtJ5BALuLFrkGzY4wfbKhOhuT+7lcHZ+F3iEA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -6307,9 +6340,9 @@
}
},
"node_modules/openai/node_modules/@types/node": {
"version": "18.19.64",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.64.tgz",
"integrity": "sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ==",
"version": "18.19.59",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.59.tgz",
"integrity": "sha512-vizm2EqwV/7Zay+A6J3tGl9Lhr7CjZe2HmWS988sefiEmsyP9CeXEleho6i4hJk/8UtZAo0bWN4QPZZr83RxvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6603,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",
@@ -7443,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",
@@ -8251,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",
+14 -4
View File
@@ -5,7 +5,7 @@
"description": "",
"version": "0.0.1",
"engines": {
"vscode": "*"
"vscode": "^1.92.0"
},
"categories": [
"Other"
@@ -21,6 +21,10 @@
"properties": {}
},
"commands": [
{
"command": "typeInspector.inspect",
"title": "Inspect Types of All Variables"
},
{
"command": "void.ctrl+l",
"title": "Show Sidebar"
@@ -112,7 +116,7 @@
"test": "vscode-test"
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.31.0",
"@anthropic-ai/sdk": "^0.29.2",
"@eslint/js": "^9.9.1",
"@google/generative-ai": "^0.21.0",
"@monaco-editor/react": "^4.6.0",
@@ -127,6 +131,7 @@
"@types/react-dom": "^18.3.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/uuid": "^10.0.0",
"@types/vscode": "1.92.0",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"@vscode/test-cli": "^0.0.10",
@@ -138,10 +143,9 @@
"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.70.2",
"openai": "^4.68.4",
"postcss": "^8.4.41",
"posthog-js": "^1.176.0",
"react": "^18.3.1",
@@ -154,5 +158,11 @@
"typescript": "5.5.4",
"typescript-eslint": "^8.3.0",
"uuid": "^10.0.0"
},
"dependencies": {
"lru-cache": "^11.0.2",
"tree-sitter": "^0.21.1",
"tree-sitter-javascript": "^0.23.1",
"tree-sitter-python": "^0.23.4"
}
}
@@ -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;
}
}
@@ -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;
}
+114
View File
@@ -0,0 +1,114 @@
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.
3. Do not return duplicate code from either START or END.
4. Make sure the MIDDLE piece of code has balanced brackets that match the START and END.
5. The MIDDLE begins on the same line as START. Please include a newline character if you want to begin on the next line.
6. Around 90% of the time, you should return just one or a few lines of code. You should keep your outputs short unless you are confident the user is trying to write boilderplate code.
# 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 }) => {
const { prefix: fullPrefix, suffix: fullSuffix } = fimInfo
const prefix = fullPrefix.split('\n').slice(-20).join('\n')
const suffix = fullSuffix.split('\n').slice(0, 20).join('\n')
console.log('prefix', JSON.stringify(prefix))
console.log('suffix', JSON.stringify(suffix))
if (!prefix.trim() && !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]${suffix}[PREFIX] ${prefix}`
} else if (voidConfig.ollama.model.includes('qwen')) {
return `<|fim_prefix|>${prefix}<|fim_suffix|>${suffix}<|fim_middle|>`
}
return ''
case 'anthropic':
case 'openAI':
case 'gemini':
case 'greptile':
case 'openRouter':
case 'openAICompatible':
case 'azure':
default:
return `## START:
\`\`\`
${prefix}
\`\`\`
## END:
\`\`\`
${suffix}
\`\`\`
`
}
}
+176 -127
View File
@@ -3,58 +3,65 @@ 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 { captureEvent } from '../webviews/common/posthog';
import { ChatMessage } from './shared_types';
import { getFIMPrompt, getFIMSystem } from './getPrompt';
export type AbortRef = { current: (() => void) | null }
export type AbortRef = { current: (() => void) }
export type OnText = (newText: string, fullText: string) => void
export type OnFinalMessage = (input: string) => void
export type LLMMessageAnthropic = {
role: 'user' | 'assistant';
content: string;
role: 'user' | 'assistant',
content: string,
}
export type LLMMessage = {
role: 'system' | 'user' | 'assistant';
content: string;
role: 'system' | 'user' | 'assistant',
content: string,
}
type LLMMessageOptions = { stopTokens?: string[] }
type SendLLMMessageFnTypeInternal = (params: {
messages: LLMMessage[];
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: (error: string) => void;
voidConfig: VoidConfig;
_setAborter: (aborter: () => void) => void;
mode: 'chat' | 'fim',
messages: LLMMessage[],
options?: LLMMessageOptions,
onText: OnText,
onFinalMessage: OnFinalMessage,
onError: (error: string) => void,
abortRef: AbortRef,
voidConfig: VoidConfig,
}) => void
type SendLLMMessageFnTypeExternal = (params: {
messages: LLMMessage[];
onText: OnText;
onFinalMessage: (fullText: string) => void;
onError: (error: string) => void;
voidConfig: VoidConfig | null;
abortRef: AbortRef;
logging: {
loggingName: string,
};
type SendLLMMessageFnTypeExternal = (params: (
| { mode?: 'chat', messages: LLMMessage[], fimInfo?: undefined, }
| { mode: 'fim', messages?: undefined, fimInfo: FimInfo, }
) & {
options?: LLMMessageOptions,
onText: OnText,
onFinalMessage: OnFinalMessage,
onError: (error: string) => void,
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
const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
let int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
if (Number.isNaN(int))
return undefined
return int
}
// Anthropic
const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
@@ -74,16 +81,19 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi
max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user
});
let did_abort = false
// when receive text
stream.on('text', (newText, fullText) => {
if (did_abort) return
onText(newText, fullText)
})
// when we get the final message on this stream (or when error/fail)
stream.on('finalMessage', (claude_response) => {
if (did_abort) return
// stringify the response's content
const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n');
let content = claude_response.content.map(c => { if (c.type === 'text') { return c.text } }).join('\n');
onFinalMessage(content)
})
@@ -97,22 +107,31 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi
}
})
// TODO need to test this to make sure it works, it might throw an error
_setAborter(() => stream.controller.abort())
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
const abort = () => {
did_abort = true
stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error
}
return { abort }
};
// Gemini
const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let didAbort = false
let fullText = ''
abortRef.current = () => {
didAbort = true
}
const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey);
const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model });
// remove system messages that get sent to Gemini
// str of all system messages
const systemMessage = messages
let systemMessage = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n');
@@ -127,9 +146,12 @@ const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, o
model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
.then(async response => {
_setAborter(() => response.stream.return(fullText))
abortRef.current = () => {
// response.stream.return(fullText)
didAbort = true;
}
for await (const chunk of response.stream) {
if (didAbort) return;
const newText = chunk.text();
fullText += newText;
onText(newText, fullText);
@@ -152,14 +174,20 @@ const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, o
}
// OpenAI, OpenRouter, OpenAICompatible
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let didAbort = false
let fullText = ''
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
abortRef.current = () => {
didAbort = true;
};
let openai: OpenAI
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
const maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
let maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
if (voidConfig.default.whichApi === 'openAI') {
openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true });
@@ -167,10 +195,10 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
}
else if (voidConfig.default.whichApi === 'openRouter') {
openai = new OpenAI({
baseURL: 'https://openrouter.ai/api/v1', apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true,
baseURL: "https://openrouter.ai/api/v1", apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true,
defaultHeaders: {
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
"HTTP-Referer": 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
"X-Title": 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
},
});
options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
@@ -187,9 +215,13 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
openai.chat.completions
.create(options)
.then(async response => {
_setAborter(() => response.controller.abort())
abortRef.current = () => {
// response.controller.abort()
didAbort = true;
}
// when receive text
for await (const chunk of response) {
if (didAbort) return;
const newText = chunk.choices[0]?.delta?.content || '';
fullText += newText;
onText(newText, fullText);
@@ -214,50 +246,104 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
};
// Ollama
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let fullText = ''
let didAbort = false
let fullText = ""
const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
ollama.chat({
model: voidConfig.ollama.model,
messages: messages,
stream: true,
options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens
})
abortRef.current = () => {
didAbort = true;
};
type GenerateResponse = Awaited<ReturnType<(typeof ollama.generate)>>
type ChatResponse = Awaited<ReturnType<(typeof ollama.chat)>>
// First check if model exists
ollama.list()
.then(async models => {
const installedModels = models.models.map(m => m.name.replace(/:latest$/, ''))
const modelExists = installedModels.some(m => m.startsWith(voidConfig.ollama.model));
if (!modelExists) {
const errorMessage = `The model "${voidConfig.ollama.model}" is not available locally. Please run 'ollama pull ${voidConfig.ollama.model}' to download it first or
try selecting one from the Installed models: ${installedModels.join(', ')}`;
onText(errorMessage, errorMessage);
onFinalMessage(errorMessage);
return Promise.reject();
}
if (mode === 'fim') {
// the fim prompt is the last message
let prompt = messages[messages.length - 1].content
return ollama.generate({
model: voidConfig.ollama.model,
prompt: prompt,
stream: true,
raw: true,
options: { stop: options?.stopTokens }
})
}
return ollama.chat({
model: voidConfig.ollama.model,
messages: messages,
stream: true,
options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) }
});
})
.then(async stream => {
_setAborter(() => stream.abort())
// iterate through the stream
if (!stream) return;
abortRef.current = () => {
didAbort = true
stream.abort()
}
for await (const chunk of stream) {
const newText = chunk.message.content;
if (didAbort) return;
const newText = (mode === 'fim'
? (chunk as GenerateResponse).response
: (chunk as ChatResponse).message.content
)
fullText += newText;
onText(newText, fullText);
}
onFinalMessage(fullText);
})
// when error/fail
.catch(error => {
onError(error)
})
// Check if the error is a connection error
if (error instanceof Error && error.message.includes('Failed to fetch')) {
const errorMessage = 'Ollama service is not running. Please start the Ollama service and try again.';
onText(errorMessage, errorMessage);
onFinalMessage(errorMessage);
} else if (error) {
onError(error);
}
});
};
// Greptile
// https://docs.greptile.com/api-reference/query
// https://docs.greptile.com/quickstart#sample-response-streamed
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let didAbort = false
let fullText = ''
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
abortRef.current = () => {
didAbort = true
}
fetch('https://api.greptile.com/v2/query', {
method: 'POST',
headers: {
'Authorization': `Bearer ${voidConfig.greptile.apikey}`,
'X-Github-Token': `${voidConfig.greptile.githubPAT}`,
'Content-Type': `application/json`,
"Authorization": `Bearer ${voidConfig.greptile.apikey}`,
"X-Github-Token": `${voidConfig.greptile.githubPAT}`,
"Content-Type": `application/json`,
},
body: JSON.stringify({
messages,
@@ -272,10 +358,12 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
})
// TODO make this actually stream, right now it just sends one message at the end
// TODO add _setAborter() when add streaming
.then(async responseArr => {
if (didAbort)
return
for (let response of responseArr) {
for (const response of responseArr) {
const type: string = response['type']
const message = response['message']
@@ -285,7 +373,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
onText(message, fullText)
}
else if (type === 'sources') {
const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
const { filepath, linestart, lineend } = message as { filepath: string, linestart: number | null, lineend: number | null }
fullText += filepath
onText(filepath, fullText)
}
@@ -304,90 +392,51 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
}
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ options, 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)
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({
messages,
onText: onText_,
onFinalMessage: onFinalMessage_,
onError: onError_,
abortRef: abortRef_,
voidConfig,
logging: { loggingName }
}) => {
if (!voidConfig) return;
}
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
.filter(m => m.content !== '')
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureChatEvent = (eventId: string, extras?: object) => {
captureEvent(eventId, {
whichApi: voidConfig.default['whichApi'],
numMessages: messages?.length,
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
version: '2024-11-02',
...extras,
})
}
const submit_time = new Date()
let _fullTextSoFar = ''
let _aborter: (() => void) | null = null
let _setAborter = (fn: () => void) => { _aborter = fn }
let _didAbort = false
const onText = (newText: string, fullText: string) => {
if (_didAbort) return
onText_(newText, fullText)
_fullTextSoFar = fullText
}
const onFinalMessage = (fullText: string) => {
if (_didAbort) return
captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
onFinalMessage_(fullText)
}
const onError = (error: string) => {
if (_didAbort) return
captureChatEvent(`${loggingName} - Error`, { error })
onError_(error)
}
const onAbort = () => {
captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
_aborter?.()
_didAbort = true
}
abortRef_.current = onAbort
captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
if (messages.length === 0)
return onError('No messages provided to LLM.');
switch (voidConfig.default.whichApi) {
case 'anthropic':
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
return sendAnthropicMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'openAI':
case 'openRouter':
case 'openAICompatible':
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
return sendOpenAIMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'gemini':
sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
return sendGeminiMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'ollama':
sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
return sendOllamaMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'greptile':
sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
return sendGreptileMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
default:
onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
break;
}
}
@@ -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,
@@ -0,0 +1,467 @@
import * as vscode from 'vscode';
import { AbortRef, LLMMessage, sendLLMMessage } from '../common/sendLLMMessage';
import { getVoidConfigFromPartial, VoidConfig } from '../webviews/common/contextForConfig';
import { LRUCache } from 'lru-cache';
/*
A summary of autotab:
Postprocessing
-one common problem for all models is outputting unbalanced parentheses
we solve this by trimming all extra closing parentheses from the generated string
in future, should make sure parentheses are always balanced
-another problem is completing the middle of a string, eg. "const [x, CURSOR] = useState()"
we complete up to first matchup character
but should instead complete the whole line / block (difficult because of parenthesis accuracy)
-too much info is bad. usually we want to show the user 1 line, and have a preloaded response afterwards
this should happen automatically with caching system
should break preloaded responses into \n\n chunks
Preprocessing
- we don't generate if cursor is at end / beginning of a line (no spaces)
- we generate 1 line if there is text to the right of cursor
- we generate 1 line if variable declaration
- (in many cases want to show 1 line but generate multiple)
State
- cache based on prefix (and do some trimming first)
- when press tab on one line, should have an immediate followup response
to do this, show autocompletes before they're fully finished
- [todo] remove each autotab when accepted
- [todo] treat windows \r\n separately from \n
!- [todo] provide type information
Details
-generated results are trimmed up to 1 leading/trailing space
-prefixes are cached up to 1 trailing newline
-
*/
type AutocompletionStatus = 'pending' | 'finished' | 'error';
type Autocompletion = {
id: number,
prefix: string,
suffix: string,
startTime: number,
endTime: number | undefined,
abortRef: AbortRef,
status: AutocompletionStatus,
llmPromise: Promise<string> | undefined,
result: string,
}
const DEBOUNCE_TIME = 500
const TIMEOUT_TIME = 60000
const MAX_CACHE_SIZE = 20
const MAX_PENDING_REQUESTS = 2
// postprocesses the result
const postprocessResult = (result: string) => {
console.log('result: ', JSON.stringify(result))
// trim all whitespace except for a single leading/trailing space
const hasLeadingSpace = result.startsWith(' ');
const hasTrailingSpace = result.endsWith(' ');
return (hasLeadingSpace ? ' ' : '')
+ result.trim()
+ (hasTrailingSpace ? ' ' : '');
}
const extractCodeFromResult = (result: string) => {
// extract the code between triple backticks
const parts = result.split(/```(?:\s*\w+)?\n?/);
// 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
}
function getStringUpToUnbalancedParenthesis(s: string, prefixToTheLeft: string): string {
const pairs: Record<string, string> = { ')': '(', '}': '{', ']': '[' };
// todo find first open bracket in prefix and get all brackets beyond it in prefix
// get all bracets in prefix
let stack: string[] = []
const firstOpenIdx = prefixToTheLeft.search(/[[({]/);
if (firstOpenIdx !== -1) stack = prefixToTheLeft.slice(firstOpenIdx).split('').filter(c => '()[]{}'.includes(c))
// Iterate through each character
for (let i = 0; i < s.length; i++) {
const char = s[i];
if (char === '(' || char === '{' || char === '[') { stack.push(char); }
else if (char === ')' || char === '}' || char === ']') {
if (stack.length === 0 || stack.pop() !== pairs[char]) { return s.substring(0, i); }
}
}
return s;
}
// 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, suffix, autocompletion, position }: { prefix: string, suffix: string, autocompletion: Autocompletion, position: vscode.Position }): vscode.InlineCompletionItem => {
const originalPrefix = autocompletion.prefix
const generatedMiddle = autocompletion.result
const trimmedOriginalPrefix = trimPrefix(originalPrefix)
const trimmedCurrentPrefix = trimPrefix(prefix)
const suffixLines = suffix.split('\n')
const prefixLines = trimmedCurrentPrefix.split('\n')
const suffixToTheRightOfCursor = suffixLines[0].trim()
const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1].trim()
const generatedLines = generatedMiddle.split('\n')
// compute startIdx
let startIdx = trimmedCurrentPrefix.length - trimmedOriginalPrefix.length
if (startIdx < 0) {
return new vscode.InlineCompletionItem('')
}
// compute endIdx
// hacks to get the suffix to render properly with lower quality models
// if the generated text matches with the suffix on the current line, stop
let endIdx: number | undefined = generatedMiddle.length // exclusive bounds
if (suffixToTheRightOfCursor !== '') { // completing in the middle of a line
console.log('1')
// complete until there is a match
const matchIndex = generatedMiddle.lastIndexOf(suffixToTheRightOfCursor[0])
if (matchIndex > 0) { endIdx = matchIndex }
}
if (prefixToTheLeftOfCursor !== '') { // completing the end of a line
console.log('2')
// show a single line
const newlineIdx = generatedMiddle.indexOf('\n')
if (newlineIdx > -1) { endIdx = newlineIdx }
}
// // if a generated line matches with a suffix line, stop
// if (suffixLines.length > 1) {
// console.log('3')
// const lines = []
// for (const generatedLine of generatedLines) {
// if (suffixLines.slice(0, 10).some(suffixLine =>
// generatedLine.trim() !== '' && suffixLine.trim() !== ''
// && generatedLine.trim().startsWith(suffixLine.trim())
// )) break;
// lines.push(generatedLine)
// }
// endIdx = lines.join('\n').length // this is hacky, remove or refactor in future
// }
let completionStr = generatedMiddle.slice(startIdx, endIdx)
// filter out unbalanced parentheses
console.log('completionStrBeforeParens: ', JSON.stringify(completionStr))
completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefixLines.slice(-2).join('\n'))
console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx)))
console.log('finalCompletionStr: ', JSON.stringify(completionStr))
return new vscode.InlineCompletionItem(completionStr, new vscode.Range(position, position))
}
// returns whether this autocompletion is in the cache
const doesPrefixMatchAutocompletion = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): boolean => {
const originalPrefix = autocompletion.prefix
const generatedMiddle = autocompletion.result
const originalPrefixTrimmed = trimPrefix(originalPrefix)
const currentPrefixTrimmed = trimPrefix(prefix)
if (currentPrefixTrimmed.length < originalPrefixTrimmed.length) {
return false
}
const isMatch = (originalPrefixTrimmed + generatedMiddle).startsWith(currentPrefixTrimmed)
return isMatch
}
const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => {
const prefixLines = prefix.split('\n')
const suffixLines = suffix.split('\n')
const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? ''
const suffixToRightOfCursor = suffixLines[0]
// default parameters
let shouldGenerate = true
let stopTokens: string[] = ['\n\n', '\r\n\r\n']
// specific cases
if (suffixToRightOfCursor.trim() !== '') { // typing between something
stopTokens = ['\n', '\r\n']
}
// if (prefixToLeftOfCursor.trim() === '' && suffixToRightOfCursor.trim() === '') { // at an empty line
// stopTokens = ['\n\n', '\r\n\r\n']
// }
if (prefixToLeftOfCursor === '' || suffixToRightOfCursor === '') { // at beginning or end of line
shouldGenerate = false
}
console.log('shouldGenerate:', shouldGenerate, stopTokens)
return { shouldGenerate, stopTokens }
}
export class AutocompleteProvider implements vscode.InlineCompletionItemProvider {
private _extensionContext: vscode.ExtensionContext;
private _autocompletionId: number = 0;
private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache<number, Autocompletion> } = {}
private _lastCompletionTime = 0
private _lastPrefix: string = ''
constructor(context: vscode.ExtensionContext) {
this._extensionContext = context
}
// used internally by vscode
// fires after every keystroke and returns the completion to show
async provideInlineCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
context: vscode.InlineCompletionContext,
token: vscode.CancellationToken,
): Promise<vscode.InlineCompletionItem[]> {
const disabled = false
if (disabled) { return []; }
const docUriStr = document.uri.toString()
const fullText = document.getText();
const cursorOffset = document.offsetAt(position);
const prefix = fullText.substring(0, cursorOffset)
const suffix = fullText.substring(cursorOffset)
const voidConfig = getVoidConfigFromPartial(this._extensionContext.globalState.get('partialVoidConfig') ?? {})
// initialize cache and other variables
// note that whenever an autocompletion is rejected, it is removed from cache
if (!this._autocompletionsOfDocument[docUriStr]) {
this._autocompletionsOfDocument[docUriStr] = new LRUCache<number, Autocompletion>({
max: MAX_CACHE_SIZE,
dispose: (autocompletion) => {
autocompletion.abortRef.current()
}
})
}
this._lastPrefix = prefix
// get all pending autocompletions
let __c = 0
this._autocompletionsOfDocument[docUriStr].forEach(a => { if (a.status === 'pending') __c += 1 })
console.log('pending: ' + __c)
// get autocompletion from cache
let cachedAutocompletion: Autocompletion | undefined = undefined
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
}
}
// if there is a cached autocompletion, return it
if (cachedAutocompletion) {
if (cachedAutocompletion.status === 'finished') {
console.log('A1')
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position })
return [inlineCompletion]
} else if (cachedAutocompletion.status === 'pending') {
console.log('A2')
try {
await cachedAutocompletion.llmPromise;
console.log('id: ' + cachedAutocompletion.id)
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position })
return [inlineCompletion]
} catch (e) {
this._autocompletionsOfDocument[docUriStr].delete(cachedAutocompletion.id)
console.error('Error creating autocompletion (1): ' + e)
}
} else if (cachedAutocompletion.status === 'error') {
console.log('A3')
}
return []
}
// else if no more typing happens, then go forwards with the request
// wait DEBOUNCE_TIME for the user to stop typing
const thisTime = Date.now()
this._lastCompletionTime = thisTime
const didTypingHappenDuringDebounce = await new Promise((resolve, reject) =>
setTimeout(() => {
if (this._lastCompletionTime === 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('B')
// if there are too many pending requests, cancel the oldest one
let numPending = 0
let oldestPending: Autocompletion | undefined = undefined
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].values()) {
if (autocompletion.status === 'pending') {
numPending += 1
if (oldestPending === undefined) {
oldestPending = autocompletion
}
if (numPending >= MAX_PENDING_REQUESTS) {
// cancel the oldest pending request and remove it from cache
this._autocompletionsOfDocument[docUriStr].delete(oldestPending.id)
break
}
}
}
const { shouldGenerate, stopTokens } = getCompletionOptions({ prefix, suffix })
if (!shouldGenerate) return []
// create a new autocompletion and add it to cache
const newAutocompletion: Autocompletion = {
id: this._autocompletionId++,
prefix: prefix,
suffix: suffix,
startTime: Date.now(),
endTime: undefined,
abortRef: { current: () => { } },
status: 'pending',
llmPromise: undefined,
result: '',
}
// set parameters of `newAutocompletion` appropriately
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
sendLLMMessage({
mode: 'fim',
fimInfo: { prefix, suffix },
options: { stopTokens },
onText: async (tokenStr, completionStr) => {
newAutocompletion.result = completionStr
// if generation doesn't match the prefix for the first few tokens generated, reject it
if (!doesPrefixMatchAutocompletion({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
reject('LLM response did not match user\'s text.')
}
},
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,
})
// if the request hasnt resolved in TIMEOUT_TIME seconds, reject it
setTimeout(() => {
if (newAutocompletion.status === 'pending') {
reject('Timeout receiving message to LLM.')
}
}, TIMEOUT_TIME)
})
// add autocompletion to cache
this._autocompletionsOfDocument[docUriStr].set(newAutocompletion.id, newAutocompletion)
// show autocompletion
try {
await newAutocompletion.llmPromise
console.log('id: ' + newAutocompletion.id)
const inlineCompletion = toInlineCompletion({ autocompletion: newAutocompletion, prefix, suffix, position })
return [inlineCompletion]
} catch (e) {
this._autocompletionsOfDocument[docUriStr].delete(newAutocompletion.id)
console.error('Error creating autocompletion (2): ' + e)
return []
}
}
}
+216 -89
View File
@@ -3,14 +3,16 @@ import { findDiffs } from './findDiffs';
import { throttle } from 'lodash';
import { DiffArea, BaseDiff, Diff } from '../common/shared_types';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
import { AbortRef, sendLLMMessage } from '../common/sendLLMMessage';
import { writeFileWithDiffInstructions } from '../common/systemPrompts';
import { VoidConfig } from '../webviews/common/contextForConfig';
import { updateWebviewHTML } from './extensionLib/updateWebviewHTML';
const THROTTLE_TIME = 100
// TODO in theory this should be disposed
const greenDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(0 255 51 / 0.2)',
isWholeLine: false, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:...
})
const lightGrayDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(218 218 218 / .2)',
isWholeLine: true,
@@ -46,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) => {
@@ -59,7 +59,7 @@ export class DiffProvider implements vscode.CodeLensProvider {
const changes = e.contentChanges.map(c => ({ startLine: c.range.start.line, endLine: c.range.end.line, text: c.text, }))
// on user change, grow/shrink/merge/delete diff areas
this.resizeDiffAreas(docUriStr, changes, 'currentFile')
this.refreshDiffAreasModel(docUriStr, changes, 'currentFile')
// refresh the diffAreas
this.refreshStylesAndDiffs(docUriStr)
@@ -98,19 +98,18 @@ export class DiffProvider implements vscode.CodeLensProvider {
// used by us only
// changes the start/line locations based on the changes that were recently made. does not change any of the diffs in the diff areas
// changes tells us how many lines were inserted/deleted so we can grow/shrink the diffAreas accordingly
public resizeDiffAreas(docUriStr: string, changes: { text: string, startLine: number, endLine: number }[], changesTo: 'originalFile' | 'currentFile') {
public refreshDiffAreasModel(docUriStr: string, changes: { text: string, startLine: number, endLine: number }[], changesTo: 'originalFile' | 'currentFile') {
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
let endLine: 'originalEndLine' | 'endLine'
let startLine: 'originalStartLine' | 'startLine'
let endName
let startName
if (changesTo === 'originalFile') {
endLine = 'originalEndLine' as const
startLine = 'originalStartLine' as const
endName = 'originalEndLine' as const
startName = 'originalStartLine' as const
} else {
endLine = 'endLine' as const
startLine = 'startLine' as const
endName = 'endLine' as const
startName = 'startLine' as const
}
for (const change of changes) {
@@ -127,11 +126,11 @@ export class DiffProvider implements vscode.CodeLensProvider {
for (const diffArea of diffAreas) {
// if the change is fully within the diffArea, elongate it by the delta amount of newlines
if (change.startLine >= diffArea[startLine] && change.endLine <= diffArea[endLine]) {
diffArea[endLine] += deltaNewlines
if (change.startLine >= diffArea[startName] && change.endLine <= diffArea[endName]) {
diffArea[endName] += deltaNewlines
}
// check if the `diffArea` was fully deleted and remove it if so
if (diffArea[startLine] > diffArea[endLine]) {
if (diffArea[startName] > diffArea[endName]) {
//remove it
const index = diffAreas.findIndex(da => da === diffArea)
diffAreas.splice(index, 1)
@@ -143,9 +142,9 @@ export class DiffProvider implements vscode.CodeLensProvider {
// if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines
for (const diffArea of diffAreas) {
if (diffArea[startLine] > change.endLine) {
diffArea[startLine] += deltaNewlines
diffArea[endLine] += deltaNewlines
if (diffArea[startName] > change.endLine) {
diffArea[startName] += deltaNewlines
diffArea[endName] += deltaNewlines
}
}
@@ -166,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;
}
@@ -175,9 +176,6 @@ export class DiffProvider implements vscode.CodeLensProvider {
// reset all diffs (we update them below)
this._diffsOfDocument[docUriStr] = []
// TODO!!!!
// vscode.languages.clearInlineDiffs(editor)
// for each diffArea
for (const diffArea of diffAreas) {
@@ -189,30 +187,47 @@ export class DiffProvider implements vscode.CodeLensProvider {
const diffs = findDiffs(originalCode, currentCode)
// add the diffs to `this._diffsOfDocument[docUriStr]`
this.createDiffs(editor.document.uri, diffs, diffArea)
// if no diffs, set diffs to []
if (!this._diffsOfDocument[docUriStr])
this._diffsOfDocument[docUriStr] = []
// add each diff and its codelens to the document
for (let i = diffs.length - 1; i > -1; i -= 1) {
let suggestedDiff = diffs[i]
this._diffsOfDocument[docUriStr].push({
...suggestedDiff,
diffid: this._diffidPool,
// originalCode: suggestedDiff.deletedText,
lenses: [
new vscode.CodeLens(suggestedDiff.range, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }),
new vscode.CodeLens(suggestedDiff.range, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] })
]
});
vscode.languages.addInlineDiff(editor, suggestedDiff.originalCode, suggestedDiff.range)
this._diffidPool += 1
}
// // print diffs
// console.log('!ORIGINAL FILE:', JSON.stringify(originalFile))
// console.log('!NEW FILE :', JSON.stringify(editor.document.getText().replace(/\r\n/g, '\n')))
// console.log('!AREA originalCode:', JSON.stringify(originalCode))
// console.log('!AREA currentCode :', JSON.stringify(currentCode))
// for (const diff of this._diffsOfDocument[docUriStr]) {
// console.log('------------')
// console.log('originalCode:', JSON.stringify(diff.originalCode))
// console.log('currentCode:', JSON.stringify(diff.code))
// console.log('originalRange:', diff.originalRange.start.line, diff.originalRange.end.line,)
// console.log('currentRange:', diff.range.start.line, diff.range.end.line,)
// }
}
// update green highlighting
editor.setDecorations(
greenDecoration,
(this._diffsOfDocument[docUriStr]
.filter(diff => diff.range !== undefined)
.map(diff => diff.range)
)
);
// update red highlighting
// this._diffsOfDocument[docUriStr]
// .filter(diff => diff.originalCode !== '')
// .forEach(diff => {
// const text = originalFile.split('\n').slice(diff.originalRange.start.line, diff.originalRange.start.line + 1).join('\n')
// const height = text.split('\n').length
// const line = diff.range.start.line - 1
// const inset = vscode.window.createWebviewTextEditorInset(editor, line, height);
// updateWebviewHTML(inset.webview, this._extensionUri, { jsOutLocation: 'dist/webviews/diffline/index.js', cssOutLocation: 'dist/webviews/styles.css' },
// { text }
// )
// })
// for each diffArea, highlight its sweepIndex in dark gray
editor.setDecorations(
@@ -243,6 +258,32 @@ export class DiffProvider implements vscode.CodeLensProvider {
}
// used by us only
public createDiffs(docUri: vscode.Uri, diffs: BaseDiff[], diffArea: DiffArea) {
const docUriStr = docUri.toString()
// if no diffs, set diffs to []
if (!this._diffsOfDocument[docUriStr])
this._diffsOfDocument[docUriStr] = []
// add each diff and its codelens to the document
for (let i = diffs.length - 1; i > -1; i -= 1) {
let suggestedDiff = diffs[i]
this._diffsOfDocument[docUriStr].push({
...suggestedDiff,
diffid: this._diffidPool,
// originalCode: suggestedDiff.deletedText,
lenses: [
new vscode.CodeLens(suggestedDiff.range, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }),
new vscode.CodeLens(suggestedDiff.range, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] })
]
});
this._diffidPool += 1
}
}
// called on void.acceptDiff
public async acceptDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
@@ -279,7 +320,7 @@ export class DiffProvider implements vscode.CodeLensProvider {
this._originalFileOfDocument[docUriStr] = newOriginalLines.join('\n');
// Update diff areas based on the change
this.resizeDiffAreas(docUriStr, [{
this.refreshDiffAreasModel(docUriStr, [{
text: changedLines.join('\n'),
startLine: diff.originalRange.start.line,
endLine: diff.originalRange.end.line
@@ -292,6 +333,11 @@ export class DiffProvider implements vscode.CodeLensProvider {
const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
const originalArea = newOriginalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
console.log('ACCEPT change', changedLines.join('\n'), diff.originalRange.start.line, diff.originalRange.end.line)
console.log('ACCEPT area lines', diffArea.startLine, diffArea.endLine, diffArea.originalStartLine, diffArea.originalEndLine)
console.log('ACCEPT currentArea', currentArea)
console.log('ACCEPT originalArea', originalArea)
if (originalArea === currentArea) {
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1)
@@ -332,6 +378,11 @@ export class DiffProvider implements vscode.CodeLensProvider {
const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
const originalArea = originalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
console.log('REJECT diff lines', diff.originalRange.start.line, diff.originalRange.end.line)
console.log('REJECT area lines', diffArea.startLine, diffArea.endLine, diffArea.originalStartLine, diffArea.originalEndLine)
console.log('REJECT currentArea', currentArea)
console.log('REJECT originalArea', originalArea)
if (originalArea === currentArea) {
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1)
@@ -340,54 +391,11 @@ export class DiffProvider implements vscode.CodeLensProvider {
this.refreshStylesAndDiffs(docUriStr)
}
async startStreamingInDiffArea({ docUri, oldFileStr, diffRepr, diffArea, voidConfig, abortRef }: { docUri: vscode.Uri, oldFileStr: string, diffRepr: string, voidConfig: VoidConfig, diffArea: DiffArea, abortRef: AbortRef }) {
const promptContent = `\
ORIGINAL_FILE
\`\`\`
${oldFileStr}
\`\`\`
DIFF
\`\`\`
${diffRepr}
\`\`\`
INSTRUCTIONS
Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation.
`
// make LLM complete the file to include the diff
await new Promise<void>((resolve, reject) => {
sendLLMMessage({
logging: { loggingName: 'streamChunk' },
messages: [
{ role: 'system', content: writeFileWithDiffInstructions, },
// TODO include more context too
{ role: 'user', content: promptContent, }
],
onText: (newText, fullText) => {
this._updateStream(docUri.toString(), diffArea, fullText)
},
onFinalMessage: (fullText) => {
this._updateStream(docUri.toString(), diffArea, fullText)
resolve();
},
onError: (e) => {
console.error('Error rewriting file with diff', e);
resolve();
},
voidConfig,
abortRef,
})
})
}
// used by us only
private _updateStream = throttle(async (docUriStr: string, diffArea: DiffArea, newDiffAreaCode: string) => {
public updateStream = throttle(async (docUriStr: string, diffArea: DiffArea, newDiffAreaCode: string) => {
const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor
if (!editor) {
@@ -442,9 +450,128 @@ Please finish writing the new file by applying the diff to the original file. Re
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)
}
/*
import * as vscode from 'vscode';
import { SuggestedEdit } from './findDiffs';
const greenDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(0 255 51 / 0.2)',
isWholeLine: false, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:...
})
export class DiffProvider {
originalCodeOfDocument: { [docUri: string]: string }
diffsOfDocument: {
[docUri: string]: {
startLine,
startCol,
endLine,
endCol,
originalText,
inset,
diffid,
}
}
// sweep
currentLine: { [docUri: string]: undefined | number }
weAreEditing: boolean = false
constructor() {
vscode.workspace.onDidChangeTextDocument((e) => {
// on user change, grow/shrink/merge/delete diff AREAS
// you dont have to do anything to the diffs here bc they all get recomputed in refresh()
// user changes only get highlighted if theyre in a diffarea
// go thru all diff areas and adjust line numbers based on the user's change
this.refreshStyles(e.document.uri.toString())
})
}
// refreshes styles on page
refreshStyles(docUriStr: string) {
if (this.weAreEditing) return
// recompute all diffs on the page
// run inset.dispose() on all diffs
// original and current code -> diffs
// originalCodeOfDocument[docUriStr]
// create new diffs
const inset = vscode.window.createWebviewTextEditorInset(editor, lineStart, height, {})
inset.webview.html = `
<html>
<body style="pointer-events:none;">Hello World!</body>
</html>
`;
}
// called on void.acceptDiff
public async acceptDiff({ diffid }: { diffid: number }) {
// update original based on the diff
// refresh()
}
// called on void.rejectDiff
public async rejectDiff({ diffid }: { diffid: number }) {
// get diffs[diffid]
// revert current file based on diff
// refresh()
}
// sweep
initializeSweep({ startLine }) {
// reject all diffs on the page
// store original code
// currentLine=start of sweep
}
onUpdateSweep(addedText) {
// update final
// refresh() ?
// currentLine += number of newlines in addedText
}
onAbortSweep() {
}
}
*/
+142 -143
View File
@@ -1,181 +1,180 @@
// import type * as vscode from 'vscode';
import * as vscode from 'vscode';
import { AbortRef, sendLLMMessage } from '../common/sendLLMMessage';
import { DiffArea } from '../common/shared_types';
import { writeFileWithDiffInstructions, searchDiffChunkInstructions } from '../common/systemPrompts';
import { VoidConfig } from '../webviews/common/contextForConfig';
import { DiffProvider } from './DiffProvider';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
// import { AbortRef, sendLLMMessage } from '../common/sendLLMMessage';
// import { DiffArea } from '../common/shared_types';
// import { writeFileWithDiffInstructions, searchDiffChunkInstructions } from '../common/systemPrompts';
// import { VoidConfig } from '../webviews/common/contextForConfig';
// import { DiffProvider } from './DiffProvider';
// import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
// const LINES_PER_CHUNK = 20 // number of lines to search at a time
const LINES_PER_CHUNK = 20 // number of lines to search at a time
// type CompetedReturn = { isFinished: true, } | { isFinished?: undefined, }
// const streamChunk = ({ diffProvider, docUri, oldFileStr, completedStr, diffRepr, diffArea, voidConfig, abortRef }: { diffProvider: DiffProvider, docUri: vscode.Uri, oldFileStr: string, completedStr: string, diffRepr: string, voidConfig: VoidConfig, diffArea: DiffArea, abortRef: AbortRef }) => {
type CompetedReturn = { isFinished: true, } | { isFinished?: undefined, }
const streamChunk = ({ diffProvider, docUri, oldFileStr, completedStr, diffRepr, diffArea, voidConfig, abortRef }: { diffProvider: DiffProvider, docUri: vscode.Uri, oldFileStr: string, completedStr: string, diffRepr: string, voidConfig: VoidConfig, diffArea: DiffArea, abortRef: AbortRef }) => {
// const promptContent = `ORIGINAL_FILE
// \`\`\`
// ${oldFileStr}
// \`\`\`
const NUM_MATCHUP_TOKENS = 20
// DIFF
// \`\`\`
// ${diffRepr}
// \`\`\`
const promptContent = `ORIGINAL_FILE
\`\`\`
${oldFileStr}
\`\`\`
// INSTRUCTIONS
// Please finish writing the new file \`NEW_FILE\`. Return ONLY the completion of the file, without any explanation.
DIFF
\`\`\`
${diffRepr}
\`\`\`
// NEW_FILE
// \`\`\`
// ${completedStr}
// \`\`\`
// `
// // create a promise that can be awaited
// return new Promise<CompetedReturn>((resolve, reject) => {
INSTRUCTIONS
Please finish writing the new file \`NEW_FILE\`. Return ONLY the completion of the file, without any explanation.
// let isAnyChangeSoFar = false
NEW_FILE
\`\`\`
${completedStr}
\`\`\`
`
// create a promise that can be awaited
return new Promise<CompetedReturn>((resolve, reject) => {
// // make LLM complete the file to include the diff
// sendLLMMessage({
// logging: { loggingName: 'applyDiffLazily' },
// messages: [{ role: 'system', content: writeFileWithDiffInstructions, }, { role: 'user', content: promptContent, }],
// onText: (newText, fullText) => {
// const fullCompletedStr = completedStr + fullText
let isAnyChangeSoFar = false
// diffProvider.updateStream(docUri.toString(), diffArea, fullCompletedStr)
// make LLM complete the file to include the diff
sendLLMMessage({
messages: [{ role: 'system', content: writeFileWithDiffInstructions, }, { role: 'user', content: promptContent, }],
onText: (newText, fullText) => {
const fullCompletedStr = completedStr + fullText
// // if there was any change from the original file
// if (!oldFileStr.includes(fullCompletedStr)) {
// isAnyChangeSoFar = true
// }
diffProvider.updateStream(docUri.toString(), diffArea, fullCompletedStr)
// if there was any change from the original file
if (!oldFileStr.includes(fullCompletedStr)) {
isAnyChangeSoFar = true
}
const isRecentMatchup = false
// the final NUM_MATCHUP_TOKENS characters of fullCompletedStr are the same as the final NUM_MATCHUP_TOKENS characters of the last item in the diffs of oldFileStr that had 0 changes
if (isAnyChangeSoFar && isRecentMatchup) {
diffProvider.updateStream(docUri.toString(), diffArea, fullCompletedStr)
// TODO resolve the promise
// resolve({ speculativeIndex: newCurrentLine + 1 });
// abort the LLM call
abortRef.current?.()
}
},
onFinalMessage: (fullText) => {
const newCompletedStr = completedStr + fullText
diffProvider.updateStream(docUri.toString(), diffArea, newCompletedStr)
resolve({ isFinished: true });
},
onError: (e) => {
resolve({ isFinished: true });
console.error('Error rewriting file with diff', e);
},
voidConfig,
abortRef,
})
})
}
// const isRecentMatchup = false
// // the final NUM_MATCHUP_TOKENS characters of fullCompletedStr are the same as the final NUM_MATCHUP_TOKENS characters of the last item in the diffs of oldFileStr that had 0 changes
const shouldApplyDiff = ({ diffRepr, oldFileStr: fileStr, speculationStr, voidConfig, abortRef }: { diffRepr: string, oldFileStr: string, speculationStr: string, voidConfig: VoidConfig, abortRef: AbortRef }) => {
// if (isAnyChangeSoFar && isRecentMatchup) {
// diffProvider.updateStream(docUri.toString(), diffArea, fullCompletedStr)
const promptContent = `DIFF
\`\`\`
${diffRepr}
\`\`\`
// // TODO resolve the promise
// // resolve({ speculativeIndex: newCurrentLine + 1 });
FILES
\`\`\`
${fileStr}
\`\`\`
// // abort the LLM call
// abortRef.current?.()
SELECTION
\`\`\`
${speculationStr}
\`\`\`
// }
Return \`true\` if ANY part of the chunk should be modified, and \`false\` if it should not be modified. You should respond only with \`true\` or \`false\` and nothing else.
`
// },
// create new promise
return new Promise<boolean>((resolve, reject) => {
// send message to LLM
sendLLMMessage({
messages: [{ role: 'system', content: searchDiffChunkInstructions, }, { role: 'user', content: promptContent, }],
onFinalMessage: (finalMessage) => {
// onFinalMessage: (fullText) => {
// const newCompletedStr = completedStr + fullText
// diffProvider.updateStream(docUri.toString(), diffArea, newCompletedStr)
// resolve({ isFinished: true });
// },
// onError: (e) => {
// resolve({ isFinished: true });
// console.error('Error rewriting file with diff', e);
// },
// voidConfig,
// abortRef,
// })
// })
// }
const containsTrue = finalMessage
.slice(-10) // check for `true` in last 10 characters
.toLowerCase()
.includes('true')
resolve(containsTrue)
},
onError: (e) => {
resolve(false);
console.error('Error in shouldApplyDiff: ', e)
},
onText: () => { },
voidConfig,
abortRef,
})
// // const shouldApplyDiff = ({ diffRepr, oldFileStr: fileStr, speculationStr, voidConfig, abortRef }: { diffRepr: string, oldFileStr: string, speculationStr: string, voidConfig: VoidConfig, abortRef: AbortRef }) => {
})
// // const promptContent = `DIFF
// // \`\`\`
// // ${diffRepr}
// // \`\`\`
// // FILES
// // \`\`\`
// // ${fileStr}
// // \`\`\`
// // SELECTION
// // \`\`\`
// // ${speculationStr}
// // \`\`\`
// // Return \`true\` if ANY part of the chunk should be modified, and \`false\` if it should not be modified. You should respond only with \`true\` or \`false\` and nothing else.
// // `
// // // create new promise
// // return new Promise<boolean>((resolve, reject) => {
// // // send message to LLM
// // sendLLMMessage({
// // messages: [{ role: 'system', content: searchDiffChunkInstructions, }, { role: 'user', content: promptContent, }],
// // onFinalMessage: (finalMessage) => {
// // const containsTrue = finalMessage
// // .slice(-10) // check for `true` in last 10 characters
// // .toLowerCase()
// // .includes('true')
// // resolve(containsTrue)
// // },
// // onError: (e) => {
// // resolve(false);
// // console.error('Error in shouldApplyDiff: ', e)
// // },
// // onText: () => { },
// // voidConfig,
// // abortRef,
// // })
// // })
// // }
}
// // lazily applies the diff to the file
// // we chunk the text in the file, and ask an LLM whether it should edit each chunk
// export const applyDiffLazily = async ({ docUri, oldFileStr, voidConfig, abortRef, diffRepr, diffProvider, diffArea }: { docUri: vscode.Uri, oldFileStr: string, diffRepr: string, voidConfig: VoidConfig, diffProvider: DiffProvider, diffArea: DiffArea, abortRef: AbortRef }) => {
// lazily applies the diff to the file
// we chunk the text in the file, and ask an LLM whether it should edit each chunk
export const applyDiffLazily = async ({ docUri, oldFileStr, voidConfig, abortRef, diffRepr, diffProvider, diffArea }: { docUri: vscode.Uri, oldFileStr: string, diffRepr: string, voidConfig: VoidConfig, diffProvider: DiffProvider, diffArea: DiffArea, abortRef: AbortRef }) => {
// // stateful variables
// let speculativeIndex = 0
// let writtenTextSoFar: string[] = []
// stateful variables
let speculativeIndex = 0
let writtenTextSoFar: string[] = []
// while (speculativeIndex < oldFileStr.split('\n').length) {
while (speculativeIndex < oldFileStr.split('\n').length) {
// const chunkStr = oldFileStr.split('\n').slice(speculativeIndex, speculativeIndex + LINES_PER_CHUNK).join('\n')
const chunkStr = oldFileStr.split('\n').slice(speculativeIndex, speculativeIndex + LINES_PER_CHUNK).join('\n')
// // ask LLM if we should apply the diff to the chunk
// const START = new Date().getTime()
// let shouldApplyDiff_ = true; //await shouldApplyDiff({ oldFileStr, speculationStr: chunkStr, diffRepr, voidConfig, abortRef })
// const END = new Date().getTime()
// ask LLM if we should apply the diff to the chunk
const START = new Date().getTime()
let shouldApplyDiff_ = await shouldApplyDiff({ oldFileStr, speculationStr: chunkStr, diffRepr, voidConfig, abortRef })
const END = new Date().getTime()
// // if should not change the chunk
// if (!shouldApplyDiff_) {
// console.log('KEEP CHUNK time: ', END - START)
// speculativeIndex += LINES_PER_CHUNK
// writtenTextSoFar.push(chunkStr)
// // diffProvider.updateStream(docUri.toString(), diffArea, writtenTextSoFar.join('\n'))
// continue;
// }
// if should not change the chunk
if (!shouldApplyDiff_) {
console.log('KEEP CHUNK time: ', END - START)
speculativeIndex += LINES_PER_CHUNK
writtenTextSoFar.push(chunkStr)
diffProvider.updateStream(docUri.toString(), diffArea, writtenTextSoFar.join('\n'))
continue;
}
// // ask LLM to rewrite file with diff (if there is significant matchup with the original file, we stop rewriting)
// const START2 = new Date().getTime()
// const completedStr = (await readFileContentOfUri(docUri)).split('\n').slice(0, speculativeIndex).join('\n');
// const result = await streamChunk({ diffProvider, docUri, oldFileStr, completedStr, diffRepr, voidConfig, diffArea, abortRef, })
// const END2 = new Date().getTime()
// ask LLM to rewrite file with diff (if there is significant matchup with the original file, we stop rewriting)
const START2 = new Date().getTime()
const completedStr = (await readFileContentOfUri(docUri)).split('\n').slice(0, speculativeIndex).join('\n');
const result = await streamChunk({ diffProvider, docUri, oldFileStr, completedStr, diffRepr, voidConfig, diffArea, abortRef, })
const END2 = new Date().getTime()
// console.log('EDIT CHUNK time: ', END2 - START2);
console.log('EDIT CHUNK time: ', END2 - START2);
// // if we are finished, stop the loop
// if (result.isFinished) {
// break;
// }
// if we are finished, stop the loop
if (result.isFinished) {
break;
}
// // TODO
// // speculativeIndex = result.speculativeIndex
// TODO
// speculativeIndex = result.speculativeIndex
// }
}
// }
}
+3 -37
View File
@@ -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,26 +20,19 @@ 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({
logging: { loggingName: 'Ctrl+K' },
messages: [{ role: 'user', content: promptContent, }],
onText: async (tokenStr, completionStr) => {
// TODO update stream
+31 -2
View File
@@ -4,10 +4,28 @@ import { v4 as uuidv4 } from 'uuid'
import { AbortRef } from '../common/sendLLMMessage';
import { MessageToSidebar, MessageFromSidebar, DiffArea, ChatThreads } from '../common/shared_types';
import { getVoidConfigFromPartial } from '../webviews/common/contextForConfig';
import { applyDiffLazily } from './applyDiffLazily';
import { DiffProvider } from './DiffProvider';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
import { SidebarWebviewProvider } from './providers/SidebarWebviewProvider';
import { CtrlKWebviewProvider } from './providers/CtrlKWebviewProvider';
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;
// }
// }
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
@@ -96,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: () => { } }
if (m.type === 'requestFiles') {
@@ -130,7 +148,7 @@ export function activate(context: vscode.ExtensionContext) {
const fileStr = await readFileContentOfUri(docUri)
const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {})
await diffProvider.startStreamingInDiffArea({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, 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') ?? {}
@@ -164,7 +182,18 @@ 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') ?? {})
// 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)
@@ -3,6 +3,22 @@
import * as vscode from 'vscode';
import { updateWebviewHTML as _updateWebviewHTML, updateWebviewHTML } from '../extensionLib/updateWebviewHTML';
// this comes from vscode.proposed.editorInsets.d.ts
declare module 'vscode' {
export interface WebviewEditorInset {
readonly editor: vscode.TextEditor;
readonly line: number;
readonly height: number;
readonly webview: vscode.Webview;
readonly onDidDispose: Event<void>;
dispose(): void;
}
export namespace window {
export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset;
}
}
export class CtrlKWebviewProvider {
@@ -1,5 +1,6 @@
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
import { awaitVSCodeResponse, getVSCodeAPI, useOnVSCodeMessage } from "./getVscodeApi"
import { Ollama } from "ollama/browser"
const configEnum = <EnumArr extends readonly string[]>(description: string, defaultVal: EnumArr[number], enumArr: EnumArr) => {
return {
@@ -29,8 +30,6 @@ export const configFields = [
'azure',
] as const
const voidConfigInfo: Record<
typeof configFields[number] | 'default', {
[prop: string]: {
@@ -119,11 +118,11 @@ const voidConfigInfo: Record<
'http://127.0.0.1:11434'
),
// TODO we should allow user to select model inside Void, but for now we'll just let them handle the Ollama setup on their own
// model: configEnum(
// 'Ollama model to use.',
// 'llama3.1',
// ["codegemma", "codegemma:2b", "codegemma:7b", "codellama", "codellama:7b", "codellama:13b", "codellama:34b", "codellama:70b", "codellama:code", "codellama:python", "command-r", "command-r:35b", "command-r-plus", "command-r-plus:104b", "deepseek-coder-v2", "deepseek-coder-v2:16b", "deepseek-coder-v2:236b", "falcon2", "falcon2:11b", "firefunction-v2", "firefunction-v2:70b", "gemma", "gemma:2b", "gemma:7b", "gemma2", "gemma2:2b", "gemma2:9b", "gemma2:27b", "llama2", "llama2:7b", "llama2:13b", "llama2:70b", "llama3", "llama3:8b", "llama3:70b", "llama3-chatqa", "llama3-chatqa:8b", "llama3-chatqa:70b", "llama3-gradient", "llama3-gradient:8b", "llama3-gradient:70b", "llama3.1", "llama3.1:8b", "llama3.1:70b", "llama3.1:405b", "llava", "llava:7b", "llava:13b", "llava:34b", "llava-llama3", "llava-llama3:8b", "llava-phi3", "llava-phi3:3.8b", "mistral", "mistral:7b", "mistral-large", "mistral-large:123b", "mistral-nemo", "mistral-nemo:12b", "mixtral", "mixtral:8x7b", "mixtral:8x22b", "moondream", "moondream:1.8b", "openhermes", "openhermes:v2.5", "phi3", "phi3:3.8b", "phi3:14b", "phi3.5", "phi3.5:3.8b", "qwen", "qwen:7b", "qwen:14b", "qwen:32b", "qwen:72b", "qwen:110b", "qwen2", "qwen2:0.5b", "qwen2:1.5b", "qwen2:7b", "qwen2:72b", "smollm", "smollm:135m", "smollm:360m", "smollm:1.7b"] as const
// ),
model: configEnum(
'Ollama model to use.',
'codestral',
["codestral", "qwen2.5-coder", "qwen2.5-coder:0.5b", "qwen2.5-coder:1.5b", "qwen2.5-coder:3b", "qwen2.5-coder:7b", "qwen2.5-coder:14b", "qwen2.5-coder:32b", "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: {
model: configString(
@@ -17,4 +17,4 @@ export const initPosthog = () => {
person_profiles: 'identified_only' // we only track events from identified users. We identify them in Sidebar
}
)
}
}
@@ -156,7 +156,7 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
// state of chat
const [messageStream, setMessageStream] = useState('')
const [isLoading, setIsLoading] = useState(false)
const abortFnRef = useRef<(() => void) | null>(null)
const abortFnRef = useRef<(() => void)>(() => { })
const [latestError, setLatestError] = useState('')
@@ -167,6 +167,21 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureChatEvent = useCallback((eventId: string, extras?: object) => {
const whichApi = voidConfig.default['whichApi']
const messages = getCurrentThread()?.messages
captureEvent(eventId, {
whichApi: whichApi,
numMessages: messages?.length,
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.displayContent?.length })),
version: '2024-10-19',
...extras,
})
}, [getCurrentThread, voidConfig.default])
// if they pressed the + to add a new chat
useOnVSCodeMessage('startNewThread', (m) => {
const allThreads = getAllThreads()
@@ -220,12 +235,15 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selection, files }
addMessageToHistory(newHistoryElt)
captureChatEvent('Chat - Sending Message', { messageLength: instructions.length })
const submit_time = new Date()
// send message to LLM
sendLLMMessage({
logging: { loggingName: 'Chat' },
messages: [...(getCurrentThread()?.messages ?? []).map(m => ({ role: m.role, content: m.content })),],
onText: (newText, fullText) => setMessageStream(fullText),
onFinalMessage: (content) => {
captureChatEvent('Chat - Received Full Message', { messageLength: content.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
// add assistant's message to chat history, and clear selection
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content }
@@ -234,6 +252,8 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
setIsLoading(false)
},
onError: (error) => {
captureChatEvent('Chat - Error', { error })
// add assistant's message to chat history, and clear selection
let content = messageStream; // just use the current content
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
@@ -251,6 +271,9 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
}
const onAbort = useCallback(() => {
captureChatEvent('Chat - Abort', { messageLengthSoFar: messageStream.length })
// abort claude
abortFnRef.current?.()
@@ -262,7 +285,7 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
setMessageStream('')
setIsLoading(false)
}, [messageStream, addMessageToHistory])
}, [captureChatEvent, messageStream, addMessageToHistory])
return <>
+2 -3
View File
@@ -1,8 +1,7 @@
{
"include": [
"src/**/*",
"../../src/vscode-dts/vscode.d.ts",
],
"src/**/*"
, "../../src/vs/workbench/contrib/welcomeGettingStarted/common/AutcompleteProvider.tsx" ],
"exclude": [
"node_modules"
],
+35 -134
View File
@@ -44,7 +44,6 @@
"native-watchdog": "^1.4.1",
"node-pty": "1.1.0-beta21",
"open": "^8.4.2",
"openai": "^4.70.2",
"tas-client-umd": "0.2.0",
"v8-inspect-profiler": "^0.1.1",
"vscode-oniguruma": "1.7.0",
@@ -2232,18 +2231,33 @@
"version": "20.14.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.13.tgz",
"integrity": "sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/node-fetch": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz",
"integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
"license": "MIT",
"version": "2.5.12",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz",
"integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==",
"dev": true,
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
"form-data": "^3.0.0"
}
},
"node_modules/@types/node-fetch/node_modules/form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@types/responselike": {
@@ -3570,18 +3584,6 @@
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@@ -3645,18 +3647,6 @@
"node": ">= 14"
}
},
"node_modules/agentkeepalive": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
"integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
"license": "MIT",
"dependencies": {
"humanize-ms": "^1.2.1"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -4202,7 +4192,8 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"node_modules/atob": {
"version": "2.1.2",
@@ -5237,6 +5228,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -5248,6 +5240,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk= sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
@@ -6913,15 +6906,6 @@
"through": "~2.3.1"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/events": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz",
@@ -7753,6 +7737,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -7762,25 +7747,6 @@
"node": ">= 6"
}
},
"node_modules/form-data-encoder": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
"license": "MIT"
},
"node_modules/formdata-node": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"license": "MIT",
"dependencies": {
"node-domexception": "1.0.0",
"web-streams-polyfill": "4.0.0-beta.3"
},
"engines": {
"node": ">= 12.20"
}
},
"node_modules/fragment-cache": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
@@ -10866,15 +10832,6 @@
"node": ">= 14"
}
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.0.0"
}
},
"node_modules/husky": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/husky/-/husky-0.13.4.tgz",
@@ -13292,6 +13249,7 @@
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
@@ -13300,6 +13258,7 @@
"version": "2.1.28",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
"dev": true,
"dependencies": {
"mime-db": "1.45.0"
},
@@ -13888,29 +13847,11 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
"integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
"dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
@@ -14504,41 +14445,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openai": {
"version": "4.70.2",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.70.2.tgz",
"integrity": "sha512-Q2ymi/KPUYv+LJ9rFxeYxpkVAhcrZFTVvnJbdF1pUHg9eMC6lY8PU4TO1XOK5UZzOZuuVicouRwVMi1iDrT4qw==",
"license": "Apache-2.0",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7"
},
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/openai/node_modules/@types/node": {
"version": "18.19.64",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.64.tgz",
"integrity": "sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/opn": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-6.0.0.tgz",
@@ -18962,7 +18868,8 @@
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
},
"node_modules/tree-kill": {
"version": "1.2.2",
@@ -19388,7 +19295,8 @@
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/union-value": {
"version": "1.0.1",
@@ -19865,15 +19773,6 @@
"node": ">=10.13.0"
}
},
"node_modules/web-streams-polyfill": {
"version": "4.0.0-beta.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/web-tree-sitter": {
"version": "0.20.8",
"resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz",
@@ -19883,7 +19782,8 @@
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true
},
"node_modules/webpack": {
"version": "5.94.0",
@@ -20144,6 +20044,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0= sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
-1
View File
@@ -106,7 +106,6 @@
"native-watchdog": "^1.4.1",
"node-pty": "1.1.0-beta21",
"open": "^8.4.2",
"openai": "^4.70.2",
"tas-client-umd": "0.2.0",
"v8-inspect-profiler": "^0.1.1",
"vscode-oniguruma": "1.7.0",
@@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from '../../../base/common/event.js';
import { ICodeEditor, IDiffEditor } from '../editorBrowser.js';
import { IDecorationRenderOptions } from '../../common/editorCommon.js';
@@ -1,138 +0,0 @@
// import * as dom from '../../../../base/browser/dom.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IModelDeltaDecoration } from '../../../common/model.js';
import { ICodeEditor, IViewZone } from '../../editorBrowser.js';
import { IRange } from '../../../common/core/range.js';
import { EditorOption } from '../../../common/config/editorOptions.js';
// // ------ eslint-disable-next-line local/code-import-patterns
// import openai from 'openai'
// console.log(openai.apply);
// import { sendLLMMessage } from './sendLLMMessage.js';
export interface IInlineDiffService {
readonly _serviceBrand: undefined;
addDiff(editor: ICodeEditor, originalText: string, modifiedRange: IRange): void;
removeDiffs(editor: ICodeEditor): void;
}
export const IInlineDiffService = createDecorator<IInlineDiffService>('inlineDiffService');
class InlineDiffService extends Disposable implements IInlineDiffService {
private readonly _diffDecorations = new Map<ICodeEditor, string[]>();
private readonly _diffZones = new Map<ICodeEditor, string[]>();
_serviceBrand: undefined;
constructor() {
super();
}
initStream() {
}
public addDiff: IInlineDiffService['addDiff'] = (editor, originalText, modifiedRange) => {
// Clear existing diffs
this.removeDiffs(editor);
// green decoration and gutter decoration
const greenDecoration: IModelDeltaDecoration[] = [{
range: modifiedRange,
options: {
className: 'line-insert', // .monaco-editor .line-insert
description: 'line-insert',
isWholeLine: true,
minimap: {
color: { id: 'minimapGutter.addedBackground' },
position: 2
},
overviewRuler: {
color: { id: 'editorOverviewRuler.addedForeground' },
position: 7
}
}
}];
this._diffDecorations.set(editor, editor.deltaDecorations([], greenDecoration));
// red in a view zone
editor.changeViewZones(accessor => {
// Get the editor's font info
const fontInfo = editor.getOption(EditorOption.fontInfo);
const domNode = document.createElement('div');
domNode.className = 'monaco-editor view-zones line-delete monaco-mouse-cursor-text';
domNode.style.fontSize = `${fontInfo.fontSize}px`;
domNode.style.fontFamily = fontInfo.fontFamily;
domNode.style.lineHeight = `${fontInfo.lineHeight}px`;
// div
const lineContent = document.createElement('div');
lineContent.className = 'view-line'; // .monaco-editor .inline-deleted-text
// span
const contentSpan = document.createElement('span');
// span
const codeSpan = document.createElement('span');
codeSpan.className = 'mtk1'; // char-delete
codeSpan.textContent = originalText;
// Mount
contentSpan.appendChild(codeSpan);
lineContent.appendChild(contentSpan);
domNode.appendChild(lineContent);
const viewZone: IViewZone = {
afterLineNumber: modifiedRange.startLineNumber - 1,
heightInLines: originalText.split('\n').length + 1,
domNode: domNode,
suppressMouseDown: true,
marginDomNode: this.createGutterElement()
};
const zoneId = accessor.addZone(viewZone);
// editor.layout();
this._diffZones.set(editor, [zoneId]);
});
}
// gutter is the thing to the left
private createGutterElement(): HTMLElement {
const gutterDiv = document.createElement('div');
gutterDiv.className = 'inline-diff-gutter';
const minusDiv = document.createElement('div');
minusDiv.className = 'inline-diff-deleted-gutter';
// minusDiv.textContent = '-';
gutterDiv.appendChild(minusDiv);
return gutterDiv;
}
public removeDiffs(editor: ICodeEditor): void {
const decorationIds = this._diffDecorations.get(editor) || [];
editor.deltaDecorations(decorationIds, []);
this._diffDecorations.delete(editor);
editor.changeViewZones(accessor => {
const zoneIds = this._diffZones.get(editor) || [];
zoneIds.forEach(id => accessor.removeZone(id));
});
this._diffZones.delete(editor);
}
override dispose(): void {
super.dispose();
this._diffDecorations.clear();
this._diffZones.clear();
}
}
registerSingleton(IInlineDiffService, InlineDiffService, InstantiationType.Eager);
@@ -425,5 +425,3 @@
margin: 0 4px;
}
}
@@ -90,9 +90,6 @@ import './mainThreadProfileContentHandlers.js';
import './mainThreadAiRelatedInformation.js';
import './mainThreadAiEmbeddingVector.js';
// Void added this:
import './mainThreadInlineDiff.js';
export class ExtensionPoints implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.extensionPoints';
@@ -1,135 +0,0 @@
// Void created this file
// it comes from mainThreadCodeInsets.ts
import { Disposable } from '../../../base/common/lifecycle.js';
import { ICodeEditorService } from '../../../editor/browser/services/codeEditorService.js';
import { MainContext, MainThreadInlineDiffShape } from '../common/extHost.protocol.js';
import { IInlineDiffService } from '../../../editor/browser/services/inlineDiffService/inlineDiffService.js';
import { ICodeEditor } from '../../../editor/browser/editorBrowser.js';
import { IRange } from '../../../editor/common/core/range.js';
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType, UndoRedoGroup } from '../../../platform/undoRedo/common/undoRedo.js';
import { IBulkEditService } from '../../../editor/browser/services/bulkEditService.js';
import { WorkspaceEdit } from '../../../editor/common/languages.js';
import { IEnvironmentService } from '../../../platform/environment/common/environment.js';
// import { IHistoryService } from '../../services/history/common/history.js';
@extHostNamedCustomer(MainContext.MainThreadInlineDiff)
export class MainThreadInlineDiff extends Disposable implements MainThreadInlineDiffShape {
// private readonly _proxy: ExtHostEditorInsetsShape;
// private readonly _disposables = new DisposableStore();
constructor(
context: IExtHostContext,
@IInlineDiffService private readonly _inlineDiff: IInlineDiffService,
@ICodeEditorService private readonly _editorService: ICodeEditorService,
// @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
@IEnvironmentService private readonly _envService: IEnvironmentService,
) {
super();
console.log('is DEV', this._envService.isExtensionDevelopment)
// this._proxy = context.getProxy(ExtHostContext.ExtHostEditorInsets);
// this._wcHistoryService.addEntry()
}
_streamingState: { type: 'streaming'; editGroup: UndoRedoGroup } | { type: 'idle' } = { type: 'idle' }
startStreaming(editorId: string) {
const editor = this._getEditor(editorId)
if (!editor) return
const model = editor.getModel()
if (!model) return
// all changes made when streaming should be a part of the group so we can undo them all together
this._streamingState = {
type: 'streaming',
editGroup: new UndoRedoGroup()
}
// TODO probably need to convert this to a stack
const diffsSnapshotBefore = { placeholder: '' }
const diffsSnapshotAfter = { placeholder: '' }
const elt: IUndoRedoElement = {
type: UndoRedoElementType.Resource,
resource: model.uri,
label: 'Add Diffs',
code: 'undoredo.inlineDiff',
undo: () => {
// reapply diffareas and diffs here
console.log('reverting diffareas...', diffsSnapshotBefore.placeholder)
},
redo: () => {
// reapply diffareas and diffs here
// when done, need to record diffSnapshotAfter
console.log('re-applying diffareas...', diffsSnapshotAfter.placeholder)
}
}
this._undoRedoService.pushElement(elt, this._streamingState.editGroup)
// ---------- START ----------
editor.updateOptions({ readOnly: true })
// ---------- WHEN DONE ----------
editor.updateOptions({ readOnly: false })
}
streamChange(editorId: string, edit: WorkspaceEdit) {
const editor = this._getEditor(editorId)
if (!editor) return
if (this._streamingState.type !== 'streaming') {
console.error('Expected streamChange to be in state \'streaming\'.')
return
}
// count all changes towards the group
this._bulkEditService.apply(edit, { undoRedoGroupId: this._streamingState.editGroup.id, })
}
_getEditor = (editorId: string): ICodeEditor | undefined => {
let editor: ICodeEditor | undefined;
editorId = editorId.substr(0, editorId.indexOf(',')); //todo@jrieken HACK
for (const candidate of this._editorService.listCodeEditors()) {
if (candidate.getId() === editorId
// && candidate.hasModel() && isEqual(candidate.getModel().uri, URI.revive(uri))
) {
editor = candidate;
break;
}
}
return editor
}
$addDiff(editorId: string, originalText: string, range: IRange): void {
const editor = this._getEditor(editorId);
if (!editor) return
this._inlineDiff.addDiff(editor, originalText, range)
}
}
@@ -109,7 +109,6 @@ import { ProxyIdentifier } from '../../services/extensions/common/proxyIdentifie
import { ExcludeSettingOptions, TextSearchCompleteMessageType, TextSearchContextNew, TextSearchMatchNew } from '../../services/search/common/searchExtTypes.js';
import type * as vscode from 'vscode';
import { ExtHostCodeMapper } from './extHostCodeMapper.js';
import { ExtHostInlineDiff } from './extHostInlineDiff.js';
export interface IExtensionRegistries {
mine: ExtensionDescriptionRegistry;
@@ -222,9 +221,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostSpeech = rpcProtocol.set(ExtHostContext.ExtHostSpeech, new ExtHostSpeech(rpcProtocol));
const extHostEmbeddings = rpcProtocol.set(ExtHostContext.ExtHostEmbeddings, new ExtHostEmbeddings(rpcProtocol));
// Void added this:
const extHostInlineDiff = rpcProtocol.set(ExtHostContext.ExtHostInlineDiff, new ExtHostInlineDiff(rpcProtocol.getProxy(MainContext.MainThreadInlineDiff), extHostEditors));
// Check that no named customers are missing
const expected = Object.values<ProxyIdentifier<any>>(ExtHostContext);
rpcProtocol.assertRegistered(expected);
@@ -523,7 +519,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
// namespace: languages
const languages: typeof vscode.languages = {
createDiagnosticCollection(name?: string): vscode.DiagnosticCollection {
return extHostDiagnostics.createDiagnosticCollection(extension.identifier, name);
},
@@ -558,11 +553,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider);
},
// Void added addInlineDiff here:
addInlineDiff(editor: vscode.TextEditor, originalText: string, modifiedRange: vscode.Range): void {
extHostInlineDiff.addDiff(editor, originalText, modifiedRange)
},
// Void added this (I think will need to add this back when add ctrl+K)
// registerVoidCtrlKProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable {
// return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider);
@@ -2984,10 +2984,7 @@ export const MainContext = {
MainThreadTesting: createProxyIdentifier<MainThreadTestingShape>('MainThreadTesting'),
MainThreadLocalization: createProxyIdentifier<MainThreadLocalizationShape>('MainThreadLocalizationShape'),
MainThreadAiRelatedInformation: createProxyIdentifier<MainThreadAiRelatedInformationShape>('MainThreadAiRelatedInformation'),
MainThreadAiEmbeddingVector: createProxyIdentifier<MainThreadAiEmbeddingVectorShape>('MainThreadAiEmbeddingVector'),
// Void added this:
MainThreadInlineDiff: createProxyIdentifier<MainThreadInlineDiffShape>('MainThreadInlineDiff'),
MainThreadAiEmbeddingVector: createProxyIdentifier<MainThreadAiEmbeddingVectorShape>('MainThreadAiEmbeddingVector')
};
export const ExtHostContext = {
@@ -3058,17 +3055,5 @@ export const ExtHostContext = {
ExtHostTimeline: createProxyIdentifier<ExtHostTimelineShape>('ExtHostTimeline'),
ExtHostTesting: createProxyIdentifier<ExtHostTestingShape>('ExtHostTesting'),
ExtHostTelemetry: createProxyIdentifier<ExtHostTelemetryShape>('ExtHostTelemetry'),
ExtHostLocalization: createProxyIdentifier<ExtHostLocalizationShape>('ExtHostLocalization'),
// Void added this:
ExtHostInlineDiff: createProxyIdentifier<ExtHostInlineDiffShape>('ExtHostInlineDiff'), // Void added this
ExtHostLocalization: createProxyIdentifier<ExtHostLocalizationShape>('ExtHostLocalization')
};
// Void added these:
export interface ExtHostInlineDiffShape {
$onDidDispose(handle: number): void;
}
export interface MainThreadInlineDiffShape {
$addDiff(editorId: string, originalText: string, range: IRange): void;
}
@@ -1,62 +0,0 @@
// This file was created by Void
// reference extHostCodeInsets.ts
import { Emitter } from '../../../base/common/event.js';
import { DisposableStore } from '../../../base/common/lifecycle.js';
import { ExtHostInlineDiffShape, MainThreadInlineDiffShape } from './extHost.protocol.js';
import * as vscode from 'vscode'
import { ExtHostTextEditor } from './extHostTextEditor.js';
import { ExtHostEditors } from './extHostTextEditors.js';
import { Range } from '../../../workbench/api/common/extHostTypeConverters.js'
export class ExtHostInlineDiff implements ExtHostInlineDiffShape {
private readonly _disposables = new DisposableStore();
private _insets = new Map<number, { editor: vscode.TextEditor; inset: vscode.WebviewEditorInset; onDidReceiveMessage: Emitter<any> }>();
constructor(
private readonly _proxy: MainThreadInlineDiffShape,
private readonly _editors: ExtHostEditors,
) { }
dispose(): void {
this._insets.forEach(value => value.inset.dispose());
this._disposables.dispose();
}
addDiff(editor: vscode.TextEditor, originalText: string, modifiedRange: vscode.Range) {
let apiEditor: ExtHostTextEditor | undefined;
for (const candidate of this._editors.getVisibleTextEditors(true)) {
if (candidate.value === editor) {
apiEditor = <ExtHostTextEditor>candidate;
break;
}
}
if (!apiEditor) {
throw new Error('not a visible editor');
}
const id = apiEditor.id;
// let uri = apiEditor.value.document.uri;
// convert to IRange
const range = Range.from(modifiedRange)
this._proxy.$addDiff(id, originalText, range)
}
// main thread calls this when disposes diff with this particular handle
$onDidDispose(handle: number): void {
const value = this._insets.get(handle);
if (value) {
value.inset.dispose();
}
}
}
@@ -3,16 +3,16 @@ import {
Extensions as ViewContainerExtensions, IViewContainersRegistry,
ViewContainerLocation, IViewsRegistry, Extensions as ViewExtensions,
IViewDescriptor
} from '../../../common/views.js';
} from '../../../../workbench/common/views.js';
import * as nls from '../../../../nls.js';
import { VoidViewPane } from './voidViewPane.js';
import { VoidViewPane } from '../../../../workbench/contrib/void/browser/voidViewPane.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { localize } from '../../../../nls.js';
import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js';
import { ViewPaneContainer } from '../../../../workbench/browser/parts/views/viewPaneContainer.js';
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
@@ -1,7 +1,7 @@
import { ViewPane } from '../../../browser/parts/views/viewPane.js';
import { ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js';
// import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
// import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
@@ -12,13 +12,6 @@ import './browser/workbench.contribution.js';
//#endregion
//#region --- void
// Void added this:
import './contrib/void/browser/void.contribution.js';
//#endregion
//#region --- workbench actions
+1 -3
View File
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
declare module 'vscode' {
/**
* The version of the editor.
*/
@@ -14073,9 +14074,6 @@ declare module 'vscode' {
*/
export namespace languages {
/** Void added this: */
export function addInlineDiff(editor: TextEditor, originalText: string, modifiedRange: Range): void;
/**
* Return the identifiers of all known languages.
* @returns Promise resolving to an array of identifier strings.