diff --git a/README.md b/README.md index ee1035c..89dd0dc 100644 --- a/README.md +++ b/README.md @@ -1 +1,91 @@ -# code4me.ai \ No newline at end of file +

+ +Code4Me.ai Logo + +

+ +## Code4Me.AI +Imagine an AI-powered tool just within your IDE that turns your ideas into code through natural language—our VS Code extension does precisely that. The Code4Me.ai extension not only aids in code generation and auto-debugging but also excels in generating unit tests and crafting quality documentation, boosting productivity and development speed by 10x for both novices and expert developers alike. + +## Feature Summary +

+ + Alt text + +

+ +### You're still in control! + +Stay in control and in the loop: Code4Me.ai not only generates code but also presents it just like a Git diff. This way, you can review, tweak, or directly accept the AI-crafted code, ensuring it perfectly aligns with what you want. + +

+ + Alt text + +

+ +## How to install + +
+Installation guide + +## 1. Download the .vsix File +- The `.vsix` file for `Code4Me.ai` is available in this GitHub repository. +- You can either clone the repository or download it as a ZIP file. + - To clone, use: `git clone [Repository URL]` + - Or download the ZIP file +## 2. Open Visual Studio Code +- Launch Visual Studio Code on your computer. + +## 3. Access the Extensions View +- Click on the Extensions icon in the Activity Bar on the side of the window. +- In the Extensions view, click on the `...` (More Actions) button at the top right. +- Select `Install from VSIX...` from the dropdown menu. + +

+ + Alt text + +

+ +## 4. Locate and Select the .vsix File +- Navigate to where you cloned/downloaded the repo in the file dialog. +- Select the `.vsix` file and click `Open`. +- VS Code will now install the extension. + +## 5. Reload/Restart VS Code +- You may need to reload VS Code to activate the extension. +- Click the `Reload` button if prompted, or restart VS Code. + +## 8. Verify Installation +- Check the Extensions view to ensure the extension is listed as installed. + +## 9. OpenAI Key +- On the first use you'll be prompted to add your openai api key. + +
+ + +## How it works 🛠️⚙️ + +

+ + Alt text + +

