First commit

This commit is contained in:
Mahesh Kommareddi 2024-08-07 19:30:36 -04:00
commit ca5bbb33e4
19 changed files with 4640 additions and 0 deletions

25
.eslintrc.json Normal file
View File

@ -0,0 +1,25 @@
{
"env": {
"browser": false,
"commonjs": true,
"es6": true,
"node": true,
"mocha": true
},
"parserOptions": {
"ecmaVersion": 2018,
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
"rules": {
"no-const-assign": "warn",
"no-this-before-super": "warn",
"no-undef": "warn",
"no-unreachable": "warn",
"no-unused-vars": "warn",
"constructor-super": "warn",
"valid-typeof": "warn"
}
}

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
.vscode-test/
*.vsix

5
.vscode-test.mjs Normal file
View File

@ -0,0 +1,5 @@
import { defineConfig } from '@vscode/test-cli';
export default defineConfig({
files: 'test/**/*.test.js',
});

8
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the extensions.json format
"recommendations": [
"dbaeumer.vscode-eslint",
"ms-vscode.extension-test-runner"
]
}

17
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,17 @@
// A launch configuration that launches the extension inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
]
}
]
}

10
.vscodeignore Normal file
View File

@ -0,0 +1,10 @@
.vscode/**
.vscode-test/**
test/**
.gitignore
.yarnrc
vsc-extension-quickstart.md
**/jsconfig.json
**/*.map
**/.eslintrc.json
**/.vscode-test.*

9
CHANGELOG.md Normal file
View File

@ -0,0 +1,9 @@
# Change Log
All notable changes to the "flinstones-llm" extension will be documented in this file.
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [Unreleased]
- Initial release

65
README.md Normal file
View File

@ -0,0 +1,65 @@
# flinstones-llm README
This is the README for your extension "flinstones-llm". After writing up a brief description, we recommend including the following sections.
## Features
Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file.
For example if there is an image subfolder under your extension project workspace:
\!\[feature X\]\(images/feature-x.png\)
> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow.
## Requirements
If you have any requirements or dependencies, add a section describing those and how to install and configure them.
## Extension Settings
Include if your extension adds any VS Code settings through the `contributes.configuration` extension point.
For example:
This extension contributes the following settings:
* `myExtension.enable`: Enable/disable this extension.
* `myExtension.thing`: Set to `blah` to do something.
## Known Issues
Calling out known issues can help limit users opening duplicate issues against your extension.
## Release Notes
Users appreciate release notes as you update your extension.
### 1.0.0
Initial release of ...
### 1.0.1
Fixed issue #.
### 1.1.0
Added features X, Y, and Z.
---
## Working with Markdown
You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts:
* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux)
* Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux)
* Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets
## For more information
* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown)
* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/)
**Enjoy!**

36
extension.js Normal file
View File

@ -0,0 +1,36 @@
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
const vscode = require('vscode');
// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
/**
* @param {vscode.ExtensionContext} context
*/
function activate(context) {
// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
console.log('Congratulations, your extension "flinstones-llm" is now active!');
// The command has been defined in the package.json file
// Now provide the implementation of the command with registerCommand
// The commandId parameter must match the command field in package.json
const disposable = vscode.commands.registerCommand('flinstones-llm.helloWorld', function () {
// The code you place here will be executed every time your command is executed
// Display a message box to the user
vscode.window.showInformationMessage('Hello World from flinstones-llm!');
});
context.subscriptions.push(disposable);
}
// This method is called when your extension is deactivated
function deactivate() {}
module.exports = {
activate,
deactivate
}

13
jsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"module": "Node16",
"target": "ES2022",
"checkJs": false, /* Typecheck .js files. */
"lib": [
"ES2022"
]
},
"exclude": [
"node_modules"
]
}

3598
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

97
package.json Normal file
View File

