├── .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
└── vscode.proposed.inlineCompletions.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 | ],
15 | "outFiles": [
16 | "${workspaceFolder}/out/**/*.js"
17 | ],
18 | "preLaunchTask": "npm: watch"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/.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/ncoop57/code-clippy-vscode/ac79fbf2460ae1927dc42ec89063677415c82e61/code-clippy-demo.gif
--------------------------------------------------------------------------------
/code_clippy_logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncoop57/code-clippy-vscode/ac79fbf2460ae1927dc42ec89063677415c82e61/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 MODEL_NAME = configuration.get("conf.resource.hfModelName", "");
28 | const API_KEY = configuration.get("conf.resource.hfAPIKey", "");
29 | const USE_GPU = configuration.get("conf.resource.useGPU", false);
30 | // vscode.comments.createCommentController
31 | const textBeforeCursor = document.getText();
32 | if (textBeforeCursor.trim() === "") {
33 | return { items: [] };
34 | }
35 | const currLineBeforeCursor = document.getText(new vscode.Range(position.with(undefined, 0), position));
36 | // Check if user's state meets one of the trigger criteria
37 | if (config_1.default.SEARCH_PHARSE_END.includes(textBeforeCursor[textBeforeCursor.length - 1]) || currLineBeforeCursor.trim() === "") {
38 | let rs;
39 | try {
40 | // Fetch the code completion based on the text in the user's document
41 | rs = yield fetchCodeCompletions_1.fetchCodeCompletionTexts(textBeforeCursor, document.fileName, MODEL_NAME, API_KEY, USE_GPU);
42 | }
43 | catch (err) {
44 | if (err instanceof Error) {
45 | // Check if it is an issue with API token and if so prompt user to enter a correct one
46 | if (err.toString() === "Error: Bearer token is invalid" || err.toString() === "Error: Authorization header is invalid, use 'Bearer API_TOKEN'") {
47 | 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));
48 | }
49 | vscode.window.showErrorMessage(err.toString());
50 | }
51 | return { items: [] };
52 | }
53 | if (rs == null) {
54 | return { items: [] };
55 | }
56 | // Add the generated code to the inline suggestion list
57 | const items = [];
58 | for (let i = 0; i < rs.completions.length; i++) {
59 | items.push({
60 | insertText: rs.completions[i],
61 | range: new vscode.Range(position.translate(0, rs.completions.length), position),
62 | trackingId: `snippet-${i}`,
63 | });
64 | }
65 | return { items };
66 | }
67 | return { items: [] };
68 | }),
69 | };
70 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
71 | // @ts-ignore
72 | vscode.languages.registerInlineCompletionItemProvider({ pattern: "**" }, provider);
73 | }
74 | exports.activate = activate;
75 |
--------------------------------------------------------------------------------
/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.fetchCodeCompletionTexts = void 0;
4 | const node_fetch_1 = require("node-fetch");
5 | function fetchCodeCompletionTexts(prompt, fileName, MODEL_NAME, API_KEY, USE_GPU) {
6 | console.log(MODEL_NAME);
7 | const API_URL = `https://api-inference.huggingface.co/models/${MODEL_NAME}`;
8 | // Setup header with API key
9 | // eslint-disable-next-line @typescript-eslint/naming-convention
10 | const headers = { "Authorization": `Bearer ${API_KEY}` };
11 | return new Promise((resolve, reject) => {
12 | // Send post request to inference API
13 | return node_fetch_1.default(API_URL, {
14 | method: "post",
15 | body: JSON.stringify({
16 | "inputs": prompt, "parameters": {
17 | "max_new_tokens": 16, "return_full_text": false,
18 | "do_sample": true, "temperature": 0.8, "top_p": 0.95,
19 | "max_time": 10.0, "num_return_sequences": 3,
20 | "use_gpu": USE_GPU
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 |
--------------------------------------------------------------------------------
/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 | "enabledApiProposals": [
3 | "inlineCompletions"
4 | ],
5 | "name": "code-clippy",
6 | "displayName": "Code Clippy",
7 | "description": "Your friendly neighborhood Clippy, ready to help you with all your code needs :D!",
8 | "version": "0.0.1",
9 | "publisher": "ncoop57",
10 | "icon": "code_clippy_logo.jpg",
11 | "repository": "https://github.com/ncoop57/gpt-code-clippy/",
12 | "engines": {
13 | "vscode": "^1.34.0"
14 | },
15 | "license": "MIT",
16 | "categories": [
17 | "Other"
18 | ],
19 | "activationEvents": [
20 | "*"
21 | ],
22 | "main": "./out/extension.js",
23 | "contributes": {
24 | "commands": [
25 | {
26 | "command": "extension.code-clippy-settings",
27 | "title": "Code Clippy Settings"
28 | }
29 | ],
30 | "menus": {
31 | "editor/inlineCompletions/actions": [
32 | {
33 | "command": "extension.code-clippy-settings"
34 | }
35 | ]
36 | },
37 | "configuration": {
38 | "title": "Code Clippy Configuration",
39 | "properties": {
40 | "conf.resource.hfModelName": {
41 | "type": "string",
42 | "default": "flax-community/gpt-neo-125M-code-clippy-dedup-2048",
43 | "description": "Model name that will be used to generate the completions.",
44 | "scope": "resource"
45 | },
46 | "conf.resource.hfAPIKey": {
47 | "type": "string",
48 | "default": "",
49 | "description": "API key for using Huggingface's Inference API: https://api-inference.huggingface.co/docs/node/html/quicktour.html",
50 | "scope": "resource"
51 | },
52 | "conf.resource.useGPU": {
53 | "type": "boolean",
54 | "default": false,
55 | "description": "Whether to use GPU for faster completions. Must have Startup plan at a minimum.",
56 | "scope": "resource"
57 | }
58 | }
59 | }
60 | },
61 | "scripts": {
62 | "vscode:prepublish": "npm run compile && npm run esbuild-base -- --minify",
63 | "compile": "tsc -p ./",
64 | "lint": "eslint . --ext .ts,.tsx",
65 | "watch": "tsc -watch -p ./",
66 | "download-api": "vscode-dts dev",
67 | "postdownload-api": "vscode-dts main",
68 | "postinstall": "npm run download-api",
69 | "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/main.js --external:vscode --format=cjs --platform=node",
70 | "esbuild": "npm run esbuild-base -- --sourcemap",
71 | "esbuild-watch": "npm run esbuild-base -- --sourcemap --watch",
72 | "test-compile": "tsc -p ./"
73 | },
74 | "devDependencies": {
75 | "@types/node": "^12.12.0",
76 | "@types/node-fetch": "^2.5.10",
77 | "@typescript-eslint/eslint-plugin": "^4.16.0",
78 | "@typescript-eslint/parser": "^4.16.0",
79 | "esbuild": "^0.15.5",
80 | "eslint": "^7.21.0",
81 | "typescript": "^4.2.2",
82 | "vscode-dts": "^0.3.1"
83 | },
84 | "dependencies": {
85 | "@types/jsdom": "^16.2.12",
86 | "jsdom": "^16.6.0",
87 | "node-fetch": "^2.6.1"
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/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 } 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 MODEL_NAME = configuration.get("conf.resource.hfModelName", "");
22 | const API_KEY = configuration.get("conf.resource.hfAPIKey", "");
23 | const USE_GPU = configuration.get("conf.resource.useGPU", false);
24 |
25 | // vscode.comments.createCommentController
26 | const textBeforeCursor = document.getText();
27 | if (textBeforeCursor.trim() === "") {
28 | return { items: [] };
29 | }
30 | const currLineBeforeCursor = document.getText(
31 | new vscode.Range(position.with(undefined, 0), position)
32 | );
33 |
34 | // Check if user's state meets one of the trigger criteria
35 | if (CSConfig.SEARCH_PHARSE_END.includes(textBeforeCursor[textBeforeCursor.length - 1]) || currLineBeforeCursor.trim() === "") {
36 | let rs;
37 |
38 | try {
39 | // Fetch the code completion based on the text in the user's document
40 | rs = await fetchCodeCompletionTexts(textBeforeCursor, document.fileName, MODEL_NAME, API_KEY, USE_GPU);
41 | } catch (err) {
42 |
43 | if (err instanceof Error) {
44 | // Check if it is an issue with API token and if so prompt user to enter a correct one
45 | if (err.toString() === "Error: Bearer token is invalid" || err.toString() === "Error: Authorization header is invalid, use 'Bearer API_TOKEN'") {
46 | vscode.window.showInputBox(
47 | { "prompt": "Please enter your HF API key in order to use Code Clippy", "password": true }
48 | ).then(apiKey => configuration.update("conf.resource.hfAPIKey", apiKey));
49 |
50 | }
51 | vscode.window.showErrorMessage(err.toString());
52 | }
53 | return { items: [] };
54 | }
55 |
56 |
57 | if (rs == null) {
58 | return { items: [] };
59 | }
60 |
61 | // Add the generated code to the inline suggestion list
62 | const items: any[] = [];
63 | for (let i = 0; i < rs.completions.length; i++) {
64 | items.push({
65 | insertText: rs.completions[i],
66 | range: new vscode.Range(position.translate(0, rs.completions.length), position),
67 | trackingId: `snippet-${i}`,
68 | });
69 | }
70 | return { items };
71 | }
72 | return { items: [] };
73 | },
74 | };
75 |
76 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
77 | // @ts-ignore
78 | vscode.languages.registerInlineCompletionItemProvider({ pattern: "**" }, provider);
79 | }
80 |
--------------------------------------------------------------------------------
/src/utils/fetchCodeCompletions.ts:
--------------------------------------------------------------------------------
1 | import fetch from "node-fetch";
2 |
3 | export type FetchCodeCompletions = {
4 | completions: Array
5 | }
6 |
7 | export function fetchCodeCompletionTexts(prompt: string, fileName: string, MODEL_NAME: string, API_KEY: string, USE_GPU: boolean): Promise {
8 | console.log(MODEL_NAME)
9 | const API_URL = `https://api-inference.huggingface.co/models/${MODEL_NAME}`;
10 | // Setup header with API key
11 | // eslint-disable-next-line @typescript-eslint/naming-convention
12 | const headers = { "Authorization": `Bearer ${API_KEY}` };
13 | return new Promise((resolve, reject) => {
14 | // Send post request to inference API
15 | return fetch(API_URL, {
16 | method: "post",
17 | body: JSON.stringify({
18 | "inputs": prompt, "parameters": {
19 | "max_new_tokens": 16, "return_full_text": false,
20 | "do_sample": true, "temperature": 0.8, "top_p": 0.95,
21 | "max_time": 10.0, "num_return_sequences": 3,
22 | "use_gpu": USE_GPU
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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/vscode.proposed.inlineCompletions.d.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | declare module 'vscode' {
7 |
8 | // https://github.com/microsoft/vscode/issues/124024 @hediet @alexdima
9 |
10 | export namespace languages {
11 | /**
12 | * Registers an inline completion provider.
13 | */
14 | export function registerInlineCompletionItemProvider(selector: DocumentSelector, provider: InlineCompletionItemProvider): Disposable;
15 | }
16 |
17 | export interface InlineCompletionItemProvider {
18 | /**
19 | * Provides inline completion items for the given position and document.
20 | * If inline completions are enabled, this method will be called whenever the user stopped typing.
21 | * It will also be called when the user explicitly triggers inline completions or asks for the next or previous inline completion.
22 | * Use `context.triggerKind` to distinguish between these scenarios.
23 | */
24 | provideInlineCompletionItems(document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult | T[]>;
25 | }
26 |
27 | export interface InlineCompletionContext {
28 | /**
29 | * How the completion was triggered.
30 | */
31 | readonly triggerKind: InlineCompletionTriggerKind;
32 |
33 | /**
34 | * Provides information about the currently selected item in the autocomplete widget if it is visible.
35 | *
36 | * If set, provided inline completions must extend the text of the selected item
37 | * and use the same range, otherwise they are not shown as preview.
38 | * As an example, if the document text is `console.` and the selected item is `.log` replacing the `.` in the document,
39 | * the inline completion must also replace `.` and start with `.log`, for example `.log()`.
40 | *
41 | * Inline completion providers are requested again whenever the selected item changes.
42 | *
43 | * The user must configure `"editor.suggest.preview": true` for this feature.
44 | */
45 | readonly selectedCompletionInfo: SelectedCompletionInfo | undefined;
46 | }
47 |
48 | export interface SelectedCompletionInfo {
49 | range: Range;
50 | text: string;
51 | completionKind: CompletionItemKind;
52 | isSnippetText: boolean;
53 | }
54 |
55 | /**
56 | * How an {@link InlineCompletionItemProvider inline completion provider} was triggered.
57 | */
58 | export enum InlineCompletionTriggerKind {
59 | /**
60 | * Completion was triggered automatically while editing.
61 | * It is sufficient to return a single completion item in this case.
62 | */
63 | Automatic = 0,
64 |
65 | /**
66 | * Completion was triggered explicitly by a user gesture.
67 | * Return multiple completion items to enable cycling through them.
68 | */
69 | Explicit = 1,
70 | }
71 |
72 | export class InlineCompletionList {
73 | items: T[];
74 |
75 | constructor(items: T[]);
76 | }
77 |
78 | export class InlineCompletionItem {
79 | /**
80 | * The text to replace the range with.
81 | *
82 | * The text the range refers to should be a prefix of this value and must be a subword (`AB` and `BEF` are subwords of `ABCDEF`, but `Ab` is not).
83 | */
84 | text: string;
85 |
86 | /**
87 | * The range to replace.
88 | * Must begin and end on the same line.
89 | *
90 | * Prefer replacements over insertions to avoid cache invalidation:
91 | * Instead of reporting a completion that inserts an extension at the end of a word,
92 | * the whole word should be replaced with the extended word.
93 | */
94 | range?: Range;
95 |
96 | /**
97 | * An optional {@link Command} that is executed *after* inserting this completion.
98 | */
99 | command?: Command;
100 |
101 | constructor(text: string, range?: Range, command?: Command);
102 | }
103 |
104 |
105 | /**
106 | * Be aware that this API will not ever be finalized.
107 | */
108 | export namespace window {
109 | export function getInlineCompletionItemController(provider: InlineCompletionItemProvider): InlineCompletionController;
110 | }
111 |
112 | /**
113 | * Be aware that this API will not ever be finalized.
114 | */
115 | export interface InlineCompletionController {
116 | /**
117 | * Is fired when an inline completion item is shown to the user.
118 | */
119 | // eslint-disable-next-line vscode-dts-event-naming
120 | readonly onDidShowCompletionItem: Event>;
121 | }
122 |
123 | /**
124 | * Be aware that this API will not ever be finalized.
125 | */
126 | export interface InlineCompletionItemDidShowEvent {
127 | completionItem: T;
128 | }
129 | }
130 |
--------------------------------------------------------------------------------