├── .gitignore ├── LICENSE ├── README.md ├── example.png ├── packages └── codemirror-copilot │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── LICENSE │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── copilot.ts │ ├── index.ts │ ├── inline-suggestion.ts │ └── lib │ │ └── utils.ts │ ├── tsconfig.json │ └── vite.config.js └── website ├── .gitignore ├── README.md ├── components.json ├── components ├── editor.jsx ├── landing.jsx └── ui │ ├── badge.jsx │ ├── card.jsx │ └── select.jsx ├── jsconfig.json ├── lib └── utils.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── _document.js ├── api │ └── autocomplete.js └── index.js ├── postcss.config.js ├── public ├── favicon.ico ├── next.svg └── vercel.svg ├── styles └── globals.css └── tailwind.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | packages/codemirror-copilot/README.md 3 | packages/codemirror-copilot/example.png 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Asad Memon 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # codemirror-copilot 2 | 3 | [![npm version](https://badge.fury.io/js/codemirror-copilot.svg)](https://www.npmjs.com/package/codemirror-copilot) 4 | 5 | This CodeMirror extension lets you use GPT to autocomplete code in CodeMirror. It let's you call your API with current code (prefix and suffix from current cursor position) and let your API return the code to autocomplete. 6 | 7 | ![Screenshot](./example.png) 8 | 9 | ## Demo 10 | 11 | https://copilot.asadmemon.com 12 | 13 | ## Installation 14 | 15 | ```bash 16 | npm install codemirror-copilot --save 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```javascript 22 | import CodeMirror from "@uiw/react-codemirror"; 23 | import { javascript } from "@codemirror/lang-javascript"; 24 | import { inlineCopilot } from "codemirror-copilot"; 25 | 26 | function CodeEditor() { 27 | return ( 28 | { 36 | const res = await fetch("/api/autocomplete", { 37 | method: "POST", 38 | headers: { 39 | "Content-Type": "application/json", 40 | }, 41 | body: JSON.stringify({ prefix, suffix, language: "javascript" }), 42 | }); 43 | 44 | const { prediction } = await res.json(); 45 | return prediction; 46 | }), 47 | ]} 48 | /> 49 | ); 50 | } 51 | ``` 52 | 53 | You also need to implement an API that returns the prediction. For above code, here is an example API in Next.js that uses OpenAI 's `gpt-3.5-turbo-1106` model: 54 | 55 | ```javascript 56 | import OpenAI from "openai"; 57 | 58 | const openai = new OpenAI({ 59 | apiKey: process.env.OPENAI_API_KEY, 60 | }); 61 | 62 | async function completion( 63 | prefix, 64 | suffix, 65 | model = "gpt-3.5-turbo-1106", 66 | language, 67 | ) { 68 | const chatCompletion = await openai.chat.completions.create({ 69 | messages: [ 70 | { 71 | role: "system", 72 | content: `You are a ${ 73 | language ? language + " " : "" 74 | }programmer that replaces part with the right code. Only output the code that replaces part. Do not add any explanation or markdown.`, 75 | }, 76 | { role: "user", content: `${prefix}${suffix}` }, 77 | ], 78 | model, 79 | }); 80 | 81 | return chatCompletion.choices[0].message.content; 82 | } 83 | 84 | export default async function handler(req, res) { 85 | const { prefix, suffix, model, language } = req.body; 86 | const prediction = await completion(prefix, suffix, model, language); 87 | console.log(prediction); 88 | res.status(200).json({ prediction }); 89 | } 90 | ``` 91 | 92 | ## API 93 | 94 | ### `inlineCopilot(apiCallingFn: (prefix: string, suffix: string) => Promise, delay: number = 1000)` 95 | 96 | Provides extension for CodeMirror that renders the hints UI + Tab completion based on the prediction returned by the API. 97 | 98 | The `delay` parameter is the time in milliseconds to wait before calling the `apiCallingFn` after the user stops typing. Default value is `1000`. 99 | 100 | The extension also implements local caching of predictions to avoid unnecessary API calls. 101 | 102 | ### `clearLocalCache()` 103 | 104 | Clears the local cache of predictions. 105 | 106 | ## Local Development 107 | 108 | In one terminal, build the library itself by running: 109 | 110 | ```bash 111 | cd packages/codemirror-copilot 112 | npm install 113 | npm run dev 114 | ``` 115 | 116 | In another terminal, run the demo website: 117 | 118 | ```bash 119 | cd website 120 | npm install 121 | npm run dev 122 | ``` 123 | 124 | ## Acknowledgements 125 | 126 | This code is based on [codemirror-extension-inline-suggestion](https://github.com/saminzadeh/codemirror-extension-inline-suggestion) by Shan Aminzadeh. 127 | 128 | ## License 129 | 130 | MIT © [Asad Memon](https://asadmemon.com) 131 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asadm/codemirror-copilot/09e737a3da8449d5d7f0b5cd8266688afaf3baa5/example.png -------------------------------------------------------------------------------- /packages/codemirror-copilot/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | node: true, 5 | }, 6 | root: true, 7 | ignorePatterns: ["dist/**"], 8 | extends: [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:prettier/recommended", 12 | "prettier", 13 | ], 14 | overrides: [], 15 | parser: "@typescript-eslint/parser", 16 | plugins: ["@typescript-eslint", "prettier"], 17 | rules: { 18 | "prettier/prettier": "error", 19 | "@typescript-eslint/no-explicit-any": "warn", 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /packages/codemirror-copilot/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/codemirror-copilot/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Asad Memon 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. -------------------------------------------------------------------------------- /packages/codemirror-copilot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codemirror-copilot", 3 | "description": "This CodeMirror extension lets you use GPT to autocomplete code in CodeMirror.", 4 | "license": "MIT", 5 | "version": "0.0.7", 6 | "type": "module", 7 | "keywords": [ 8 | "codemirror", 9 | "extension", 10 | "autocomplete" 11 | ], 12 | "files": [ 13 | "dist", 14 | "example.png", 15 | "index.d.ts" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/asadm/codemirror-copilot.git" 20 | }, 21 | "main": "./dist/index.cjs", 22 | "module": "./dist/index.js", 23 | "types": "./dist/index.d.ts", 24 | "exports": { 25 | "types": "./dist/index.d.ts", 26 | "import": "./dist/index.js", 27 | "require": "./dist/index.cjs" 28 | }, 29 | "scripts": { 30 | "dev": "nodemon --watch 'src/**/*.ts' --ext 'ts' --exec \"npm run build && npm run copytowebsite\"", 31 | "lint": "eslint --ext .ts,.tsx src", 32 | "copytowebsite": "cp -R dist ../../website/", 33 | "copyreadme": "cp ../../README.md . && cp ../../example.png .", 34 | "lint:fix": "eslint --fix --ext .ts,.tsx src", 35 | "build": "tsc && vite build && npm run copyreadme && npm run copytowebsite", 36 | "prepublish": "npm run lint && npm run build", 37 | "test": "npm run lint" 38 | }, 39 | "devDependencies": { 40 | "@typescript-eslint/eslint-plugin": "^6.14.0", 41 | "@typescript-eslint/parser": "^6.14.0", 42 | "eslint": "^8.55.0", 43 | "eslint-config-prettier": "^9.1.0", 44 | "eslint-plugin-prettier": "^5.0.1", 45 | "nodemon": "^3.0.2", 46 | "prettier": "3.1.1", 47 | "typescript": "^5.3.3", 48 | "vite": "^5.0.8", 49 | "vite-plugin-dts": "^3.6.4" 50 | }, 51 | "peerDependencies": { 52 | "@codemirror/state": "^6.2.0", 53 | "@codemirror/view": "^6.7.2" 54 | }, 55 | "engines": { 56 | "node": "*" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/codemirror-copilot/src/copilot.ts: -------------------------------------------------------------------------------- 1 | import { inlineSuggestion } from "./inline-suggestion"; 2 | import type { EditorState } from "@codemirror/state"; 3 | 4 | /** 5 | * Should fetch autosuggestions from your AI 6 | * of choice. If there are no suggestions, 7 | * you should return an empty string. 8 | */ 9 | export type SuggestionRequestCallback = ( 10 | prefix: string, 11 | suffix: string, 12 | ) => Promise; 13 | 14 | const localSuggestionsCache: { [key: string]: string } = {}; 15 | 16 | /** 17 | * Wraps a user-provided fetch method so that users 18 | * don't have to interact directly with the EditorState 19 | * object, and connects it to the local result cache. 20 | */ 21 | function wrapUserFetcher(onSuggestionRequest: SuggestionRequestCallback) { 22 | return async function fetchSuggestion(state: EditorState) { 23 | const { from, to } = state.selection.ranges[0]; 24 | const text = state.doc.toString(); 25 | const prefix = text.slice(0, to); 26 | const suffix = text.slice(from); 27 | 28 | // If we have a local suggestion cache, use it 29 | const key = `${prefix}<:|:>${suffix}`; 30 | const localSuggestion = localSuggestionsCache[key]; 31 | if (localSuggestion) { 32 | return localSuggestion; 33 | } 34 | 35 | const prediction = await onSuggestionRequest(prefix, suffix); 36 | localSuggestionsCache[key] = prediction; 37 | return prediction; 38 | }; 39 | } 40 | 41 | /** 42 | * Configure the UI, state, and keymap to power 43 | * auto suggestions, with an abstracted 44 | * fetch method. 45 | */ 46 | export const inlineCopilot = ( 47 | onSuggestionRequest: SuggestionRequestCallback, 48 | delay = 1000, 49 | acceptOnClick = true, 50 | ) => { 51 | return inlineSuggestion({ 52 | fetchFn: wrapUserFetcher(onSuggestionRequest), 53 | delay, 54 | acceptOnClick, 55 | }); 56 | }; 57 | 58 | export const clearLocalCache = () => { 59 | Object.keys(localSuggestionsCache).forEach((key) => { 60 | delete localSuggestionsCache[key]; 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /packages/codemirror-copilot/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./copilot"; 2 | -------------------------------------------------------------------------------- /packages/codemirror-copilot/src/inline-suggestion.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ViewPlugin, 3 | DecorationSet, 4 | EditorView, 5 | ViewUpdate, 6 | Decoration, 7 | WidgetType, 8 | keymap, 9 | } from "@codemirror/view"; 10 | import { 11 | StateEffect, 12 | Text, 13 | Facet, 14 | Prec, 15 | StateField, 16 | EditorState, 17 | EditorSelection, 18 | TransactionSpec, 19 | } from "@codemirror/state"; 20 | import { debouncePromise } from "./lib/utils"; 21 | 22 | /** 23 | * The inner method to fetch suggestions: this is 24 | * abstracted by `inlineCopilot`. 25 | */ 26 | type InlineFetchFn = (state: EditorState) => Promise; 27 | 28 | /** 29 | * Current state of the autosuggestion 30 | */ 31 | const InlineSuggestionState = StateField.define<{ suggestion: null | string }>({ 32 | create() { 33 | return { suggestion: null }; 34 | }, 35 | update(previousValue, tr) { 36 | const inlineSuggestion = tr.effects.find((e) => 37 | e.is(InlineSuggestionEffect), 38 | ); 39 | if (tr.state.doc) { 40 | if (inlineSuggestion && tr.state.doc == inlineSuggestion.value.doc) { 41 | // There is a new selection that has been set via an effect, 42 | // and it applies to the current document. 43 | return { suggestion: inlineSuggestion.value.text }; 44 | } else if (!tr.docChanged && !tr.selection) { 45 | // This transaction is irrelevant to the document state 46 | // and could be generate by another plugin, so keep 47 | // the previous value. 48 | return previousValue; 49 | } 50 | } 51 | return { suggestion: null }; 52 | }, 53 | }); 54 | 55 | const InlineSuggestionEffect = StateEffect.define<{ 56 | text: string | null; 57 | doc: Text; 58 | }>(); 59 | 60 | /** 61 | * Rendered by `renderInlineSuggestionPlugin`, 62 | * this creates possibly multiple lines of ghostly 63 | * text to show what would be inserted if you accept 64 | * the AI suggestion. 65 | */ 66 | function inlineSuggestionDecoration(view: EditorView, suggestionText: string) { 67 | const pos = view.state.selection.main.head; 68 | const widgets = []; 69 | const w = Decoration.widget({ 70 | widget: new InlineSuggestionWidget(suggestionText), 71 | side: 1, 72 | }); 73 | widgets.push(w.range(pos)); 74 | return Decoration.set(widgets); 75 | } 76 | 77 | export const suggestionConfigFacet = Facet.define< 78 | { acceptOnClick: boolean; fetchFn: InlineFetchFn }, 79 | { acceptOnClick: boolean; fetchFn: InlineFetchFn | undefined } 80 | >({ 81 | combine(value) { 82 | return { 83 | acceptOnClick: !!value.at(-1)?.acceptOnClick, 84 | fetchFn: value.at(-1)?.fetchFn, 85 | }; 86 | }, 87 | }); 88 | 89 | /** 90 | * Renders the suggestion inline 91 | * with the rest of the code in the editor. 92 | */ 93 | class InlineSuggestionWidget extends WidgetType { 94 | suggestion: string; 95 | 96 | /** 97 | * Create a new suggestion widget. 98 | */ 99 | constructor(suggestion: string) { 100 | super(); 101 | this.suggestion = suggestion; 102 | } 103 | toDOM(view: EditorView) { 104 | const span = document.createElement("span"); 105 | span.style.opacity = "0.4"; 106 | span.className = "cm-inline-suggestion"; 107 | span.textContent = this.suggestion; 108 | span.onclick = (e) => this.accept(e, view); 109 | return span; 110 | } 111 | accept(e: MouseEvent, view: EditorView) { 112 | const config = view.state.facet(suggestionConfigFacet); 113 | if (!config.acceptOnClick) return; 114 | 115 | e.stopPropagation(); 116 | e.preventDefault(); 117 | 118 | const suggestionText = view.state.field(InlineSuggestionState)?.suggestion; 119 | 120 | // If there is no suggestion, do nothing and let the default keymap handle it 121 | if (!suggestionText) { 122 | return false; 123 | } 124 | 125 | view.dispatch({ 126 | ...insertCompletionText( 127 | view.state, 128 | suggestionText, 129 | view.state.selection.main.head, 130 | view.state.selection.main.head, 131 | ), 132 | }); 133 | return true; 134 | } 135 | } 136 | 137 | /** 138 | * Listens to document updates and calls `fetchFn` 139 | * to fetch auto-suggestions. This relies on 140 | * `InlineSuggestionState` also being installed 141 | * in the editor’s extensions. 142 | */ 143 | export const fetchSuggestion = ViewPlugin.fromClass( 144 | class Plugin { 145 | async update(update: ViewUpdate) { 146 | const doc = update.state.doc; 147 | // Only fetch if the document has changed 148 | if (!update.docChanged) { 149 | return; 150 | } 151 | 152 | const isAutocompleted = update.transactions.some((t) => 153 | t.isUserEvent("input.complete"), 154 | ); 155 | if (isAutocompleted) { 156 | return; 157 | } 158 | // for (const tr of update.transactions) { 159 | // // Check the userEvent property of the transaction 160 | // if (tr.isUserEvent("input.complete")) { 161 | // console.log("Change was due to autocomplete"); 162 | // } else { 163 | // console.log("Change was due to user input"); 164 | // } 165 | // } 166 | 167 | // console.log("CH", update); 168 | const config = update.view.state.facet(suggestionConfigFacet); 169 | if (!config.fetchFn) { 170 | console.error( 171 | "Unexpected issue in codemirror-copilot: fetchFn was not configured", 172 | ); 173 | return; 174 | } 175 | const result = await config.fetchFn(update.state); 176 | update.view.dispatch({ 177 | effects: InlineSuggestionEffect.of({ text: result, doc: doc }), 178 | }); 179 | } 180 | }, 181 | ); 182 | 183 | const renderInlineSuggestionPlugin = ViewPlugin.fromClass( 184 | class Plugin { 185 | decorations: DecorationSet; 186 | constructor() { 187 | // Empty decorations 188 | this.decorations = Decoration.none; 189 | } 190 | update(update: ViewUpdate) { 191 | const suggestionText = update.state.field(InlineSuggestionState) 192 | ?.suggestion; 193 | if (!suggestionText) { 194 | this.decorations = Decoration.none; 195 | return; 196 | } 197 | // console.log("SUGGESTION", suggestionText, update.transactions.map(t => t.effects.map(e=>e.is(InlineSuggestionEffect)))); 198 | // for (const tr of update.transactions) { 199 | // // Check the userEvent property of the transaction 200 | // if (wasAuto){ 201 | // wasAuto = false; 202 | // debugger; 203 | // } 204 | // if (tr.isUserEvent("input.complete")) { 205 | // console.log("Change was due to autocomplete"); 206 | // } else { 207 | // console.log("Change was due to user input"); 208 | // } 209 | // } 210 | this.decorations = inlineSuggestionDecoration( 211 | update.view, 212 | suggestionText, 213 | ); 214 | } 215 | }, 216 | { 217 | decorations: (v) => v.decorations, 218 | }, 219 | ); 220 | 221 | /** 222 | * Attaches a keybinding on `Tab` that accepts 223 | * the suggestion if there is one. 224 | */ 225 | const inlineSuggestionKeymap = Prec.highest( 226 | keymap.of([ 227 | { 228 | key: "Tab", 229 | run: (view) => { 230 | const suggestionText = view.state.field(InlineSuggestionState) 231 | ?.suggestion; 232 | 233 | // If there is no suggestion, do nothing and let the default keymap handle it 234 | if (!suggestionText) { 235 | return false; 236 | } 237 | 238 | view.dispatch({ 239 | ...insertCompletionText( 240 | view.state, 241 | suggestionText, 242 | view.state.selection.main.head, 243 | view.state.selection.main.head, 244 | ), 245 | }); 246 | return true; 247 | }, 248 | }, 249 | ]), 250 | ); 251 | 252 | function insertCompletionText( 253 | state: EditorState, 254 | text: string, 255 | from: number, 256 | to: number, 257 | ): TransactionSpec { 258 | return { 259 | ...state.changeByRange((range) => { 260 | if (range == state.selection.main) 261 | return { 262 | changes: { from: from, to: to, insert: text }, 263 | range: EditorSelection.cursor(from + text.length), 264 | }; 265 | const len = to - from; 266 | if ( 267 | !range.empty || 268 | (len && 269 | state.sliceDoc(range.from - len, range.from) != 270 | state.sliceDoc(from, to)) 271 | ) 272 | return { range }; 273 | return { 274 | changes: { from: range.from - len, to: range.from, insert: text }, 275 | range: EditorSelection.cursor(range.from - len + text.length), 276 | }; 277 | }), 278 | userEvent: "input.complete", 279 | }; 280 | } 281 | 282 | /** 283 | * Options to configure the AI suggestion UI. 284 | */ 285 | type InlineSuggestionOptions = { 286 | fetchFn: InlineFetchFn; 287 | /** 288 | * Delay after typing to query the API. A shorter 289 | * delay will query more often, and cost more. 290 | */ 291 | delay?: number; 292 | 293 | /** 294 | * Whether clicking the suggestion will 295 | * automatically accept it. 296 | */ 297 | acceptOnClick?: boolean; 298 | }; 299 | 300 | /** 301 | * Configure the UI, state, and keymap to power 302 | * auto suggestions. 303 | */ 304 | export function inlineSuggestion(options: InlineSuggestionOptions) { 305 | const { delay = 500, acceptOnClick = true } = options; 306 | const fetchFn = debouncePromise(options.fetchFn, delay); 307 | return [ 308 | suggestionConfigFacet.of({ acceptOnClick, fetchFn }), 309 | InlineSuggestionState, 310 | fetchSuggestion, 311 | renderInlineSuggestionPlugin, 312 | inlineSuggestionKeymap, 313 | ]; 314 | } 315 | -------------------------------------------------------------------------------- /packages/codemirror-copilot/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param f callback 3 | * @param wait milliseconds 4 | * @param abortValue if has abortValue, promise will reject it if 5 | * @returns Promise 6 | */ 7 | export function debouncePromise any>( 8 | fn: T, 9 | wait: number, 10 | abortValue: any = undefined, 11 | ) { 12 | let cancel = () => { 13 | // do nothing 14 | }; 15 | // type Awaited = T extends PromiseLike ? U : T 16 | type ReturnT = Awaited>; 17 | const wrapFunc = (...args: Parameters): Promise => { 18 | cancel(); 19 | return new Promise((resolve, reject) => { 20 | const timer = setTimeout(() => resolve(fn(...args)), wait); 21 | cancel = () => { 22 | clearTimeout(timer); 23 | if (abortValue !== undefined) { 24 | reject(abortValue); 25 | } 26 | }; 27 | }); 28 | }; 29 | return wrapFunc; 30 | } 31 | -------------------------------------------------------------------------------- /packages/codemirror-copilot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/codemirror-copilot/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { resolve } from 'path'; 3 | import dts from 'vite-plugin-dts'; 4 | 5 | export default defineConfig({ 6 | build: { 7 | sourcemap: true, 8 | lib: { 9 | formats: ['es', 'cjs'], 10 | entry: [resolve(__dirname, 'src/index.ts')], 11 | }, 12 | rollupOptions: { 13 | output: { 14 | preserveModules: true, 15 | }, 16 | external: ['@codemirror/state', '@codemirror/view'], 17 | }, 18 | }, 19 | plugins: [dts()], 20 | }); 21 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .env 39 | dist -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 20 | 21 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 22 | 23 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 24 | 25 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 26 | 27 | ## Learn More 28 | 29 | To learn more about Next.js, take a look at the following resources: 30 | 31 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 32 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 33 | 34 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 35 | 36 | ## Deploy on Vercel 37 | 38 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 39 | 40 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 41 | -------------------------------------------------------------------------------- /website/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": false, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "styles/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": false 11 | }, 12 | "aliases": { 13 | "utils": "@/lib/utils", 14 | "components": "@/components" 15 | } 16 | } -------------------------------------------------------------------------------- /website/components/editor.jsx: -------------------------------------------------------------------------------- 1 | import CodeMirror from "@uiw/react-codemirror"; 2 | import { javascript } from "@codemirror/lang-javascript"; 3 | import { dracula } from "@uiw/codemirror-theme-dracula"; 4 | 5 | import { inlineCopilot, clearLocalCache } from "../dist"; 6 | import { 7 | Select, 8 | SelectContent, 9 | SelectItem, 10 | SelectTrigger, 11 | SelectValue, 12 | } from "@/components/ui/select"; 13 | import { Badge } from "@/components/ui/badge"; 14 | import { useState } from "react"; 15 | 16 | const DEFAULTCODE = `function add(num1, num2){ 17 | ret 18 | }`; 19 | 20 | function CodeEditor() { 21 | const [model, setModel] = useState("gpt-3.5-turbo-1106"); 22 | const [acceptOnClick, setAcceptOnClick] = useState(true); 23 | return ( 24 | <> 25 | 50 | { 70 | const res = await fetch("/api/autocomplete", { 71 | method: "POST", 72 | headers: { 73 | "Content-Type": "application/json", 74 | }, 75 | body: JSON.stringify({ 76 | prefix, 77 | suffix, 78 | language: "javascript", 79 | model, 80 | }), 81 | }); 82 | 83 | const { prediction } = await res.json(); 84 | return prediction; 85 | }, 86 | 500, 87 | acceptOnClick, 88 | ), 89 | ]} 90 | /> 91 |
92 | 103 |
104 | 105 | ); 106 | } 107 | 108 | export default CodeEditor; 109 | 110 | -------------------------------------------------------------------------------- /website/components/landing.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was generated by v0 by Vercel. 3 | * @see https://v0.dev/t/fMJ3WqQYx6N 4 | */ 5 | import Link from "next/link" 6 | import { Card } from "@/components/ui/card" 7 | 8 | export function Landing({ children }) { 9 | return ( 10 | (
11 |
12 |
14 |
15 |
16 |