@ -0,0 +1,97 @@
{
"name": "flinstones-llm",
"displayName": "Flinstones LLM",
"description": "A VS Code extension that uses AWS Bedrock to suggest code changes",
"version": "0.0.1",
"engines": {
"vscode": "^1.60.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onCommand:flinstones-llm.processCodeChanges"
],
"main": "./src/extension.js",
"contributes": {
"menus": {
"explorer/context": [
{
"when": "resourceLangId",
"command": "flinstones-llm.addFileToContext",
"group": "flinstonesLlm@1"
},
{
"when": "explorerResourceIsFolder",
"command": "flinstones-llm.addDirectoryToContext",
"group": "flinstonesLlm@2"
}
]
},
"commands": [
{
"command": "flinstones-llm.processCodeChanges",
"title": "Flinstones LLM: Process Code Changes"
},
{
"command": "flinstones-llm.openChat",
"title": "Flinstones LLM: Open Chat"
},
{
"command": "flinstones-llm.addFileToContext",
"title": "Add to Flinstones LLM Context"
},
{
"command": "flinstones-llm.addDirectoryToContext",
"title": "Add Directory to Flinstones LLM Context"
},
{
"command": "flinstones-llm.refreshFileContent",
"title": "Flinstones LLM: Refresh File Content"
},
{
"command": "flinstones-llm.clearContext",
"title": "Flinstones LLM: Clear Context"
}
],
"viewsContainers": {
"activitybar": [
{
"id": "flinstones-llm-chat",
"title": "Flinstones LLM",
"icon": "resources/chat-icon.svg"
}
]
},
"views": {
"flinstones-llm-chat": [
{
"type": "webview",
"id": "flinstones-llm-chat-view",
"name": "Chat"
}
]
}
},
"scripts": {
"lint": "eslint .",
"pretest": "npm run lint",
"test": "node ./test/runTest.js"
},
"devDependencies": {
"@types/glob": "^7.1.3",
"@types/mocha": "^8.2.2",
"@types/node": "14.x",
"@types/vscode": "^1.60.0",
"eslint": "^7.27.0",
"glob": "^7.1.7",
"mocha": "^8.4.0",
"typescript": "^4.3.2",
"vscode-test": "^1.5.2"
},
"dependencies": {
"@aws-sdk/client-bedrock-runtime": "^3.x",
"diff": "^5.2.0",
"ignore": "^5.2.0"
}
}

3
resources/chat-icon.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/>
</svg>

After

Width:  |  Height:  |  Size: 212 B

105
src/bedrock-service.js Normal file
View File

@ -0,0 +1,105 @@
const { BedrockRuntimeClient, InvokeModelCommand } = require("@aws-sdk/client-bedrock-runtime");
const fs = require('fs').promises;
class BedrockService {
constructor() {
this.client = new BedrockRuntimeClient({ region: "us-east-1" }); // Set your region
}
async invokeModel(prompt, imagePath) {
if (!this.client) {
throw new Error('Bedrock client not initialized');
}
console.log("DEBUG: Generating response for prompt:", prompt);
try {
const requestBody = {
anthropic_version: "bedrock-2023-05-31",
max_tokens: 20000,
temperature: 0.7,
top_p: 0.9,
messages: [
{
role: "user",
content: []
}
]
};
if (imagePath) {
const imageBuffer = await fs.readFile(imagePath);
const base64Image = imageBuffer.toString('base64');
requestBody.messages[0].content.push({
type: "image",
source: {
type: "base64",
media_type: "image/jpeg",
data: base64Image
}
});
}
const improvedPrompt = `
${prompt}
When suggesting changes to files, please provide a unified diff format that includes line numbers. Use the following format for each file change:
File: /path/to/file.js
\`\`\`diff
@@ -<old_start>,<old_count> +<new_start>,<new_count> @@
context line
-removed line
+added line
context line
\`\`\`
Ensure that:
1. The @@ line includes accurate line numbers for where the changes should be applied.
2. The <old_start> represents the line number in the original file where changes begin.
3. The <old_count> is the number of lines from the original file in this chunk.
4. The <new_start> represents the line number in the new file where changes begin.
5. The <new_count> is the number of lines in the new file in this chunk.
6. Provide at least 3 lines of context before and after the changes.
7. For multiple changes in the same file, provide separate @@ blocks for each change.
`;
requestBody.messages[0].content.push({
type: "text",
text: improvedPrompt
});
const params = {
modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
contentType: "application/json",
accept: "application/json",
body: JSON.stringify(requestBody),
};
const command = new InvokeModelCommand(params);
const response = await this.client.send(command);
const responseBody = JSON.parse(new TextDecoder().decode(response.body));
console.log("DEBUG: Raw response from Bedrock:", JSON.stringify(responseBody));
if (responseBody.content && Array.isArray(responseBody.content) && responseBody.content.length > 0) {
const generatedText = responseBody.content[0].text;
if (generatedText) {
console.log("DEBUG: Generated text:", generatedText);
return generatedText;
} else {
console.log("WARNING: Generated text is empty. Full response:", JSON.stringify(responseBody));
return "No response generated";
}
} else {
console.log("WARNING: Unexpected response format. Full response:", JSON.stringify(responseBody));
return "Unexpected response format";
}
} catch (error) {
console.error("ERROR: Failed to generate text with Bedrock:", error);
return "Error: " + error.message;
}
}
}
module.exports = BedrockService;