+ +1. **Grab the Current Code:** + + First, our tool looks at what you're working on. It takes the code from the open file in your editor. If the file is new and empty, that's okay too—our AI can handle that. + +2. **Your Instructions** + + Next, you tell the tool what you need. You can type this in a box at the bottom left of the AI Coding panel. We've also got some common tasks ready to go, like adding packages or making tests. + +3. **AI Does Its Thing (OpenAI Completions API)** + + Then, we send your instructions and the code to the AI. It starts working and keeps going until it's done writing the code you need. + +4. **You Check the Code** + + Finally, you get to see the new code next to your old one, just like when you look at changes in Git. If you like what you see, hit accept, and the new code gets added to your file. Simple as that! \ No newline at end of file diff --git a/code4me-0.0.2.vsix b/code4me-0.0.2.vsix new file mode 100644 index 0000000..eb9be22 Binary files /dev/null and b/code4me-0.0.2.vsix differ diff --git a/code4me_banner.jpeg b/code4me_banner.jpeg new file mode 100644 index 0000000..c2a4018 Binary files /dev/null and b/code4me_banner.jpeg differ diff --git a/icon-small.jpg b/icon-small.jpg new file mode 100644 index 0000000..ce99775 Binary files /dev/null and b/icon-small.jpg differ diff --git a/images/diff.png b/images/diff.png new file mode 100644 index 0000000..bc31a70 Binary files /dev/null and b/images/diff.png differ diff --git a/images/features.png b/images/features.png new file mode 100644 index 0000000..565eb03 Binary files /dev/null and b/images/features.png differ diff --git a/images/install.png b/images/install.png new file mode 100644 index 0000000..26a458e Binary files /dev/null and b/images/install.png differ diff --git a/images/system.jpeg b/images/system.jpeg new file mode 100644 index 0000000..622457a Binary files /dev/null and b/images/system.jpeg differ diff --git a/images/view.png b/images/view.png new file mode 100644 index 0000000..45ddfc9 Binary files /dev/null and b/images/view.png differ diff --git a/images/vsix.png b/images/vsix.png new file mode 100644 index 0000000..7ed6714 Binary files /dev/null and b/images/vsix.png differ diff --git a/package-lock.json b/package-lock.json index dccf7ef..031958c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "Code4Me.ai", - "version": "0.0.1", + "name": "Code4Me", + "version": "0.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index cd87d5d..2e0158f 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { - "name": "Code4Me.ai", + "name": "code4me", "displayName": "Code4Me.ai", - "description": "Code4Me.ai is a VS Code extension that uses OpenAI's gpt models to generate code from natural language.", - "version": "0.0.1", + "description": "codeformeai is a VS Code extension that uses OpenAI's gpt models to generate code from natural language.", + "version": "0.0.2", "publisher": "Team Afri-Can", "icon": "icon-small.jpg", "repository": { "type": "git", - "url": "https://github.com/Paulooh007/code4me.ai/blob/main/LICENSE" + "url": "https://github.com/Paulooh007/code4me.ai" }, "engines": { "vscode": "^1.65.0" @@ -29,7 +29,7 @@ { "type": "webview", "id": "codegen.editing", - "name": "Code4Me.ai" + "name": "codeformeai" } ] }, diff --git a/src/codegen.ts b/src/codegen.ts index d968799..02b79c0 100644 --- a/src/codegen.ts +++ b/src/codegen.ts @@ -3,11 +3,16 @@ import * as generator from './generator'; import * as vscode from 'vscode'; import * as fs from 'fs'; import { getState } from './state'; +import * as path from 'path'; + + + const doneText = '[x]'; -const generatedFolder = '/.ai/'; +const generatedFolder = '.ai'; const apiKeyName = 'openaiApiKey'; + async function getPendingCommands(commands: string[]): Promise { return commands.filter(command => { return !command.includes(doneText); @@ -22,11 +27,18 @@ async function markCommandAsDone(command: any, filePath: string) { async function saveResult(currentFolder: string, result: string, outputFileName: string): Promise { return new Promise((resolve, reject) => { - if (!fs.existsSync(currentFolder + generatedFolder)) { - fs.mkdirSync(currentFolder + generatedFolder, { recursive: true }); + const generatedFolderPath = path.join(currentFolder, generatedFolder); // Construct the path using path.join + + // Check if the generated folder exists, if not, create it + if (!fs.existsSync(generatedFolderPath)) { + fs.mkdirSync(generatedFolderPath, { recursive: true }); } - let files = fs.readdirSync(currentFolder + generatedFolder); + + // Read the existing files in the generated folder + let files = fs.readdirSync(generatedFolderPath); let index = 0; + + // Iterate through the files to find the highest index for (let i = 0; i < files.length; i++) { if (files[i].endsWith('.codegen')) { let fileName = files[i].split('.')[0]; @@ -37,13 +49,15 @@ async function saveResult(currentFolder: string, result: string, outputFileName: } } - let filePath = currentFolder + generatedFolder + outputFileName; + // Construct the full path for the new file + let filePath = path.join(generatedFolderPath, outputFileName); + // Write the result to the new file fs.writeFile(filePath, result, (err) => { if (err) { - reject(err); + reject(err); // Reject the promise if an error occurs } - resolve(filePath); + resolve(filePath); // Resolve the promise with the new file path }); }); } @@ -86,7 +100,6 @@ async function generateNextCode(state: any) { try { console.log("processing.cmd: ", processing.command); code = await generator.generateCode(processing.command, processing.files[processing.index]); - // console.log("processing.cmd: ", processing.command);` console.log("CODE: ", code); } catch(e) { vscode.window.showErrorMessage('Error generating code. ' + e); @@ -103,17 +116,17 @@ async function generateNextCode(state: any) { // Check if file exists in current folder if exists show diff else show code let currentFolder = processing.currentFolder; let filePath = processing.files[processing.index]; - if (!filePath.startsWith('/')) { - filePath = currentFolder + '/' + filePath; + if (!path.isAbsolute(filePath)) { + filePath = path.join(currentFolder, filePath); } - let fileName = filePath.split('/').pop(); + let fileName = path.basename(filePath); let generatedFilePath = null; if (fs.existsSync(filePath)) { generatedFilePath = await saveResult(currentFolder, code, fileName); await vscode.commands.executeCommand('vscode.diff', vscode.Uri.file(filePath), vscode.Uri.file(generatedFilePath)); } else { - generatedFilePath = await saveToFileAndShowResult(processing.currentFolder, code, fileName); + generatedFilePath = await saveToFileAndShowResult(currentFolder, code, fileName); } processing.codeFilePath = generatedFilePath; state.set('processing', processing); @@ -121,44 +134,43 @@ async function generateNextCode(state: any) { return true; } + export async function cancel(state: any) { let processing = state.get('processing'); if (processing && processing.codeFilePath) { // Move to generated subfolder (same level as original) let filePath = processing.codeFilePath; - let fileName = filePath.split('/').pop(); - let folder = filePath.split('/').slice(0, -1).join('/'); - let generatedFolder = folder + '/generated/'; + let fileName = path.basename(filePath); + let folder = path.dirname(filePath); + let generatedFolder = path.join(folder, 'generated'); + if (!fs.existsSync(generatedFolder)) { fs.mkdirSync(generatedFolder, { recursive: true }); } - let newFilePath = generatedFolder+ fileName; + + let newFilePath = path.join(generatedFolder, fileName); fs.renameSync(filePath, newFilePath); } + vscode.commands.executeCommand('workbench.action.closeActiveEditor'); state.stopLooping(); } function applyChanges(processingState: any) { - // If file exists, replace it with the generated code else create a new file and paste the code let currentFolder = processingState.currentFolder; let file = processingState.files[processingState.index - 1]; - let filePath = file; - if (!filePath.startsWith('/')) { - filePath = currentFolder + '/' + filePath; - } + let filePath = path.isAbsolute(file) ? file : path.join(currentFolder, file); let code = fs.readFileSync(processingState.codeFilePath, 'utf8'); - // Move processingState.codeFilePath to subfolder generated on the same level as the file - let codeFileFolder = processingState.codeFilePath.split('/'); - codeFileFolder.pop(); - codeFileFolder.push('generated'); - codeFileFolder = codeFileFolder.join('/'); + + // Move processingState.codeFilePath to subfolder 'generated' on the same level as the file + let codeFileFolder = path.join(path.dirname(processingState.codeFilePath), 'generated'); if (!fs.existsSync(codeFileFolder)) { fs.mkdirSync(codeFileFolder, { recursive: true }); } - let codeFileName = processingState.codeFilePath.split('/').pop(); - let codeFilePath = codeFileFolder + '/' + codeFileName; + + let codeFileName = path.basename(processingState.codeFilePath); + let codeFilePath = path.join(codeFileFolder, codeFileName); fs.renameSync(processingState.codeFilePath, codeFilePath); if (fs.existsSync(filePath)) { @@ -166,9 +178,7 @@ function applyChanges(processingState: any) { fs.writeFileSync(filePath, code); } else { // Create file folder recursively if not exists - let fileFolder = filePath.split('/'); - fileFolder.pop(); - let folderPath = fileFolder.join('/'); + let folderPath = path.dirname(filePath); if (!fs.existsSync(folderPath)) { fs.mkdirSync(folderPath, { recursive: true }); } @@ -203,12 +213,14 @@ export async function accept(state: any) { async function startHumanAILoop(command: any, currentFolder: string, state: any) { command.folder = currentFolder; if (command.type === 'test') { - let fileName = command.file.split('/').pop(); - let testsFolder = currentFolder + '/tests/'; - let unitTestFile = testsFolder + fileName; + let fileName = path.basename(command.file); + let testsFolder = path.join(currentFolder, 'tests'); + let unitTestFile = path.join(testsFolder, fileName); + if (!fs.existsSync(testsFolder)) { fs.mkdirSync(testsFolder, { recursive: true }); } + // Create empty file (if not exists) if (!fs.existsSync(unitTestFile)) { fs.writeFileSync(unitTestFile, ''); @@ -218,27 +230,25 @@ async function startHumanAILoop(command: any, currentFolder: string, state: any) 'state': 'files', 'command': command, 'currentFolder': currentFolder, - 'files': [ - unitTestFile - ], + 'files': [unitTestFile], 'index': 0 }); state.bar.loading.show(); await accept(state); return; + } else if (command.type === 'edit') { state.set('processing', { 'state': 'files', 'command': command, 'currentFolder': currentFolder, - 'files': [ - command.file - ], + 'files': [command.file], 'index': 0 }); state.bar.loading.show(); await accept(state); return; + } else { let result = await generator.generateFilesToModify(command); await saveToFileAndShowResult(currentFolder, result.code, 'files.md'); @@ -261,11 +271,11 @@ export async function generate(context: vscode.ExtensionContext, state: any, cur return; } - let currentFolder = currentFile.substring(0, currentFile.lastIndexOf('/')); + let currentFolder = path.dirname(currentFile); let inputText = fs.readFileSync(currentFile, 'utf8'); let commands = await parser.parse(inputText); let pendingCommands = await getPendingCommands(commands); - // Show message box to the user if there are no pending commands + if (pendingCommands.length === 0) { vscode.window.showInformationMessage('No pending commands'); return; diff --git a/src/extension.ts b/src/extension.ts index c394538..2ac600c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,8 +3,9 @@ import * as codegen from './codegen'; import * as generator from './generator'; import HumanAILoopView from './views'; import { getState } from './state'; -import { config } from 'process'; -import { get } from 'http'; +import * as path from 'path'; +import { promises as fs } from 'fs'; + async function editCode(context: vscode.ExtensionContext, text:string) { if (getState().isGenerating()) { @@ -31,10 +32,22 @@ async function editCode(context: vscode.ExtensionContext, text:string) { await codegen.edit(context, getState(), text, currentFile, currentFolder); } -function documentCode(context: vscode.ExtensionContext, text:string) { - text = 'Improve code quality of the code in triple backticks, do this by add docstrings, comments, and refactoring. \ - Return the new code as a string and do not add any extra comments, just code as a string'; - return editCode(context, text); +function generateInstallCommandPrompt(code: string): string { + return `Return the correct one-line command to install the dependencies needed in the following code \ + if no external dependencies are needed, return "None": + \`\`\` + ${code} + \`\`\``; +} + +function generateDocumentationPrompt(code: string): string { + return `Generate a good readme for the following code, \ + It should include all the elements of a good readme and documentation best practices, \ + return only the content of the readme as markdown, \ + no extra comments from you: + \`\`\` + ${code} + \`\`\``; } export function activate(context: vscode.ExtensionContext) { @@ -44,9 +57,38 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.commands.registerCommand('codegen.edit', (text) => { editCode(context, text); })); - context.subscriptions.push(vscode.commands.registerCommand('codegen.docs', (text) => { - documentCode(context, text); + // documentCode(context, text); + // })); + + context.subscriptions.push(vscode.commands.registerCommand('codegen.docs', async () => { + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return; + } + + const currentFolder = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri)?.uri.fsPath; + if (!currentFolder) { + return; + } + + const currentFile = activeEditor.document.fileName; + + const currentFilenameBase = path.basename(currentFile, path.extname(currentFile)); + const docsFolderPath = path.join(currentFolder, 'docs'); + const mdFilePath = path.join(docsFolderPath, `readme_${currentFilenameBase}.md`); + + try { + await fs.mkdir(docsFolderPath, { recursive: true }); + const content = await generator.getOpenAIResponse(generateDocumentationPrompt(currentFile), context); // Adjust as per your implementation + await fs.writeFile(mdFilePath, content); + + vscode.window.showInformationMessage('Documentation generated successfully.'); + } catch (error) { + vscode.window.showErrorMessage(`Error generating documentation: ${error instanceof Error ? error.message : String(error)}`); + } })); + + context.subscriptions.push(vscode.commands.registerCommand('codegen.humanailoop.accept', async () => { await codegen.accept(getState()); provider.clearInput(); @@ -59,29 +101,34 @@ export function activate(context: vscode.ExtensionContext) { if (getState().isGenerating()) { return; } - // Add status bar item and wait for click if (!vscode.window.activeTextEditor) { return; } let currentFileText = vscode.window.activeTextEditor.document.getText(); - + getState().startGenerating(); getState().bar.loading.show(); let code = ''; try { - code = await generator.getInstallCommand(currentFileText, context); - } - catch (e) { + code = await generator.getOpenAIResponse(generateInstallCommandPrompt(currentFileText), context); + } catch (e) { vscode.window.showErrorMessage('Error generating code.'); - console.log(e) + console.log(e); getState().endGenerating(); getState().bar.loading.hide(); return; + } finally { + getState().endGenerating(); + getState().bar.loading.hide(); } - getState().endGenerating(); - getState().bar.loading.hide(); - - // Get first line only + + // Check if the code is 'None' + if (code.trim().toLowerCase() === 'none') { + vscode.window.showInformationMessage('No installation needed.'); + return; + } + + // Ensure code ends with a newline if (!code.endsWith('\n')) { code += '\n'; } @@ -91,6 +138,8 @@ export function activate(context: vscode.ExtensionContext) { terminal.show(); terminal.sendText(code, true); // 'true' means the text is executed (like pressing Enter) })); + + context.subscriptions.push(vscode.commands.registerCommand('codegen.generate', async () => { if (getState().isLooping()) { diff --git a/src/generator.ts b/src/generator.ts index d03abcf..df48f38 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -4,10 +4,10 @@ import { Configuration, CreateCompletionResponse, OpenAIApi } from 'openai'; import * as fs from 'fs'; import { getState } from './state'; import axios from 'axios'; +import * as path from 'path'; const ALWAYS_INCLUDE_FILENAME = true; - let mapExtensionStartInstall: any = { 'js': 'npm install', 'ts': 'npm install', @@ -26,7 +26,6 @@ function getOpenAI() { return openai; } - function generateUnitTestPrompt(code: string): string { return `Generate code for a maximum of 5 unit tests for the following code, \ The returned code must contain all the necessary imports and must run without errors and \ @@ -111,7 +110,10 @@ async function generateUntilDone(engine: string, prompt: string, input: any): Pr } async function generate(input: any): Promise { + console.log("input.file", input.file) + console.log("input.variables", input.variables) let prompt = await prompts.build(input.file, input.variables); + console.log("prompt", prompt) let result = await generateUntilDone(input.engine, prompt, input); return result; } @@ -133,25 +135,21 @@ function getIntroText(command: any) { } async function getExistingFileCode(file: string, folder: string) { - if (file.startsWith('/')) { - return fs.readFileSync(file, 'utf8'); - } - // Check if file exists - let filePath = folder + '/' + file; + let filePath = path.isAbsolute(file) ? file : path.join(folder, file); + if (!fs.existsSync(filePath)) { return ''; } - return fs.readFileSync(folder + '/' + file, 'utf8'); + return fs.readFileSync(filePath, 'utf8'); } -const getFilesRecursively = (path: string) => { +const getFilesRecursively = (directoryPath: string) => { const files = []; - for (const file of fs.readdirSync(path)) { - const fullPath = path + '/' + file; - if(fs.lstatSync(fullPath).isDirectory()) { - getFilesRecursively(fullPath).forEach(x => files.push(file + '/' + x)); - } - else { + for (const file of fs.readdirSync(directoryPath)) { + const fullPath = path.join(directoryPath, file); + if (fs.lstatSync(fullPath).isDirectory()) { + getFilesRecursively(fullPath).forEach(x => files.push(path.join(file, x))); + } else { files.push(file); } } @@ -286,13 +284,19 @@ function mapFileToStart(filename: string, mapExtension: any) { } export async function generateCode(command: any, file: string) { + console.log("generateCode", command.command, file) let input = await getVariablesAndPromptForCommand('code', command); + console.log("INPUT: ", input) + console.log("INPUT: ", command.type) input.variables.file = file; if (command.type === 'test') { - input.variables.start = "\n"; + input.variables.start = ""; } + if (command.type === 'docs') { + input.variables.start = "\n"; + } if (command.type === 'install') { let start = mapFileToStart(file, mapExtensionStartInstall); input.config = { @@ -310,20 +314,13 @@ export async function generateCode(command: any, file: string) { return result; } -function generateInstallCommandPrompt(code: string): string { - return `Return the correct one-line command to install the dependencies needed in the following code: - \`\`\` - ${code} - \`\`\``; -} - -export async function getInstallCommand(prompt: string, context: any, model: string = "gpt-3.5-turbo-instruct", maxTokens: number = 500, temperature: number = 0): Promise { +export async function getOpenAIResponse(prompt: string, context: any, model: string = "gpt-3.5-turbo-instruct", maxTokens: number = 500, temperature: number = 0): Promise { // Retrieve the apiKey from your state management const OPENAI_API_KEY = context.globalState.get("openaiApiKey"); // Rest of your function remains the same const response = await axios.post('https://api.openai.com/v1/completions', { model: model, - prompt: generateInstallCommandPrompt(prompt), + prompt: prompt, max_tokens: maxTokens, temperature: temperature }, {