├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── .vscodeignore ├── LICENSE ├── README.md ├── code-clippy-demo.gif ├── code_clippy_logo.jpg ├── out ├── config.js ├── extension.js └── utils │ ├── extractGoogleResults.js │ ├── extractStackOverflowResults.js │ ├── fetchCodeCompletion.js │ ├── fetchCodeCompletions.js │ ├── fetchPageContent copy.js │ ├── fetchPageContent.js │ └── search.js ├── package.json ├── src ├── config.ts ├── extension.ts └── utils │ └── fetchCodeCompletions.ts ├── tsconfig.json └── vscode.d.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | vscode.d.ts 2 | vscode.proposed.inlineCompletions.d.ts 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /**@type {import('eslint').Linter.Config} */ 2 | // eslint-disable-next-line no-undef 3 | module.exports = { 4 | root: true, 5 | parser: '@typescript-eslint/parser', 6 | plugins: [ 7 | '@typescript-eslint', 8 | ], 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | ], 13 | rules: { 14 | 'semi': [2, "always"], 15 | '@typescript-eslint/no-unused-vars': 0, 16 | '@typescript-eslint/no-explicit-any': 0, 17 | '@typescript-eslint/explicit-module-boundary-types': 0, 18 | '@typescript-eslint/no-non-null-assertion': 0, 19 | } 20 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | 6 | yarn.lock 7 | package-lock.json -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [{ 8 | "name": "Run Extension", 9 | "type": "extensionHost", 10 | "request": "launch", 11 | "runtimeExecutable": "${execPath}", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}", 14 | "--enable-proposed-api" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | "preLaunchTask": "npm: watch" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | .yarnrc 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hieu Nguyen (Jack) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Code Clippy — Inline code suggestions from your friendly neighborhood hacker Clippy for VSCode 2 | 3 |

4 |
5 | 6 |
7 | Courtesy of the awesome Aimee Trevett! 8 |