323
src/chat-view.js Normal file
View File

@ -0,0 +1,323 @@
const vscode = require('vscode');
const BedrockService = require('./bedrock-service');
const path = require('path');
const fs = require('fs').promises;
const diff = require('diff');
class ChatViewProvider {
constructor(context) {
this.context = context;
this.bedrockService = new BedrockService();
this._view = null;
this.contextContent = '';
this.fileContents = new Map();
}
resolveWebviewView(webviewView) {
this._view = webviewView;
webviewView.webview.options = {
enableScripts: true,
};
webviewView.webview.html = this.getWebviewContent();
webviewView.webview.onDidReceiveMessage(async (data) => {
if (data.type === 'userInput') {
const fullPrompt = `${this.contextContent}\n\nUser: ${data.value}`;
const response = await this.bedrockService.invokeModel(fullPrompt);
this._view.webview.postMessage({ type: 'aiResponse', value: response });
const fileChanges = this.extractFileChanges(response);
if (fileChanges.length > 0) {
await this.handleFileChanges(fileChanges);
}
} else if (data.type === 'refreshContent') {
await this.refreshFileContent();
} else if (data.type === 'clearContext') {
this.clearContext();
}
});
}
addToContext(content) {
this.contextContent += content;
if (this._view) {
this._view.webview.postMessage({ type: 'contextUpdate', value: this.contextContent });
}
}
extractFileChanges(response) {
const fileChangeRegex = /File: (.*?)\n```diff\n([\s\S]*?)\n```/g;
const fileChanges = [];
let match;
while ((match = fileChangeRegex.exec(response)) !== null) {
fileChanges.push({
filename: match[1],
diff: match[2].trim() // Trim to remove any leading/trailing whitespace
});
}
return fileChanges;
}
async handleFileChanges(fileChanges) {
for (const change of fileChanges) {
console.log(`Processing changes for file: ${change.filename}`);
console.log(`Diff content:\n${change.diff}`);
// Try to find the file in the workspace
const files = await vscode.workspace.findFiles(`**/${path.basename(change.filename)}`);
let uri;
if (files.length > 0) {
uri = files[0];
} else {
// If file not found, ask the user if they want to create it
const create = await vscode.window.showQuickPick(['Yes', 'No'], {
placeHolder: `File ${change.filename} not found. Create it?`
});
if (create === 'Yes') {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (workspaceFolders) {
uri = vscode.Uri.joinPath(workspaceFolders[0].uri, change.filename);
await vscode.workspace.fs.writeFile(uri, new Uint8Array());
} else {
vscode.window.showErrorMessage('No workspace folder is open');
continue;
}
} else {
vscode.window.showInformationMessage(`Skipped changes for ${change.filename}`);
continue;
}
}
let document;
try {
document = await vscode.workspace.openTextDocument(uri);
} catch (error) {
console.error(`Failed to open file: ${change.filename}. Error: ${error.message}`);
vscode.window.showErrorMessage(`Failed to open file: ${change.filename}. Error: ${error.message}`);
continue;
}
const originalContent = document.getText();
console.log(`Original content:\n${originalContent}`);
const updatedContent = this.applyDiff(originalContent, change.diff);
console.log(`Updated content:\n${updatedContent}`);
// Show diff and ask for approval
const diffHtml = await this.createDiffHtml(originalContent, updatedContent);
const approved = await this.showDiffAndConfirm(diffHtml, change.filename);
if (approved) {
const edit = new vscode.WorkspaceEdit();
edit.replace(uri, new vscode.Range(0, 0, document.lineCount, 0), updatedContent);
await vscode.workspace.applyEdit(edit);
vscode.window.showInformationMessage(`Changes applied to ${path.basename(change.filename)}`);
} else {
vscode.window.showInformationMessage(`Changes to ${path.basename(change.filename)} were not applied`);
}
}
}
applyDiff(originalContent, diffContent) {
const lines = originalContent.split('\n');
const diffLines = diffContent.split('\n');
let lineIndex = 0;
for (const diffLine of diffLines) {
if (diffLine.startsWith('@@')) {
const match = diffLine.match(/@@ -(\d+),\d+ \+(\d+),\d+ @@/);
if (match) {
lineIndex = parseInt(match[1]) - 1; // -1 because array indices start at 0
}
} else if (diffLine.startsWith('+')) {
lines.splice(lineIndex, 0, diffLine.slice(1));
lineIndex++;
} else if (diffLine.startsWith('-')) {
lines.splice(lineIndex, 1);
} else if (!diffLine.startsWith(' ')) {
lineIndex++;
}
}
return lines.join('\n');
}
async createDiffHtml(oldContent, newContent) {
const differences = diff.createTwoFilesPatch('old', 'new', oldContent, newContent);
return `
<style>
.diff { white-space: pre; font-family: monospace; }
.diff-add { background-color: #e6ffed; }
.diff-remove { background-color: #ffeef0; }
</style>
<div class="diff">
${differences.split('\n').map(line => {
if (line.startsWith('+')) return `<div class="diff-add">${this.escapeHtml(line)}</div>`;
if (line.startsWith('-')) return `<div class="diff-remove">${this.escapeHtml(line)}</div>`;
return `<div>${this.escapeHtml(line)}</div>`;
}).join('')}
</div>
`;
}
escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
async showDiffAndConfirm(diffHtml, filename) {
return new Promise((resolve) => {
const panel = vscode.window.createWebviewPanel(
'diffView',
`Proposed Changes for ${filename}`,
vscode.ViewColumn.One,
{ enableScripts: true }
);
panel.webview.html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Proposed Changes</title>
</head>
<body>
<h2>Proposed Changes for ${this.escapeHtml(filename)}</h2>
${diffHtml}
<button id="approve">Approve Changes</button>
<button id="reject">Reject Changes</button>
<script>
const vscode = acquireVsCodeApi();
document.getElementById('approve').addEventListener('click', () => {
vscode.postMessage({ type: 'approve' });
});
document.getElementById('reject').addEventListener('click', () => {
vscode.postMessage({ type: 'reject' });
});
</script>
</body>
</html>
`;
panel.webview.onDidReceiveMessage(
message => {
if (message.type === 'approve') {
resolve(true);
} else if (message.type === 'reject') {
resolve(false);
}
panel.dispose();
},
undefined,
this.context.subscriptions
);
});
}
async refreshFileContent() {
this.fileContents.clear();
for (const [filePath, _] of this.fileContents) {
await this.addFileToContext(vscode.Uri.file(filePath));
}
this.updateContextView();
}
clearContext() {
this.contextContent = '';
this.fileContents.clear();
this.updateContextView();
}
async addFileToContext(uri) {
const content = await fs.readFile(uri.fsPath, 'utf8');
this.fileContents.set(uri.fsPath, content);
this.updateContextView();
}
updateContextView() {
let contextContent = '';
for (const [filePath, content] of this.fileContents) {
contextContent += `File: ${filePath}\n\n${content}\n\n`;
}
this.contextContent = contextContent;
if (this._view) {
this._view.webview.postMessage({ type: 'contextUpdate', value: this.contextContent });
}
}
getWebviewContent() {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flinstones LLM Chat</title>
<style>
body { font-family: Arial, sans-serif; padding: 10px; }
#chat-container { height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; }
#context-container { height: 100px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; }
#user-input { width: 100%; padding: 5px; }
.button { margin-right: 10px; }
</style>
</head>
<body>
<div id="context-container"></div>
<div id="chat-container"></div>
<input type="text" id="user-input" placeholder="Type your message...">
<button id="refresh-content" class="button">Refresh File Content</button>
<button id="clear-context" class="button">Clear Context</button>
<script>
const vscode = acquireVsCodeApi();
const contextContainer = document.getElementById('context-container');
const chatContainer = document.getElementById('chat-container');
const userInput = document.getElementById('user-input');
const refreshButton = document.getElementById('refresh-content');
const clearButton = document.getElementById('clear-context');
userInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const message = userInput.value;
chatContainer.innerHTML += '<p><strong>You:</strong> ' + message + '</p>';
vscode.postMessage({ type: 'userInput', value: message });
userInput.value = '';
}
});
refreshButton.addEventListener('click', () => {
vscode.postMessage({ type: 'refreshContent' });
});
clearButton.addEventListener('click', () => {
vscode.postMessage({ type: 'clearContext' });
});
window.addEventListener('message', (event) => {
const message = event.data;
if (message.type === 'aiResponse') {
chatContainer.innerHTML += '<p><strong>AI:</strong> ' + message.value + '</p>';
chatContainer.scrollTop = chatContainer.scrollHeight;
} else if (message.type === 'contextUpdate') {
contextContainer.textContent = message.value;
}
});
</script>
</body>
</html>
`;
}
}
module.exports = ChatViewProvider;

219
src/extension.js Normal file
View File

@ -0,0 +1,219 @@
const vscode = require("vscode");
const fs = require('fs').promises;
const path = require('path');
const ignore = require('ignore');
const BedrockService = require("./bedrock-service");
const { getDiff, applyPatch } = require("./git-utils");
const ChatViewProvider = require("./chat-view");
let chatViewProvider;
let ig;
async function processCodeChanges() {
const bedrockService = new BedrockService();
// Get the current workspace folder
const workspaceFolder = vscode.workspace.workspaceFolders[0];
if (!workspaceFolder) {
vscode.window.showErrorMessage("No workspace folder is open");
return;
}
// Get the file tree
const fileTree = await getFileTree(workspaceFolder.uri.fsPath);
// Get the contents of all files
const fileContents = await getAllFileContents(workspaceFolder.uri.fsPath);
const prompt = `Given the following file tree and file contents, suggest code improvements:
File Tree:
${JSON.stringify(fileTree, null, 2)}
File Contents:
${JSON.stringify(fileContents, null, 2)}
Provide your suggestions in the form of a git patch for each file that needs changes.`;
const response = await bedrockService.invokeModel(prompt);
// Extract the patches from the response
const patches = extractPatchesFromResponse(response);
if (patches.length > 0) {
for (const patch of patches) {
const shouldApply = await vscode.window.showQuickPick(["Yes", "No"], {
placeHolder: `Apply the suggested changes to ${patch.filename}?`,
});
if (shouldApply === "Yes") {
await applyPatch(patch.content, patch.filename);
vscode.window.showInformationMessage(`Changes applied successfully to ${patch.filename}!`);
}
}
} else {
vscode.window.showWarningMessage("No valid patches found in the response.");
}
}
async function getFileTree(dir) {
const files = await fs.readdir(dir);
const fileTree = {};
for (const file of files) {
const filePath = path.join(dir, file);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
fileTree[file] = await getFileTree(filePath);
} else {
fileTree[file] = null;
}
}
return fileTree;
}
async function getAllFileContents(dir) {
const files = await vscode.workspace.findFiles('**/*', '**/node_modules/**');
const fileContents = {};
for (const file of files) {
const relativePath = path.relative(dir, file.fsPath);
const content = await fs.readFile(file.fsPath, 'utf8');
fileContents[relativePath] = content;
}
return fileContents;
}
function extractPatchesFromResponse(response) {
const patchRegex = /File: (.*?)\n```diff\n([\s\S]*?)\n```/g;
const patches = [];
let match;
while ((match = patchRegex.exec(response)) !== null) {
patches.push({
filename: match[1],
content: match[2]
});
}
return patches;
}
async function initializeIgnore(workspaceRoot) {
ig = ignore();
try {
const gitignorePath = path.join(workspaceRoot, '.gitignore');
const gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
ig.add(gitignoreContent);
} catch (error) {
// .gitignore file doesn't exist or couldn't be read
console.log("No .gitignore file found or couldn't be read");
}
}
async function addFileToContext(uri) {
if (!uri) {
uri = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
filters: {
'Text files': ['cjs', 'svelte', 'txt', 'md', 'js', 'ts', 'py', 'java', 'c', 'cpp', 'cs', 'html', 'css', 'json']
}
}).then(fileUri => fileUri[0]);
}
if (uri) {
const content = await fs.readFile(uri.fsPath, 'utf8');
const lines = content.split('\n');
const numberedContent = lines.map((line, index) => `${index + 1}: ${line}`).join('\n');
chatViewProvider.addToContext(`File: ${uri.fsPath}\n\n${numberedContent}\n\n`);
vscode.window.showInformationMessage(`Added ${path.basename(uri.fsPath)} to context with line numbers`);
}
}
async function addDirectoryToContext(uri) {
if (!uri) {
uri = await vscode.window.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false
}).then(folderUri => folderUri[0]);
}
if (uri) {
await initializeIgnore(uri.fsPath);
await addFilesRecursively(uri);
vscode.window.showInformationMessage(`Added files from ${uri.fsPath} and its subdirectories to context (respecting .gitignore)`);
}
}
async function addFilesRecursively(uri) {
const stats = await fs.stat(uri.fsPath);
if (stats.isDirectory()) {
const entries = await fs.readdir(uri.fsPath, { withFileTypes: true });
for (const entry of entries) {
const entryUri = vscode.Uri.file(path.join(uri.fsPath, entry.name));
const relativePath = path.relative(vscode.workspace.workspaceFolders[0].uri.fsPath, entryUri.fsPath);
if (!ig.ignores(relativePath)) {
if (entry.isDirectory()) {
await addFilesRecursively(entryUri);
} else {
await addFileIfSupported(entryUri);
}
}
}
} else {
await addFileIfSupported(uri);
}
}
async function addFileIfSupported(uri) {
const supportedExtensions = ['.cjs', '.svelte', '.txt', '.md', '.js', '.ts', '.py', '.java', '.c', '.cpp', '.cs', '.html', '.css', '.json'];
const fileExtension = path.extname(uri.fsPath).toLowerCase();
if (supportedExtensions.includes(fileExtension)) {
const relativePath = path.relative(vscode.workspace.workspaceFolders[0].uri.fsPath, uri.fsPath);
if (!ig.ignores(relativePath)) {
await addFileToContext(uri);
}
}
}
function activate(context) {
chatViewProvider = new ChatViewProvider(context);
let processCodeChangesDisposable = vscode.commands.registerCommand('flinstones-llm.processCodeChanges', processCodeChanges);
let chatViewDisposable = vscode.window.registerWebviewViewProvider('flinstones-llm-chat-view', chatViewProvider);
let openChatDisposable = vscode.commands.registerCommand('flinstones-llm.openChat', () => {
vscode.commands.executeCommand('workbench.view.extension.flinstones-llm-chat');
});
let addFileDisposable = vscode.commands.registerCommand('flinstones-llm.addFileToContext', addFileToContext);
let addDirectoryDisposable = vscode.commands.registerCommand('flinstones-llm.addDirectoryToContext', addDirectoryToContext);
// New commands
let refreshFileContentDisposable = vscode.commands.registerCommand('flinstones-llm.refreshFileContent', () => chatViewProvider.refreshFileContent());
let clearContextDisposable = vscode.commands.registerCommand('flinstones-llm.clearContext', () => chatViewProvider.clearContext());
context.subscriptions.push(
processCodeChangesDisposable,
chatViewDisposable,
openChatDisposable,
addFileDisposable,
addDirectoryDisposable,
refreshFileContentDisposable,
clearContextDisposable
);
}
function deactivate() {}
module.exports = {
activate,
deactivate
};