18 | Copilot for CodeMirror 19 |

20 |

21 | An open-source extension for CodeMirror that adds GitHub Copilot-like autocompletion using OpenAI's GPT models. 22 |

23 |
24 |
25 |
26 |
npm i codemirror-copilot --save
27 |
28 | 32 | GitHub 33 | 34 | 35 |
36 |
37 | 38 |
39 |

Try it out!

40 | {children} 41 |
42 |
43 |
44 |
45 | 48 |
) 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /website/components/ui/badge.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border border-gray-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2 dark:border-gray-800 dark:focus:ring-gray-300", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-gray-900 text-gray-50 hover:bg-gray-900/80 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-50/80", 13 | secondary: 14 | "border-transparent bg-gray-100 text-gray-900 hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/80", 15 | destructive: 16 | "border-transparent bg-red-500 text-gray-50 hover:bg-red-500/80 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/80", 17 | outline: "text-gray-950 dark:text-gray-50", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | function Badge({ 27 | className, 28 | variant, 29 | ...props 30 | }) { 31 | return (
); 32 | } 33 | 34 | export { Badge, badgeVariants } 35 | -------------------------------------------------------------------------------- /website/components/ui/card.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef(({ className, ...props }, ref) => ( 6 |
10 | )) 11 | Card.displayName = "Card" 12 | 13 | const CardHeader = React.forwardRef(({ className, ...props }, ref) => ( 14 |
18 | )) 19 | CardHeader.displayName = "CardHeader" 20 | 21 | const CardTitle = React.forwardRef(({ className, ...props }, ref) => ( 22 |

26 | )) 27 | CardTitle.displayName = "CardTitle" 28 | 29 | const CardDescription = React.forwardRef(({ className, ...props }, ref) => ( 30 |

34 | )) 35 | CardDescription.displayName = "CardDescription" 36 | 37 | const CardContent = React.forwardRef(({ className, ...props }, ref) => ( 38 |

39 | )) 40 | CardContent.displayName = "CardContent" 41 | 42 | const CardFooter = React.forwardRef(({ className, ...props }, ref) => ( 43 |
47 | )) 48 | CardFooter.displayName = "CardFooter" 49 | 50 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 51 | -------------------------------------------------------------------------------- /website/components/ui/select.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SelectPrimitive from "@radix-ui/react-select" 3 | import { Check, ChevronDown, ChevronUp } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Select = SelectPrimitive.Root 8 | 9 | const SelectGroup = SelectPrimitive.Group 10 | 11 | const SelectValue = SelectPrimitive.Value 12 | 13 | const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => ( 14 | span]:line-clamp-1 dark:border-gray-800 dark:bg-gray-950 dark:ring-offset-gray-950 dark:placeholder:text-gray-400 dark:focus:ring-gray-300", 18 | className 19 | )} 20 | {...props}> 21 | {children} 22 | 23 | 24 | 25 | 26 | )) 27 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 28 | 29 | const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => ( 30 | 34 | 35 | 36 | )) 37 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 38 | 39 | const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => ( 40 | 44 | 45 | 46 | )) 47 | SelectScrollDownButton.displayName = 48 | SelectPrimitive.ScrollDownButton.displayName 49 | 50 | const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => ( 51 | 52 | 62 | 63 | 66 | {children} 67 | 68 | 69 | 70 | 71 | )) 72 | SelectContent.displayName = SelectPrimitive.Content.displayName 73 | 74 | const SelectLabel = React.forwardRef(({ className, ...props }, ref) => ( 75 | 79 | )) 80 | SelectLabel.displayName = SelectPrimitive.Label.displayName 81 | 82 | const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => ( 83 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | {children} 97 | 98 | )) 99 | SelectItem.displayName = SelectPrimitive.Item.displayName 100 | 101 | const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => ( 102 | 106 | )) 107 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 108 | 109 | export { 110 | Select, 111 | SelectGroup, 112 | SelectValue, 113 | SelectTrigger, 114 | SelectContent, 115 | SelectLabel, 116 | SelectItem, 117 | SelectSeparator, 118 | SelectScrollUpButton, 119 | SelectScrollDownButton, 120 | } 121 | -------------------------------------------------------------------------------- /website/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /website/lib/utils.js: -------------------------------------------------------------------------------- 1 | import { clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /website/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website2", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "cd ../packages/codemirror-copilot/ && npm install", 7 | "dev": "next dev", 8 | "build:copilot": "cd ../packages/codemirror-copilot/ && npm run build", 9 | "build": "npm run build:copilot && next build", 10 | "start": "next start", 11 | "lint": "next lint" 12 | }, 13 | "dependencies": { 14 | "@codemirror/lang-javascript": "^6.2.1", 15 | "@radix-ui/react-icons": "^1.3.0", 16 | "@radix-ui/react-select": "^2.0.0", 17 | "@uiw/codemirror-theme-dracula": "^4.21.21", 18 | "@uiw/codemirror-theme-solarized": "^4.21.21", 19 | "@uiw/codemirror-theme-sublime": "^4.21.21", 20 | "@uiw/react-codemirror": "^4.21.21", 21 | "class-variance-authority": "^0.7.0", 22 | "clsx": "^2.0.0", 23 | "lucide-react": "^0.294.0", 24 | "next": "14.0.4", 25 | "openai": "^4.21.0", 26 | "react": "^18", 27 | "react-dom": "^18", 28 | "tailwind-merge": "^2.1.0", 29 | "tailwindcss-animate": "^1.0.7" 30 | }, 31 | "devDependencies": { 32 | "autoprefixer": "^10.4.16", 33 | "postcss": "^8", 34 | "tailwindcss": "^3.3.6" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /website/pages/_app.js: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | 3 | export default function App({ Component, pageProps }) { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /website/pages/_document.js: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /website/pages/api/autocomplete.js: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | 3 | const openai = new OpenAI({ 4 | apiKey: process.env.OPENAI_API_KEY, 5 | }); 6 | 7 | const perplexityOpenai = new OpenAI({ 8 | apiKey: process.env.PERPLEXITY_KEY, 9 | baseURL: "https://api.perplexity.ai", 10 | }); 11 | 12 | function removeOverlapPrefix(text, prefix) { 13 | // Remove overlapping part from the start (prefix) 14 | let commonPrefixLength = 0; 15 | for (let i = 0; i < prefix.length; i++) { 16 | if (text.startsWith(prefix.slice(i))) { 17 | commonPrefixLength = prefix.length - i; 18 | break; 19 | } 20 | } 21 | if (commonPrefixLength > 0 || prefix === "") { 22 | text = text.slice(commonPrefixLength); 23 | } 24 | return text; 25 | } 26 | 27 | async function completionCodeLlama(prefix, suffix, model, language){ 28 | const chatCompletion = await perplexityOpenai.chat.completions.create({ 29 | messages: [ 30 | { 31 | role: "system", 32 | content: `You are a ${language?(language + " "):""}programmer that replaces part with the right code. Only output the code that replaces part. Do not add any explanation or markdown.`, 33 | }, 34 | { role: "user", content: `${prefix}${suffix}` }, 35 | ], 36 | model, 37 | }); 38 | 39 | 40 | return removeOverlapPrefix(chatCompletion.choices[0].message.content, prefix); 41 | } 42 | 43 | async function completionLlama(prefix, suffix, language){ 44 | try { 45 | const response = await fetch( 46 | `https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDLFARE_ID}/ai/run/@hf/thebloke/codellama-7b-instruct-awq`, { 47 | method: 'POST', 48 | headers: { 49 | 'Authorization': `Bearer ${process.env.CLOUDFLARE_KEY}`, 50 | 'Content-Type': 'application/json' 51 | }, 52 | body: JSON.stringify({ "prompt": `You are a ${language?(language + " "):""}programmer. Do not add any explanation or markdown.
${prefix}${suffix}`, "max_tokens": 30 })
53 |     });
54 |     
55 |     const data = await response.json();
56 |     return data.result.response;
57 |   } catch (error) {
58 |     console.error('Error:', error);
59 |   }
60 | }
61 | 
62 | async function completionOpenAI(prefix, suffix, model="gpt-3.5-turbo-1106", language){
63 |   const chatCompletion = await openai.chat.completions.create({
64 |     messages: [
65 |       {
66 |         role: "system",
67 |         content: `You are a ${language?(language + " "):""}programmer that replaces  part with the right code. Only output the code that replaces  part. Do not add any explanation or markdown.`,
68 |       },
69 |       { role: "user", content: `${prefix}${suffix}` },
70 |     ],
71 |     model,
72 |   });
73 | 
74 |   return chatCompletion.choices[0].message.content;
75 | }
76 | 
77 | export default async function handler(req, res) {
78 |   const { prefix, suffix, model, language } = req.body;
79 |   const completionMethod = model.startsWith("codellama") ? completionCodeLlama : completionOpenAI;
80 |   const prediction = await completionMethod(prefix, suffix, model, language);
81 |   console.log(model, prediction)
82 |   res.status(200).json({ prediction })
83 | }
84 | 


--------------------------------------------------------------------------------
/website/pages/index.js:
--------------------------------------------------------------------------------
 1 | import CodeEditor from "@/components/editor";
 2 | import { Landing } from "@/components/landing";
 3 | 
 4 | export default function Home() {
 5 |   return (
 6 |     
7 | 8 | 9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /website/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asadm/codemirror-copilot/09e737a3da8449d5d7f0b5cd8266688afaf3baa5/website/public/favicon.ico -------------------------------------------------------------------------------- /website/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | -------------------------------------------------------------------------------- /website/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [require("tailwindcss-animate")], 18 | } 19 | --------------------------------------------------------------------------------