9 | 10 | This extension is an effort to create an open source version of [Github Copilot](https://copilot.github.com/) where both the extension, model, and data that the model was trained on is free for everyone to use. If you'd like to learn more about how the model power Code Clippy, check out this [repo](https://github.com/ncoop57/gpt-code-clippy/). 11 | 12 | This extension also sits completely atop this other clone of Github Copilot aptly named [Captain Stack](https://github.com/hieunc229/copilot-clone), since instead of synthesizing the answers using deep learning, it extracts them from StackOverflow posts. 13 | 14 | ## Demo 15 | 16 |

17 |
18 | 19 |
20 |

21 | 22 | ## Table of contents: 23 | 24 | 1. [How to Install](#1-how-to-install) 25 | 2. [Using Code Clippy](#2-using-code-clippy) 26 | 3. [Limitations](#3-limitations) 27 | 28 | --- 29 | 30 | ## 1. How to Install 31 | 32 | In order to use the extension, you will need to download and install [VSCode](https://code.visualstudio.com/Download) and [Node and npm](https://nodejs.dev/learn/how-to-install-nodejs) (tested on Node `10.19.0` and npm `7.13.0`). Additionally, you will need a [Huggingface account](https://huggingface.co/join) in order to obtain the necessary API key that is used to authorize calls to Huggingface's Inference API. 33 | 34 | Download and install Code Clippy: 35 | ```bash 36 | git clone https://github.com/CodedotAl/code-clippy-vscode 37 | cd code-clippy-vscode 38 | npm install 39 | code . 40 | ``` 41 | 42 | ## 1b. How to Install as a Packaged Extension 43 | 44 | Alternatively to running the extension in debug mode, you can package it locally and install it. 45 | 46 | ```$ npm install 47 | $ npm run esbuild 48 | $ vsce package 49 | ``` 50 | Say 'y' to the warning. Then install the extension with vscode➡️…➡️Install from VSIX➡️. Finally, once installed, set conf.resource.hfAPIKey to your HuggingFace API key. 51 | 52 | **Note -** 53 | You may need to update your Node version to build the package. If you get an error like this: 54 | ``` 55 | return (translations ?? []) 56 | ^ 57 | 58 | SyntaxError: Unexpected token ? 59 | ``` 60 | 61 | Tested on Node `v16.17.0` and npm `v7.13.0`. 62 | 63 | --- 64 | 65 | ## 2. Using Code Clippy 66 | 67 | Once VSCode has opened up, you can press F5 to launch the extension in another VSCode window in debug model that you can test out. Open up a file of your choice in the new VSCode window and start typing. You will be prompted to enter an API key, go to your Huggingface account's [API token page](https://huggingface.co/settings/tokens) and copy your API key and paste it into the prompt. Now you should be able to type and see suggestions! To accept a suggestion, just press tab and to cycle through different suggestions press `ALT + [` to go to the next suggestion or `ALT + ]` to go to the previous one. 68 | 69 | **Note -** 70 | 71 | It can take a few seconds (~20seconds) to spin up the model on Huggingface's servers, so if you get an error about waiting for the model to load, just give it about 20 seconds and try again. If you don't see any suggestions, make sure that `showInlineCompletions` is enabled in your settings: 72 | ``` 73 | "editor.inlineSuggest.enabled": true 74 | ``` 75 | 76 | Additionally, Huggingface's Inference API free tier has a limited amount of requests you can make per hour and per month so if you are on the free tier you will only be able to use the extension as a demo. If you would like to be able to use the extension without this limitation you can host the model yourself and edit the code to send requests there or you can upgrade your Huggingface account to a different [tier](https://huggingface.co/pricing). 77 | 78 | You can also edit the model used to generate the code suggestions. I recommend using EleutherAI/gpt-neo-1.3B or EleutherAI/gpt-neo-2.7B over the default as in my testing they produce a lot better suggestions, but is significantly slower. To edit the model press `CTRL + SHIFT + P` to bring up the command prompt and type settings and select "Preferences: Open User Settings". This will open another tap with your settings. Search for "code clippy" and update the "Hf Model Name" configuration to be whatever model you want such as: 79 | ``` 80 | EleutherAI/gpt-neo-1.3B 81 | ``` 82 | 83 | From these code clippy settings, you can also update your API key or enable using GPU for doing the generation. However, for this feature to work, you need to have at least the Startup tier for Huggingface's Inference API. 84 | 85 | --- 86 | 87 | ## 3. Limitations 88 | 89 | | :exclamation: **Important -** First and formost, this extension is a **prototype** and the model it was trained on is for **research purposes** only and should not be used for developing real world applications. This is because the default model that is used to generate the code suggestions was trained on a large set of data scraped from [GitHub]() that might have contained things such as vulnerable code or private information such as private keys or passwords. Vulnerable code or private information can and therefore probably will leak into the suggestions. Currently the suggestions are just limited to a few additional tokens since the model starts to [hallucinate]() variables and methods the longer suggestions it is allowed to generate. If you would like to read more about the shortcomings of the model used in the generation and data used to train the model please refer to this [model card]() and [datasheet]() that explain it more in-depth. If you would like to learn more about how the model was trained and data was collected please refer to this [repository](https://github.com/ncoop57/gpt-code-clippy/). | 90 | |-----------------------------------------| 91 | -------------------------------------------------------------------------------- /code-clippy-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodedotAl/code-clippy-vscode/1b1edf6ccbb2568cb1873459f53d63f5b6dbce2e/code-clippy-demo.gif -------------------------------------------------------------------------------- /code_clippy_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodedotAl/code-clippy-vscode/1b1edf6ccbb2568cb1873459f53d63f5b6dbce2e/code_clippy_logo.jpg -------------------------------------------------------------------------------- /out/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const CSConfig = { 4 | SEARCH_PHARSE_END: ['.', ',', '{', '(', ' ', '-', '_', '+', '-', '*', '=', '/', '?', '<', '>'] 5 | }; 6 | exports.default = CSConfig; 7 | -------------------------------------------------------------------------------- /out/extension.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.activate = void 0; 13 | const vscode = require("vscode"); 14 | const config_1 = require("./config"); 15 | const fetchCodeCompletions_1 = require("./utils/fetchCodeCompletions"); 16 | function activate(context) { 17 | const disposable = vscode.commands.registerCommand('extension.code-clippy-settings', () => { 18 | vscode.window.showInformationMessage('Show settings'); 19 | }); 20 | context.subscriptions.push(disposable); 21 | const provider = { 22 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 23 | // @ts-ignore 24 | provideInlineCompletionItems: (document, position, context, token) => __awaiter(this, void 0, void 0, function* () { 25 | // Grab the api key from the extension's config 26 | const configuration = vscode.workspace.getConfiguration('', document.uri); 27 | const USE_FAUXPILOT = configuration.get("conf.resource.useFauxPilot", false); 28 | // if (!USE_FAUXPILOT) { 29 | const MODEL_NAME = configuration.get("conf.resource.hfModelName", ""); 30 | const API_KEY = configuration.get("conf.resource.hfAPIKey", ""); 31 | const USE_GPU = configuration.get("conf.resource.useGPU", false); 32 | // } 33 | // vscode.comments.createCommentController 34 | const textBeforeCursor = document.getText(); 35 | if (textBeforeCursor.trim() === "") { 36 | return { items: [] }; 37 | } 38 | const currLineBeforeCursor = document.getText(new vscode.Range(position.with(undefined, 0), position)); 39 | // Check if user's state meets one of the trigger criteria 40 | if (config_1.default.SEARCH_PHARSE_END.includes(textBeforeCursor[textBeforeCursor.length - 1]) || currLineBeforeCursor.trim() === "") { 41 | let rs; 42 | if (USE_FAUXPILOT) { 43 | try { 44 | // Fetch the code completion based on the text in the user's document 45 | rs = yield (0, fetchCodeCompletions_1.fetchCodeCompletionTextsFaux)(textBeforeCursor); 46 | } 47 | catch (err) { 48 | if (err instanceof Error) { 49 | vscode.window.showErrorMessage(err.toString()); 50 | } 51 | return { items: [] }; 52 | } 53 | } 54 | else { 55 | try { 56 | // Fetch the code completion based on the text in the user's document 57 | rs = yield (0, fetchCodeCompletions_1.fetchCodeCompletionTexts)(textBeforeCursor, document.fileName, MODEL_NAME, API_KEY, USE_GPU); 58 | } 59 | catch (err) { 60 | if (err instanceof Error) { 61 | // Check if it is an issue with API token and if so prompt user to enter a correct one 62 | if (err.toString() === "Error: Bearer token is invalid" || err.toString() === "Error: Authorization header is invalid, use 'Bearer API_TOKEN'") { 63 | vscode.window.showInputBox({ "prompt": "Please enter your HF API key in order to use Code Clippy", "password": true }).then(apiKey => configuration.update("conf.resource.hfAPIKey", apiKey)); 64 | } 65 | vscode.window.showErrorMessage(err.toString()); 66 | } 67 | return { items: [] }; 68 | } 69 | } 70 | if (rs == null) { 71 | return { items: [] }; 72 | } 73 | // Add the generated code to the inline suggestion list 74 | const items = []; 75 | for (let i = 0; i < rs.completions.length; i++) { 76 | items.push({ 77 | insertText: rs.completions[i], 78 | range: new vscode.Range(position.translate(0, rs.completions.length), position), 79 | trackingId: `snippet-${i}`, 80 | }); 81 | } 82 | return { items }; 83 | } 84 | return { items: [] }; 85 | }), 86 | }; 87 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 88 | // @ts-ignore 89 | vscode.languages.registerInlineCompletionItemProvider({ pattern: "**" }, provider); 90 | } 91 | exports.activate = activate; 92 | -------------------------------------------------------------------------------- /out/utils/extractGoogleResults.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.extractGoogleResults = void 0; 4 | const config_1 = require("../config"); 5 | const fetchPageContent_1 = require("./fetchPageContent"); 6 | // Get search results from google, then return a list of stackoverflow links 7 | function extractGoogleResults(keyword) { 8 | return new Promise((resolve, reject) => { 9 | return fetchPageContent_1.fetchPageTextContent(`${config_1.default.SEARCH_ENDPOINT}${keyword.replace(/\s/, '+')}`) 10 | .then(rs => { 11 | let urls = rs.textContent.match(/(https:\/\/stackoverflow.com\/[a-z0-9-/]+)/g); 12 | urls && (urls = urls.filter((url, i, list) => list.indexOf(url) === i)); 13 | resolve(urls); 14 | }) 15 | .catch(reject); 16 | }); 17 | } 18 | exports.extractGoogleResults = extractGoogleResults; 19 | -------------------------------------------------------------------------------- /out/utils/extractStackOverflowResults.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.extractSnippetResults = void 0; 4 | const jsdom_1 = require("jsdom"); 5 | // Extract and sort stackoverflow answers 6 | function extractSnippetResults(options) { 7 | var doc = new jsdom_1.JSDOM(options.textContent); 8 | let answersWithCodeBlock = Array.from(doc.window.document.querySelectorAll(".answer")) 9 | .filter((item) => item.querySelector("code") != null); 10 | let results = answersWithCodeBlock 11 | .map((item) => ({ 12 | textContent: item.textContent, 13 | votes: parseInt(item.querySelector(".js-vote-count").textContent), 14 | // TODO: Handle answers with more than one code block 15 | // p/s: they often about explaining the something 16 | code: item.querySelector("code").textContent, 17 | sourceURL: item.querySelector(".js-share-link").href, 18 | hasCheckMark: item.querySelector("iconCheckmarkLg") != null 19 | })) 20 | .filter(item => isCodeValid(item.code)); 21 | results.sort(sortSnippetResultFn); 22 | return { url: options.url, results }; 23 | } 24 | exports.extractSnippetResults = extractSnippetResults; 25 | function sortSnippetResultFn(a, b) { 26 | if (a.hasCheckMark != b.hasCheckMark) { 27 | return a.hasCheckMark ? 1 : -1; 28 | } 29 | let result = b.votes - a.votes; 30 | return result === 0 ? b.code.length - a.code.length : result; 31 | } 32 | // Check whether the input should be considered as code input or random text 33 | function isCodeValid(input) { 34 | // This is just a temporary solution, 35 | // it would filter codes that are too short 36 | return input.length > 12; 37 | } 38 | -------------------------------------------------------------------------------- /out/utils/fetchCodeCompletion.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.fetchCodeCompletionText = void 0; 4 | const node_fetch_1 = require("node-fetch"); 5 | const API_URL = "https://api-inference.huggingface.co/models/flax-community/gpt-neo-125M-code-clippy-dedup-2048"; 6 | function fetchCodeCompletionText(prompt, API_KEY) { 7 | // Setup header with API key 8 | // eslint-disable-next-line @typescript-eslint/naming-convention 9 | const headers = { "Authorization": `Bearer ${API_KEY}` }; 10 | return new Promise((resolve, reject) => { 11 | // Send post request to inference API 12 | return node_fetch_1.default(API_URL, { 13 | method: "post", 14 | body: JSON.stringify({ 15 | "inputs": prompt, "parameters": { 16 | "max_new_tokens": 16, "return_full_text": false, 17 | "do_sample": true, "temperature": 0.8, 18 | "max_time": 5.0, "num_return_sequences": 3, 19 | // "use_gpu": true 20 | } 21 | }), 22 | headers: headers 23 | }) 24 | .then(res => res.json()) 25 | .then(json => { 26 | if (Array.isArray(json)) { 27 | const generations = Array(); 28 | for (let i = 0; i < json.length; i++) { 29 | generations.push(json[i].generated_text.trimStart()); 30 | resolve({ generations }); 31 | } 32 | } 33 | else { 34 | console.log(json); 35 | throw new Error(json["error"]); 36 | } 37 | }) 38 | .catch(err => reject(err)); 39 | }); 40 | } 41 | exports.fetchCodeCompletionText = fetchCodeCompletionText; 42 | // def incr_list(l: list): 43 | // """Return list with elements incremented by 1. 44 | // >>> incr_list([1, 2, 3]) 45 | // [2, 3, 4] 46 | // >>> incr_list([5, 3, 5, 2, 3, 3, 9, 0, 123]) 47 | // [6, 4, 6, 3, 4, 4, 10, 1, 124] 48 | // """ 49 | -------------------------------------------------------------------------------- /out/utils/fetchCodeCompletions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.fetchCodeCompletionTextsFaux = exports.fetchCodeCompletionTexts = void 0; 4 | const node_fetch_1 = require("node-fetch"); 5 | const openai = require("openai"); 6 | function fetchCodeCompletionTexts(prompt, fileName, MODEL_NAME, API_KEY, USE_GPU) { 7 | console.log(MODEL_NAME); 8 | const API_URL = `https://api-inference.huggingface.co/models/${MODEL_NAME}`; 9 | // Setup header with API key 10 | // eslint-disable-next-line @typescript-eslint/naming-convention 11 | const headers = { "Authorization": `Bearer ${API_KEY}` }; 12 | return new Promise((resolve, reject) => { 13 | // Send post request to inference API 14 | return (0, node_fetch_1.default)(API_URL, { 15 | method: "POST", 16 | body: JSON.stringify({ 17 | "inputs": prompt, "parameters": { 18 | "max_new_tokens": 16, "return_full_text": false, 19 | "do_sample": true, "temperature": 0.8, "top_p": 0.95, 20 | "max_time": 10.0, "num_return_sequences": 3 21 | } 22 | }), 23 | headers: headers 24 | }) 25 | .then(res => res.json()) 26 | .then(json => { 27 | if (Array.isArray(json)) { 28 | const completions = Array(); 29 | for (let i = 0; i < json.length; i++) { 30 | const completion = json[i].generated_text.trimStart(); 31 | if (completion.trim() === "") 32 | continue; 33 | completions.push(completion); 34 | } 35 | console.log(completions); 36 | resolve({ completions }); 37 | } 38 | else { 39 | console.log(json); 40 | throw new Error(json["error"]); 41 | } 42 | }) 43 | .catch(err => reject(err)); 44 | }); 45 | } 46 | exports.fetchCodeCompletionTexts = fetchCodeCompletionTexts; 47 | function fetchCodeCompletionTextsFaux(prompt) { 48 | console.log('fastertransformer'); 49 | return new Promise((resolve, reject) => { 50 | const oa = new openai.OpenAIApi(new openai.Configuration({ 51 | apiKey: "dummy", 52 | basePath: "http://localhost:5000/v1", 53 | })); 54 | const response = oa.createCompletion({ 55 | model: "fastertransformer", 56 | prompt: prompt, 57 | stop: ["\n\n"], 58 | }); 59 | return response 60 | .then(res => res.data.choices) 61 | .then(choices => { 62 | var _a; 63 | if (Array.isArray(choices)) { 64 | const completions = Array(); 65 | for (let i = 0; i < choices.length; i++) { 66 | const completion = (_a = choices[i].text) === null || _a === void 0 ? void 0 : _a.trimStart(); 67 | if (completion === undefined) 68 | continue; 69 | if ((completion === null || completion === void 0 ? void 0 : completion.trim()) === "") 70 | continue; 71 | completions.push(completion); 72 | } 73 | console.log(completions); 74 | resolve({ completions }); 75 | } 76 | else { 77 | console.log(choices); 78 | throw new Error("Error"); 79 | } 80 | }) 81 | .catch(err => reject(err)); 82 | }); 83 | } 84 | exports.fetchCodeCompletionTextsFaux = fetchCodeCompletionTextsFaux; 85 | -------------------------------------------------------------------------------- /out/utils/fetchPageContent copy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.fetchPageTextContent = void 0; 4 | const node_fetch_1 = require("node-fetch"); 5 | function fetchPageTextContent(url) { 6 | return new Promise((resolve, reject) => { 7 | return node_fetch_1.default(url) 8 | .then(rs => rs.text()) 9 | .then(textContent => resolve({ textContent, url })) 10 | .catch(reject); 11 | }); 12 | } 13 | exports.fetchPageTextContent = fetchPageTextContent; 14 | -------------------------------------------------------------------------------- /out/utils/fetchPageContent.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.fetchPageTextContent = void 0; 4 | const node_fetch_1 = require("node-fetch"); 5 | function fetchPageTextContent(url) { 6 | return new Promise((resolve, reject) => { 7 | return node_fetch_1.default(url) 8 | .then(rs => rs.text()) 9 | .then(textContent => resolve({ textContent, url })) 10 | .catch(reject); 11 | }); 12 | } 13 | exports.fetchPageTextContent = fetchPageTextContent; 14 | -------------------------------------------------------------------------------- /out/utils/search.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.search = void 0; 13 | const extractGoogleResults_1 = require("./extractGoogleResults"); 14 | const extractStackOverflowResults_1 = require("./extractStackOverflowResults"); 15 | const fetchPageContent_1 = require("./fetchPageContent"); 16 | // Send search query to google, get answers from stackoverflow 17 | // then extract and return code results 18 | function search(keyword) { 19 | return __awaiter(this, void 0, void 0, function* () { 20 | return new Promise((resolve, reject) => { 21 | extractGoogleResults_1.extractGoogleResults(keyword) 22 | .then((urls) => __awaiter(this, void 0, void 0, function* () { 23 | if (urls === null) { 24 | return Promise.resolve(null); 25 | } 26 | let results = []; 27 | try { 28 | let fetchResult; 29 | for (const i in urls.splice(0, 6)) { 30 | if (urls[i]) { 31 | fetchResult = yield fetchPageContent_1.fetchPageTextContent(urls[i]); 32 | results = results.concat(extractStackOverflowResults_1.extractSnippetResults(fetchResult).results); 33 | } 34 | } 35 | resolve({ results }); 36 | } 37 | catch (err) { 38 | reject(err); 39 | } 40 | })).catch(reject); 41 | }); 42 | }); 43 | } 44 | exports.search = search; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-clippy", 3 | "displayName": "Code Clippy", 4 | "description": "Your friendly neighborhood Clippy, ready to help you with all your code needs :D!", 5 | "version": "0.0.1", 6 | "publisher": "ncoop57", 7 | "icon": "code_clippy_logo.jpg", 8 | "repository": "https://github.com/ncoop57/gpt-code-clippy/", 9 | "engines": { 10 | "vscode": "^1.34.0" 11 | }, 12 | "license": "MIT", 13 | "categories": [ 14 | "Other" 15 | ], 16 | "activationEvents": [ 17 | "*" 18 | ], 19 | "main": "./out/extension.js", 20 | "contributes": { 21 | "commands": [ 22 | { 23 | "command": "extension.code-clippy-settings", 24 | "title": "Code Clippy Settings" 25 | } 26 | ], 27 | "configuration": { 28 | "title": "Code Clippy Configuration", 29 | "properties": { 30 | "conf.resource.useFauxPilot": { 31 | "type": "boolean", 32 | "default": false, 33 | "description": "Whether to use Faux Pilo: https://github.com/moyix/fauxpilot", 34 | "scope": "resource" 35 | }, 36 | "conf.resource.hfModelName": { 37 | "type": "string", 38 | "default": "flax-community/gpt-neo-125M-code-clippy-dedup-2048", 39 | "description": "Model name that will be used to generate the completions.", 40 | "scope": "resource" 41 | }, 42 | "conf.resource.hfAPIKey": { 43 | "type": "string", 44 | "default": "", 45 | "description": "API key for using Huggingface's Inference API: https://api-inference.huggingface.co/docs/node/html/quicktour.html", 46 | "scope": "resource" 47 | }, 48 | "conf.resource.useGPU": { 49 | "type": "boolean", 50 | "default": false, 51 | "description": "Whether to use GPU for faster completions. Must have Startup plan at a minimum.", 52 | "scope": "resource" 53 | } 54 | } 55 | } 56 | }, 57 | "scripts": { 58 | "vscode:prepublish": "npm run compile && npm run esbuild-base -- --minify", 59 | "compile": "tsc -p ./", 60 | "lint": "eslint . --ext .ts,.tsx", 61 | "watch": "tsc -watch -p ./", 62 | "download-api": "vscode-dts dev", 63 | "postdownload-api": "vscode-dts main", 64 | "postinstall": "npm run download-api", 65 | "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/main.js --external:vscode --format=cjs --platform=node", 66 | "esbuild": "npm run esbuild-base -- --sourcemap", 67 | "esbuild-watch": "npm run esbuild-base -- --sourcemap --watch", 68 | "test-compile": "tsc -p ./" 69 | }, 70 | "devDependencies": { 71 | "@types/node": "^12.12.0", 72 | "@types/node-fetch": "^2.5.10", 73 | "@typescript-eslint/eslint-plugin": "^4.16.0", 74 | "@typescript-eslint/parser": "^4.16.0", 75 | "esbuild": "^0.15.5", 76 | "eslint": "^7.21.0", 77 | "typescript": "^4.2.2", 78 | "vscode-dts": "^0.3.1" 79 | }, 80 | "dependencies": { 81 | "@types/jsdom": "^16.2.12", 82 | "jsdom": "^16.6.0", 83 | "node-fetch": "^2.6.1", 84 | "openai": "^3.0.0" 85 | } 86 | } -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | const CSConfig = { 2 | SEARCH_PHARSE_END: ['.', ',', '{', '(', ' ', '-', '_', '+', '-', '*', '=', '/', '?', '<', '>'] 3 | } 4 | 5 | export default CSConfig -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import CSConfig from './config'; 3 | import { fetchCodeCompletionTexts, fetchCodeCompletionTextsFaux } from './utils/fetchCodeCompletions'; 4 | 5 | export function activate(context: vscode.ExtensionContext) { 6 | const disposable = vscode.commands.registerCommand( 7 | 'extension.code-clippy-settings', 8 | () => { 9 | vscode.window.showInformationMessage('Show settings'); 10 | } 11 | ); 12 | 13 | context.subscriptions.push(disposable); 14 | 15 | const provider: vscode.CompletionItemProvider = { 16 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 17 | // @ts-ignore 18 | provideInlineCompletionItems: async (document, position, context, token) => { 19 | // Grab the api key from the extension's config 20 | const configuration = vscode.workspace.getConfiguration('', document.uri); 21 | const USE_FAUXPILOT = configuration.get("conf.resource.useFauxPilot", false); 22 | // if (!USE_FAUXPILOT) { 23 | const MODEL_NAME = configuration.get("conf.resource.hfModelName", ""); 24 | const API_KEY = configuration.get("conf.resource.hfAPIKey", ""); 25 | const USE_GPU = configuration.get("conf.resource.useGPU", false); 26 | // } 27 | 28 | // vscode.comments.createCommentController 29 | const textBeforeCursor = document.getText(); 30 | if (textBeforeCursor.trim() === "") { 31 | return { items: [] }; 32 | } 33 | const currLineBeforeCursor = document.getText( 34 | new vscode.Range(position.with(undefined, 0), position) 35 | ); 36 | 37 | // Check if user's state meets one of the trigger criteria 38 | if (CSConfig.SEARCH_PHARSE_END.includes(textBeforeCursor[textBeforeCursor.length - 1]) || currLineBeforeCursor.trim() === "") { 39 | let rs; 40 | 41 | if (USE_FAUXPILOT) { 42 | try { 43 | // Fetch the code completion based on the text in the user's document 44 | rs = await fetchCodeCompletionTextsFaux(textBeforeCursor); 45 | } catch (err) { 46 | 47 | if (err instanceof Error) { 48 | vscode.window.showErrorMessage(err.toString()); 49 | } 50 | return { items: [] }; 51 | } 52 | } 53 | 54 | else { 55 | try { 56 | // Fetch the code completion based on the text in the user's document 57 | rs = await fetchCodeCompletionTexts(textBeforeCursor, document.fileName, MODEL_NAME, API_KEY, USE_GPU); 58 | } catch (err) { 59 | 60 | if (err instanceof Error) { 61 | // Check if it is an issue with API token and if so prompt user to enter a correct one 62 | if (err.toString() === "Error: Bearer token is invalid" || err.toString() === "Error: Authorization header is invalid, use 'Bearer API_TOKEN'") { 63 | vscode.window.showInputBox( 64 | { "prompt": "Please enter your HF API key in order to use Code Clippy", "password": true } 65 | ).then(apiKey => configuration.update("conf.resource.hfAPIKey", apiKey)); 66 | 67 | } 68 | vscode.window.showErrorMessage(err.toString()); 69 | } 70 | return { items: [] }; 71 | } 72 | } 73 | 74 | if (rs == null) { 75 | return { items: [] }; 76 | } 77 | 78 | // Add the generated code to the inline suggestion list 79 | const items: any[] = []; 80 | for (let i = 0; i < rs.completions.length; i++) { 81 | items.push({ 82 | insertText: rs.completions[i], 83 | range: new vscode.Range(position.translate(0, rs.completions.length), position), 84 | trackingId: `snippet-${i}`, 85 | }); 86 | } 87 | return { items }; 88 | } 89 | return { items: [] }; 90 | }, 91 | }; 92 | 93 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 94 | // @ts-ignore 95 | vscode.languages.registerInlineCompletionItemProvider({ pattern: "**" }, provider); 96 | } 97 | -------------------------------------------------------------------------------- /src/utils/fetchCodeCompletions.ts: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import * as openai from 'openai'; 3 | 4 | export type FetchCodeCompletions = { 5 | completions: Array 6 | } 7 | 8 | export function fetchCodeCompletionTexts(prompt: string, fileName: string, MODEL_NAME: string, API_KEY: string, USE_GPU: boolean): Promise { 9 | console.log(MODEL_NAME); 10 | const API_URL = `https://api-inference.huggingface.co/models/${MODEL_NAME}`; 11 | // Setup header with API key 12 | // eslint-disable-next-line @typescript-eslint/naming-convention 13 | const headers = { "Authorization": `Bearer ${API_KEY}` }; 14 | return new Promise((resolve, reject) => { 15 | // Send post request to inference API 16 | return fetch(API_URL, { 17 | method: "POST", 18 | body: JSON.stringify({ 19 | "inputs": prompt, "parameters": { 20 | "max_new_tokens": 16, "return_full_text": false, 21 | "do_sample": true, "temperature": 0.8, "top_p": 0.95, 22 | "max_time": 10.0, "num_return_sequences": 3 23 | } 24 | }), 25 | headers: headers 26 | }) 27 | .then(res => res.json()) 28 | .then(json => { 29 | if (Array.isArray(json)) { 30 | const completions = Array(); 31 | for (let i = 0; i < json.length; i++) { 32 | const completion = json[i].generated_text.trimStart(); 33 | if (completion.trim() === "") continue; 34 | 35 | completions.push( 36 | completion 37 | ); 38 | } 39 | console.log(completions); 40 | resolve({ completions }); 41 | } 42 | else { 43 | console.log(json); 44 | throw new Error(json["error"]); 45 | } 46 | }) 47 | .catch(err => reject(err)); 48 | }); 49 | } 50 | 51 | export function fetchCodeCompletionTextsFaux(prompt: string): Promise { 52 | console.log('fastertransformer'); 53 | return new Promise((resolve, reject) => { 54 | const oa = new openai.OpenAIApi( 55 | new openai.Configuration({ 56 | apiKey: "dummy", 57 | basePath: "http://localhost:5000/v1", 58 | }), 59 | ); 60 | const response = oa.createCompletion({ 61 | model: "fastertransformer", 62 | prompt: prompt as openai.CreateCompletionRequestPrompt, 63 | stop: ["\n\n"], 64 | }); 65 | return response 66 | .then(res => res.data.choices) 67 | .then(choices => { 68 | if (Array.isArray(choices)) { 69 | const completions = Array(); 70 | for (let i = 0; i < choices.length; i++) { 71 | const completion = choices[i].text?.trimStart(); 72 | if (completion === undefined) continue; 73 | if (completion?.trim() === "") continue; 74 | 75 | completions.push( 76 | completion 77 | ); 78 | } 79 | console.log(completions); 80 | resolve({ completions }); 81 | } 82 | else { 83 | console.log(choices); 84 | throw new Error("Error"); 85 | } 86 | }) 87 | .catch(err => reject(err)); 88 | }); 89 | } 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": ["es6"], 6 | "outDir": "out", 7 | "sourceMap": false, 8 | "strict": true, 9 | "rootDir": "src", 10 | "skipDefaultLibCheck": true, 11 | "skipLibCheck": true 12 | }, 13 | "exclude": ["node_modules", ".vscode-test"] 14 | } 15 | --------------------------------------------------------------------------------