48
src/git-utils.js Normal file
View File

@ -0,0 +1,48 @@
const { exec } = require('child_process');
const { promisify } = require('util');
const vscode = require('vscode');
const execAsync = promisify(exec);
async function getDiff() {
const { stdout } = await execAsync('git diff');
return stdout;
}
async function applyPatch(patch, filePath) {
const uri = vscode.Uri.file(filePath);
const document = await vscode.workspace.openTextDocument(uri);
const edit = new vscode.WorkspaceEdit();
const originalContent = document.getText();
const updatedContent = applyDiffToContent(originalContent, patch);
edit.replace(uri, new vscode.Range(0, 0, document.lineCount, 0), updatedContent);
await vscode.workspace.applyEdit(edit);
}
function applyDiffToContent(originalContent, patch) {
const lines = originalContent.split('\n');
const diffLines = patch.split('\n');
let lineIndex = 0;
for (const diffLine of diffLines) {
if (diffLine.startsWith('@@')) {
const match = diffLine.match(/@@ -(\d+),\d+ \+(\d+),\d+ @@/);
if (match) {
lineIndex = parseInt(match[1]) - 1;
}
} else if (diffLine.startsWith('+')) {
lines.splice(lineIndex, 0, diffLine.slice(1));
lineIndex++;
} else if (diffLine.startsWith('-')) {
lines.splice(lineIndex, 1);
} else if (!diffLine.startsWith(' ')) {
lineIndex++;
}
}
return lines.join('\n');
}
module.exports = { getDiff, applyPatch };

15
test/extension.test.js Normal file
View File

@ -0,0 +1,15 @@
const assert = require('assert');
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
const vscode = require('vscode');
// const myExtension = require('../extension');
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
test('Sample test', () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
});
});

View File

@ -0,0 +1,41 @@
# Welcome to your VS Code Extension
## What's in the folder
* This folder contains all of the files necessary for your extension.
* `package.json` - this is the manifest file in which you declare your extension and command.
* The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesnt yet need to load the plugin.
* `extension.js` - this is the main file where you will provide the implementation of your command.
* The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
* We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
## Get up and running straight away
* Press `F5` to open a new window with your extension loaded.
* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
* Set breakpoints in your code inside `extension.js` to debug your extension.
* Find output from your extension in the debug console.
## Make changes
* You can relaunch the extension from the debug toolbar after changing code in `extension.js`.
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
## Explore the API
* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
## Run tests
* Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner)
* Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A`
* See the output of the test result in the Test Results view.
* Make changes to `test/extension.test.js` or create new test files inside the `test` folder.
* The provided test runner will only consider files matching the name pattern `**.test.js`.
* You can create folders inside the `test` folder to structure your tests any way you want.
## Go further
* [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns.
* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace.
* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).