├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierignore ├── LICENSE ├── README.md ├── babel.config.json ├── buf.gen.yaml ├── buf.lock ├── buf.yaml ├── build.sh ├── codeium.svg ├── exa ├── codeium_common_pb │ └── codeium_common.proto ├── language_server_pb │ └── language_server.proto └── seat_management_pb │ └── seat_management.proto ├── generate.js ├── package.json ├── patches ├── @jupyterlab+codeeditor+3.5.2.patch └── @jupyterlab+shared-models+3.5.2.patch ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── prettier.config.js ├── src ├── auth.ts ├── codemirror.ts ├── codemirrorInject.ts ├── codemirrorLanguages.ts ├── common.ts ├── component │ └── Options.tsx ├── contentScript.ts ├── enterprise.d.ts ├── jupyterInject.ts ├── jupyterlabPlugin.ts ├── monacoCompletionProvider.ts ├── monacoLanguages.ts ├── notebook.ts ├── options.tsx ├── popup.ts ├── script.ts ├── serviceWorker.ts ├── shared.ts ├── storage.ts ├── urls.ts ├── utf.ts └── utils.ts ├── static ├── codeium.css ├── icons │ ├── 16 │ │ ├── codeium_square_error.png │ │ ├── codeium_square_inactive.png │ │ └── codeium_square_logo.png │ ├── 32 │ │ ├── codeium_square_error.png │ │ ├── codeium_square_inactive.png │ │ └── codeium_square_logo.png │ ├── 48 │ │ ├── codeium_square_error.png │ │ ├── codeium_square_inactive.png │ │ └── codeium_square_logo.png │ ├── 128 │ │ ├── codeium_square_error.png │ │ ├── codeium_square_inactive.png │ │ └── codeium_square_logo.png │ ├── 128x.png │ ├── 16x.png │ ├── 32x.png │ └── 48x.png ├── logged_in_popup.html ├── manifest.json ├── options.html └── popup.html ├── styles ├── common.scss ├── options.scss └── popup.scss ├── tsconfig.json ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist_enterprise 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "plugins": ["@typescript-eslint"], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/strict", 10 | "prettier", 11 | "plugin:import/recommended", 12 | "plugin:import/typescript" 13 | ], 14 | "env": { 15 | "browser": true, 16 | "webextensions": true 17 | }, 18 | "rules": { 19 | "sort-imports": [ 20 | "error", 21 | { 22 | "ignoreCase": false, 23 | "ignoreDeclarationSort": true, 24 | "ignoreMemberSort": false, 25 | "memberSyntaxSortOrder": ["none", "all", "multiple", "single"], 26 | "allowSeparatedGroups": true 27 | } 28 | ], 29 | "import/order": [ 30 | "error", 31 | { 32 | "groups": [ 33 | "builtin", // Built-in imports (come from NodeJS native) go first 34 | "external", // <- External imports 35 | "internal", // <- Absolute imports 36 | ["sibling", "parent"], // <- Relative imports, the sibling and parent types they can be mingled together 37 | "index", // <- index imports 38 | "unknown" // <- unknown 39 | ], 40 | "newlines-between": "always", 41 | "alphabetize": { 42 | /* sort in ascending order. Options: ["ignore", "asc", "desc"] */ 43 | "order": "asc", 44 | /* ignore case. Options: [true, false] */ 45 | "caseInsensitive": true 46 | } 47 | } 48 | ], 49 | "no-unused-vars": "off", 50 | "@typescript-eslint/no-unused-vars": "error", 51 | "@typescript-eslint/no-unnecessary-condition": "off", 52 | "@typescript-eslint/no-floating-promises": "error" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name != 'main' || github.sha }} 9 | cancel-in-progress: true 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out repository code 15 | uses: actions/checkout@v3 16 | with: 17 | ref: ${{ github.event.pull_request.head.sha || github.sha }} 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 16 21 | - uses: pnpm/action-setup@v2 22 | with: 23 | version: 8 24 | run_install: | 25 | - args: [--frozen-lockfile] 26 | standalone: true 27 | - uses: pre-commit/action@v3.0.0 28 | with: 29 | extra_args: --all-files 30 | - run: pnpm prettier:check 31 | - run: pnpm build 32 | test-enterprise: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Check out repository code 36 | uses: actions/checkout@v3 37 | with: 38 | ref: ${{ github.event.pull_request.head.sha || github.sha }} 39 | - uses: actions/setup-node@v3 40 | with: 41 | node-version: 16 42 | - uses: pnpm/action-setup@v2 43 | with: 44 | version: 8 45 | run_install: | 46 | - args: [--frozen-lockfile] 47 | standalone: true 48 | - uses: pre-commit/action@v3.0.0 49 | with: 50 | extra_args: --all-files 51 | - run: pnpm prettier:check 52 | - run: pnpm build:enterprise 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist_enterprise 3 | node_modules 4 | .DS_Store 5 | proto 6 | dist.zip 7 | dist_enterprise.zip 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - id: check-case-conflict 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist_enterprise 3 | node_modules 4 | proto 5 | pnpm-lock.yaml 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Exafunction 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 |

2 | Codeium 3 |

4 | 5 | --- 6 | 7 | [![Discord](https://img.shields.io/discord/1027685395649015980?label=community&color=5865F2&logo=discord&logoColor=FFFFFF)](https://discord.gg/3XFf78nAx5) 8 | [![Twitter Follow](https://img.shields.io/badge/style--blue?style=social&logo=twitter&label=Follow%20%40codeiumdev)](https://twitter.com/intent/follow?screen_name=codeiumdev) 9 | ![License](https://img.shields.io/github/license/Exafunction/codeium-chrome) 10 | [![Docs](https://img.shields.io/badge/Codeium%20Docs-09B6A2)](https://docs.codeium.com) 11 | 12 | [![Visual Studio](https://img.shields.io/visual-studio-marketplace/i/Codeium.codeium?label=Visual%20Studio&logo=visualstudio)](https://marketplace.visualstudio.com/items?itemName=Codeium.codeium) 13 | [![JetBrains](https://img.shields.io/jetbrains/plugin/d/20540?label=JetBrains)](https://plugins.jetbrains.com/plugin/20540-codeium/) 14 | [![Open VSX](https://img.shields.io/open-vsx/dt/Codeium/codeium?label=Open%20VSX)](https://open-vsx.org/extension/Codeium/codeium) 15 | [![Google Chrome](https://img.shields.io/chrome-web-store/users/hobjkcpmjhlegmobgonaagepfckjkceh?label=Google%20Chrome&logo=googlechrome&logoColor=FFFFFF)](https://chrome.google.com/webstore/detail/codeium/hobjkcpmjhlegmobgonaagepfckjkceh) 16 | 17 | # codeium-chrome 18 | 19 | _Free, ultrafast code autocomplete for Chrome_ 20 | 21 | [Codeium](https://codeium.com/) autocompletes your code with AI in all major IDEs. This includes web editors as well! This Chrome extension currently supports: 22 | 23 | - [CodePen](https://codepen.io/) 24 | - [Codeshare](https://codeshare.io/) 25 | - [Codewars](https://www.codewars.com/) 26 | - [Databricks notebooks (Monaco editor only)](https://www.databricks.com/) 27 | - [Deepnote](https://deepnote.com/) 28 | - [GitHub](https://github.com/) 29 | - [Google Colab](https://colab.research.google.com/) 30 | - [JSFiddle](https://jsfiddle.net/) 31 | - [JupyterLab 3.x/Jupyter notebooks](https://jupyter.org/) 32 | - [LiveCodes](https://livecodes.io/) 33 | - [Paperspace](https://www.paperspace.com/) 34 | - [Quadratic](https://www.quadratichq.com/) 35 | - [StackBlitz](https://stackblitz.com/) 36 | 37 | In addition, any web page can support autocomplete in editors by adding the following meta tag to the `` section of the page: 38 | 39 | ```html 40 | 41 | ``` 42 | 43 | The `content` attribute accepts a comma-separated list of supported editors. These currently include: `"monaco"` and `"codemirror5"`. 44 | 45 | To disable the extension in a specific page add the following meta tag: 46 | 47 | ```html 48 | 49 | ``` 50 | 51 | Contributions are welcome! Feel free to submit pull requests and issues related to the extension or to add links to supported websites. 52 | 53 | 🔗 [Original Chrome extension launch announcement](https://codeium.com/blog/codeium-chrome-extension-launch) 54 | 55 | ## 🚀 Getting started 56 | 57 | To use the extension, install it from the [Chrome Web Store.](https://chrome.google.com/webstore/detail/codeium/hobjkcpmjhlegmobgonaagepfckjkceh) 58 | 59 | If you'd like to develop the extension, you'll need Node and pnpm (`npm install -g pnpm`). After `pnpm install`, use `pnpm start` to develop, and `pnpm build` to package. For the enterprise build, use `pnpm build:enterprise`. 60 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-typescript"], 3 | "plugins": ["@babel/plugin-transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | plugins: 3 | - local: [node, node_modules/@bufbuild/protoc-gen-es/bin/protoc-gen-es] 4 | out: proto 5 | opt: 6 | - target=ts 7 | - import_extension=none 8 | - local: [node, node_modules/@connectrpc/protoc-gen-connect-es/bin/protoc-gen-connect-es] 9 | out: proto 10 | opt: 11 | - target=ts 12 | - import_extension=none 13 | -------------------------------------------------------------------------------- /buf.lock: -------------------------------------------------------------------------------- 1 | # Generated by buf. DO NOT EDIT. 2 | version: v1 3 | deps: 4 | - remote: buf.build 5 | owner: envoyproxy 6 | repository: protoc-gen-validate 7 | commit: 6607b10f00ed4a3d98f906807131c44a 8 | -------------------------------------------------------------------------------- /buf.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | deps: 3 | - buf.build/envoyproxy/protoc-gen-validate 4 | build: 5 | excludes: 6 | - node_modules 7 | lint: 8 | use: 9 | - DEFAULT 10 | except: 11 | - PACKAGE_VERSION_SUFFIX 12 | allow_comment_ignores: true 13 | breaking: 14 | except: 15 | - FILE_NO_DELETE 16 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | if [[ $# -lt 1 || "$1" != "public" && "$1" != "enterprise" ]]; then 6 | echo "Usage: $0 " 7 | exit 1 8 | fi 9 | 10 | # Make sure local git changes are clean. 11 | git diff-index --quiet HEAD 12 | 13 | cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" 14 | 15 | cd ../../.. && git clean -ffdx -e local.bazelrc && cd - 16 | pnpm install 17 | # If the first arg is public, use pnpm run build 18 | if [[ "$1" == "public" ]]; then 19 | pnpm run build 20 | else 21 | pnpm run build:enterprise 22 | fi 23 | -------------------------------------------------------------------------------- /codeium.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /exa/codeium_common_pb/codeium_common.proto: -------------------------------------------------------------------------------- 1 | // Copyright Exafunction, Inc. 2 | 3 | syntax = "proto3"; 4 | 5 | package exa.codeium_common_pb; 6 | 7 | import "google/protobuf/duration.proto"; 8 | import "google/protobuf/timestamp.proto"; 9 | import "validate/validate.proto"; 10 | 11 | option go_package = "github.com/Exafunction/Exafunction/exa/codeium_common_pb"; 12 | 13 | enum ExperimentKey { 14 | UNSPECIFIED = 0; 15 | JUPYTER_FORMAT = 77; 16 | } 17 | 18 | // Next ID: 12, Previous field: entropy. 19 | message Completion { 20 | string completion_id = 1; 21 | string text = 2; 22 | string prefix = 3; 23 | string stop = 4; 24 | double score = 5; 25 | repeated uint64 tokens = 6; 26 | repeated string decoded_tokens = 7; 27 | repeated double probabilities = 8; 28 | repeated double adjusted_probabilities = 9; 29 | uint64 generated_length = 10; 30 | } 31 | 32 | // Authentication source for users on the cloud service. 33 | enum AuthSource { 34 | AUTH_SOURCE_CODEIUM = 0; 35 | } 36 | 37 | // Next ID: 15, Previous field: url. 38 | message Metadata { 39 | string ide_name = 1 [(validate.rules).string.min_len = 1]; 40 | string ide_version = 7 [(validate.rules).string.min_len = 1]; 41 | string extension_name = 12; 42 | string extension_version = 2 [(validate.rules).string.min_len = 1]; 43 | string api_key = 3 [(validate.rules).string.uuid = true]; 44 | // Regex derived from https://stackoverflow.com/a/48300605. 45 | // TODO(prem): Should this be mandatory? 46 | string locale = 4 [(validate.rules).string = { 47 | ignore_empty: true, 48 | pattern: "^[A-Za-z]{2,4}([_-][A-Za-z]{4})?([_-]([A-Za-z]{2}|[0-9]{3}))?$" 49 | }]; 50 | // UID identifying a single session for the given user. 51 | string session_id = 10; 52 | 53 | // Used purely in language server to cancel in flight requests. 54 | // If request_id is 0, then the request is not cancelable. 55 | // This should be a strictly monotonically increasing number 56 | // for the duration of a session. 57 | uint64 request_id = 9; 58 | 59 | // Browser-specific information. 60 | string user_agent = 13; 61 | string url = 14 [(validate.rules).string = { 62 | ignore_empty: true, 63 | uri: true 64 | }]; 65 | 66 | // Authentication source information. 67 | AuthSource auth_source = 15; 68 | } 69 | 70 | // Next ID: 3, Previous field: insert_spaces. 71 | message EditorOptions { 72 | uint64 tab_size = 1 [(validate.rules).uint64.gt = 0]; 73 | bool insert_spaces = 2; 74 | } 75 | 76 | message Event { 77 | EventType event_type = 1; 78 | string event_json = 2; 79 | int64 timestamp_unix_ms = 3; 80 | } 81 | 82 | enum EventType { 83 | EVENT_TYPE_UNSPECIFIED = 0; 84 | EVENT_TYPE_ENABLE_CODEIUM = 1; 85 | EVENT_TYPE_DISABLE_CODEIUM = 2; 86 | EVENT_TYPE_SHOW_PREVIOUS_COMPLETION = 3; 87 | EVENT_TYPE_SHOW_NEXT_COMPLETION = 4; 88 | } 89 | 90 | enum CompletionSource { 91 | COMPLETION_SOURCE_UNSPECIFIED = 0; 92 | COMPLETION_SOURCE_TYPING_AS_SUGGESTED = 1; 93 | COMPLETION_SOURCE_CACHE = 2; 94 | COMPLETION_SOURCE_NETWORK = 3; 95 | } 96 | 97 | // Every time this list is updated, we should be redeploying the API server 98 | // since it uses the string representation for BQ. 99 | enum Language { 100 | LANGUAGE_UNSPECIFIED = 0; 101 | LANGUAGE_C = 1; 102 | LANGUAGE_CLOJURE = 2; 103 | LANGUAGE_COFFEESCRIPT = 3; 104 | LANGUAGE_CPP = 4; 105 | LANGUAGE_CSHARP = 5; 106 | LANGUAGE_CSS = 6; 107 | LANGUAGE_CUDACPP = 7; 108 | LANGUAGE_DOCKERFILE = 8; 109 | LANGUAGE_GO = 9; 110 | LANGUAGE_GROOVY = 10; 111 | LANGUAGE_HANDLEBARS = 11; 112 | LANGUAGE_HASKELL = 12; 113 | LANGUAGE_HCL = 13; 114 | LANGUAGE_HTML = 14; 115 | LANGUAGE_INI = 15; 116 | LANGUAGE_JAVA = 16; 117 | LANGUAGE_JAVASCRIPT = 17; 118 | LANGUAGE_JSON = 18; 119 | LANGUAGE_JULIA = 19; 120 | LANGUAGE_KOTLIN = 20; 121 | LANGUAGE_LATEX = 21; 122 | LANGUAGE_LESS = 22; 123 | LANGUAGE_LUA = 23; 124 | LANGUAGE_MAKEFILE = 24; 125 | LANGUAGE_MARKDOWN = 25; 126 | LANGUAGE_OBJECTIVEC = 26; 127 | LANGUAGE_OBJECTIVECPP = 27; 128 | LANGUAGE_PERL = 28; 129 | LANGUAGE_PHP = 29; 130 | LANGUAGE_PLAINTEXT = 30; 131 | LANGUAGE_PROTOBUF = 31; 132 | LANGUAGE_PBTXT = 32; 133 | LANGUAGE_PYTHON = 33; 134 | LANGUAGE_R = 34; 135 | LANGUAGE_RUBY = 35; 136 | LANGUAGE_RUST = 36; 137 | LANGUAGE_SASS = 37; 138 | LANGUAGE_SCALA = 38; 139 | LANGUAGE_SCSS = 39; 140 | LANGUAGE_SHELL = 40; 141 | LANGUAGE_SQL = 41; 142 | LANGUAGE_STARLARK = 42; 143 | LANGUAGE_SWIFT = 43; 144 | LANGUAGE_TSX = 44; 145 | LANGUAGE_TYPESCRIPT = 45; 146 | LANGUAGE_VISUALBASIC = 46; 147 | LANGUAGE_VUE = 47; 148 | LANGUAGE_XML = 48; 149 | LANGUAGE_XSL = 49; 150 | LANGUAGE_YAML = 50; 151 | LANGUAGE_SVELTE = 51; 152 | LANGUAGE_TOML = 52; 153 | LANGUAGE_DART = 53; 154 | LANGUAGE_RST = 54; 155 | LANGUAGE_OCAML = 55; 156 | LANGUAGE_CMAKE = 56; 157 | LANGUAGE_PASCAL = 57; 158 | LANGUAGE_ELIXIR = 58; 159 | LANGUAGE_FSHARP = 59; 160 | LANGUAGE_LISP = 60; 161 | LANGUAGE_MATLAB = 61; 162 | LANGUAGE_POWERSHELL = 62; 163 | LANGUAGE_SOLIDITY = 63; 164 | LANGUAGE_ADA = 64; 165 | LANGUAGE_OCAML_INTERFACE = 65; 166 | } 167 | -------------------------------------------------------------------------------- /exa/language_server_pb/language_server.proto: -------------------------------------------------------------------------------- 1 | // Copyright Exafunction, Inc. 2 | 3 | syntax = "proto3"; 4 | 5 | package exa.language_server_pb; 6 | 7 | import "exa/codeium_common_pb/codeium_common.proto"; 8 | import "validate/validate.proto"; 9 | 10 | option go_package = "github.com/Exafunction/Exafunction/exa/language_server_pb"; 11 | 12 | service LanguageServerService { 13 | rpc GetCompletions(GetCompletionsRequest) returns (GetCompletionsResponse) {} 14 | rpc AcceptCompletion(AcceptCompletionRequest) returns (AcceptCompletionResponse) {} 15 | rpc GetAuthToken(GetAuthTokenRequest) returns (GetAuthTokenResponse) {} 16 | } 17 | 18 | // Next ID: 9, Previous field: disable_cache. 19 | message GetCompletionsRequest { 20 | codeium_common_pb.Metadata metadata = 1 [(validate.rules).message.required = true]; 21 | Document document = 2 [(validate.rules).message.required = true]; 22 | codeium_common_pb.EditorOptions editor_options = 3 [(validate.rules).message.required = true]; 23 | repeated Document other_documents = 5; 24 | ExperimentConfig experiment_config = 7; 25 | 26 | string model_name = 10; 27 | } 28 | 29 | // Next ID: 5, Previous field: latency_info. 30 | message GetCompletionsResponse { 31 | State state = 1; 32 | repeated CompletionItem completion_items = 2; 33 | } 34 | 35 | // Next ID: 3, Previous field: completion_id. 36 | message AcceptCompletionRequest { 37 | codeium_common_pb.Metadata metadata = 1 [(validate.rules).message.required = true]; 38 | string completion_id = 2; 39 | } 40 | 41 | // Next ID: 1, Previous field: N/A. 42 | message AcceptCompletionResponse {} 43 | 44 | // Next ID: 1, Previous field: N/A. 45 | message GetAuthTokenRequest {} 46 | 47 | // Next ID: 3, Previous field: uuid. 48 | message GetAuthTokenResponse { 49 | string auth_token = 1; 50 | string uuid = 2; 51 | } 52 | 53 | /*****************************************************************************/ 54 | /* Helper Messages */ 55 | /*****************************************************************************/ 56 | 57 | message DocumentPosition { 58 | // 0-indexed. Measured in UTF-8 bytes. 59 | uint64 row = 1; 60 | // 0-indexed. Measured in UTF-8 bytes. 61 | uint64 col = 2; 62 | } 63 | 64 | // Next ID: 9, Previous field: cursor_position. 65 | message Document { 66 | // OS specific separators. 67 | string absolute_path_migrate_me_to_uri = 1 [deprecated = true]; 68 | string absolute_uri = 12; 69 | // Path relative to the root of the workspace. Slash separated. 70 | // Leave empty if the document is not in the workspace. 71 | string relative_path_migrate_me_to_workspace_uri = 2 [deprecated = true]; 72 | string workspace_uri = 13; 73 | string text = 3; 74 | // Language ID provided by the editor. 75 | string editor_language = 4 [(validate.rules).string.min_len = 1]; 76 | // Language enum standardized across editors. 77 | codeium_common_pb.Language language = 5; 78 | // Measured in number of UTF-8 bytes. 79 | uint64 cursor_offset = 6; 80 | // May be present instead of cursor_offset. 81 | DocumentPosition cursor_position = 8; 82 | // \n or \r\n, if known. 83 | string line_ending = 7 [(validate.rules).string = { 84 | in: [ 85 | "", 86 | "\n", 87 | "\r\n" 88 | ] 89 | }]; 90 | 91 | // These fields are not used by the chrome extension. 92 | Range visible_range = 9; 93 | bool is_cutoff_start = 10; 94 | bool is_cutoff_end = 11; 95 | int32 lines_cutoff_start = 14; 96 | int32 lines_cutoff_end = 15; 97 | } 98 | 99 | message ExperimentConfig { 100 | repeated codeium_common_pb.ExperimentKey force_enable_experiments = 1 [(validate.rules).repeated.unique = true]; 101 | } 102 | 103 | enum CodeiumState { 104 | CODEIUM_STATE_UNSPECIFIED = 0; 105 | CODEIUM_STATE_INACTIVE = 1; 106 | CODEIUM_STATE_PROCESSING = 2; 107 | CODEIUM_STATE_SUCCESS = 3; 108 | CODEIUM_STATE_WARNING = 4; 109 | CODEIUM_STATE_ERROR = 5; 110 | } 111 | 112 | // Next ID: 3, Previous field: message. 113 | message State { 114 | CodeiumState state = 1; 115 | string message = 2; 116 | } 117 | 118 | enum LineType { 119 | LINE_TYPE_UNSPECIFIED = 0; 120 | LINE_TYPE_SINGLE = 1; 121 | LINE_TYPE_MULTI = 2; 122 | } 123 | 124 | // Next ID: 5, Previous field: end_position. 125 | message Range { 126 | uint64 start_offset = 1; 127 | uint64 end_offset = 2; 128 | DocumentPosition start_position = 3; 129 | DocumentPosition end_position = 4; 130 | } 131 | 132 | message Suffix { 133 | // Text to insert after the cursor when accepting the completion. 134 | string text = 1; 135 | // Cursor position delta (as signed offset) from the end of the inserted 136 | // completion (including the suffix). 137 | int64 delta_cursor_offset = 2; 138 | } 139 | 140 | enum CompletionPartType { 141 | COMPLETION_PART_TYPE_UNSPECIFIED = 0; 142 | // Single-line completion parts that appear within an existing line of text. 143 | COMPLETION_PART_TYPE_INLINE = 1; 144 | // Possibly multi-line completion parts that appear below an existing line of text. 145 | COMPLETION_PART_TYPE_BLOCK = 2; 146 | // Like COMPLETION_PART_TYPE_INLINE, but overwrites the existing text. 147 | COMPLETION_PART_TYPE_INLINE_MASK = 3; 148 | } 149 | 150 | // Represents a contiguous part of the completion text that is not 151 | // already in the document. 152 | // Next ID: 4, Previous field: prefix. 153 | message CompletionPart { 154 | string text = 1; 155 | // Offset in the original document where the part starts. For block 156 | // parts, this is always the end of the line before the block. 157 | uint64 offset = 2; 158 | CompletionPartType type = 3; 159 | // The section of the original line that came before this part. Only valid for 160 | // COMPLETION_PART_TYPE_INLINE. 161 | string prefix = 4; 162 | // In the case of COMPLETION_PART_TYPE_BLOCK, represents the line it is below. 163 | uint64 line = 5; 164 | } 165 | 166 | // Next ID: 9, Previous field: completion_parts. 167 | message CompletionItem { 168 | codeium_common_pb.Completion completion = 1; 169 | Suffix suffix = 5; 170 | Range range = 2; 171 | codeium_common_pb.CompletionSource source = 3; 172 | repeated CompletionPart completion_parts = 8; 173 | } 174 | -------------------------------------------------------------------------------- /exa/seat_management_pb/seat_management.proto: -------------------------------------------------------------------------------- 1 | // Copyright Exafunction, Inc. 2 | 3 | syntax = "proto3"; 4 | 5 | package exa.seat_management_pb; 6 | 7 | option go_package = "github.com/Exafunction/Exafunction/exa/seat_management_pb"; 8 | 9 | service SeatManagementService { 10 | rpc RegisterUser(RegisterUserRequest) returns (RegisterUserResponse) {} 11 | } 12 | 13 | message RegisterUserRequest { 14 | // In enterprise deployments, this is actually a supabase auth token. 15 | string firebase_id_token = 1 [json_name = "firebase_id_token"]; 16 | } 17 | 18 | message RegisterUserResponse { 19 | string api_key = 1 [json_name = "api_key"]; 20 | string name = 2 [json_name = "name"]; 21 | } 22 | -------------------------------------------------------------------------------- /generate.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | 3 | require('dotenv').config(); 4 | if (process.env.CODEIUM_ENV === 'monorepo') { 5 | execSync( 6 | 'pnpm buf generate ../../.. --path ../../language_server_pb/language_server.proto --path ../../seat_management_pb/seat_management.proto --path ../../opensearch_clients_pb/opensearch_clients.proto --include-imports' 7 | ); 8 | } else { 9 | execSync('pnpm buf generate'); 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeium-chrome", 3 | "version": "1.26.3", 4 | "description": "", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "pnpm generate && pnpm lint && webpack --config webpack.prod.js && cd dist && zip -r ../dist.zip .", 8 | "build:enterprise": "pnpm generate && pnpm lint && webpack --config webpack.prod.js --env enterprise && cd dist_enterprise && zip -r ../dist_enterprise.zip .", 9 | "generate": "rm -rf ./proto && node generate.js", 10 | "preinstall": "npx only-allow pnpm", 11 | "lint": "tsc --noEmit && eslint --ext .ts,.tsx --max-warnings=0 .", 12 | "prettier": "prettier --write .", 13 | "prettier:check": "prettier --check .", 14 | "start": "pnpm generate && webpack --watch --config webpack.dev.js", 15 | "start:enterprise": "pnpm generate && webpack --watch --config webpack.dev.js --env enterprise" 16 | }, 17 | "husky": { 18 | "hooks": { 19 | "pre-commit": "lint-staged" 20 | } 21 | }, 22 | "lint-staged": { 23 | "*.{ts,js}": [ 24 | "pnpm prettier", 25 | "pnpm lint" 26 | ] 27 | }, 28 | "browserslist": "last 10 Chrome versions", 29 | "dependencies": { 30 | "@babel/runtime": "^7.18.6", 31 | "@bufbuild/protobuf": "1.9.0", 32 | "@connectrpc/connect": "1.4.0", 33 | "@connectrpc/connect-web": "1.4.0", 34 | "@emotion/react": "^11.10.6", 35 | "@emotion/styled": "^11.10.6", 36 | "@mui/icons-material": "^5.11.16", 37 | "@mui/material": "^5.12.2", 38 | "normalize.css": "^8.0.1", 39 | "object-assign": "^4.1.1", 40 | "react": "^17.0.2", 41 | "react-dom": "^17.0.2", 42 | "uuid": "^9.0.0" 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.21.4", 46 | "@babel/plugin-transform-runtime": "^7.18.6", 47 | "@babel/preset-env": "^7.21.4", 48 | "@babel/preset-react": "^7.18.6", 49 | "@babel/preset-typescript": "^7.18.6", 50 | "@bufbuild/buf": "1.36.0", 51 | "@bufbuild/protoc-gen-es": "1.9.0", 52 | "@connectrpc/protoc-gen-connect-es": "1.4.0", 53 | "@jupyterlab/application": "^3.5.2", 54 | "@jupyterlab/codeeditor": "^3.5.2", 55 | "@jupyterlab/codemirror": "^3.5.2", 56 | "@jupyterlab/docmanager": "^3.5.2", 57 | "@jupyterlab/fileeditor": "^3.5.2", 58 | "@jupyterlab/notebook": "^3.5.2", 59 | "@lumino/disposable": "^2.1.2", 60 | "@lumino/widgets": "^1.36.0", 61 | "@types/chrome": "^0.0.246", 62 | "@types/codemirror": "^5.60.6", 63 | "@types/prop-types": "^15.7.8", 64 | "@types/react": "^17.0.2", 65 | "@types/react-dom": "^17.0.21", 66 | "@types/uuid": "^9.0.1", 67 | "@typescript-eslint/eslint-plugin": "^5.48.1", 68 | "@typescript-eslint/parser": "^5.48.1", 69 | "babel-loader": "^8.3.0", 70 | "copy-webpack-plugin": "^11.0.0", 71 | "css-loader": "^6.7.1", 72 | "dotenv": "^16.3.1", 73 | "dotenv-webpack": "^8.0.0", 74 | "eslint": "^8.31.0", 75 | "eslint-config-prettier": "^8.6.0", 76 | "eslint-plugin-import": "^2.27.4", 77 | "eslint-plugin-prettier": "^4.2.1", 78 | "eslint-webpack-plugin": "^3.2.0", 79 | "html-webpack-plugin": "^5.5.1", 80 | "husky": "^8.0.1", 81 | "lint-staged": "^13.0.3", 82 | "mini-css-extract-plugin": "^2.6.1", 83 | "monaco-editor": "^0.34.1", 84 | "prettier": "^2.8.2", 85 | "sass": "^1.53.0", 86 | "sass-loader": "^13.0.2", 87 | "svg-inline-loader": "^0.8.2", 88 | "typescript": "^4.9.4", 89 | "utility-types": "^3.10.0", 90 | "webpack": "^5.80.0", 91 | "webpack-cli": "^4.10.0", 92 | "webpack-dev-server": "^4.13.3", 93 | "webpack-merge": "^5.8.0", 94 | "yjs": "^13.6.8" 95 | }, 96 | "pnpm": { 97 | "patchedDependencies": { 98 | "@jupyterlab/codeeditor@3.5.2": "patches/@jupyterlab+codeeditor+3.5.2.patch", 99 | "@jupyterlab/shared-models@3.5.2": "patches/@jupyterlab+shared-models+3.5.2.patch" 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /patches/@jupyterlab+codeeditor+3.5.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/lib/editor.d.ts b/lib/editor.d.ts 2 | index ffe8d1f..b4fc388 100644 3 | --- a/lib/editor.d.ts 4 | +++ b/lib/editor.d.ts 5 | @@ -44,6 +44,7 @@ export declare namespace CodeEditor { 6 | /** 7 | * An interface describing editor state coordinates. 8 | */ 9 | + // @ts-ignore 10 | interface ICoordinate extends JSONObject, ClientRect { 11 | } 12 | /** 13 | -------------------------------------------------------------------------------- /patches/@jupyterlab+shared-models+3.5.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/lib/ymodels.d.ts b/lib/ymodels.d.ts 2 | index 95ed330..10f65b5 100644 3 | --- a/lib/ymodels.d.ts 4 | +++ b/lib/ymodels.d.ts 5 | @@ -14,7 +14,7 @@ export interface IYText extends models.ISharedText { 6 | readonly undoManager: Y.UndoManager | null; 7 | } 8 | export declare type YCellType = YRawCell | YCodeCell | YMarkdownCell; 9 | -export declare class YDocument implements models.ISharedDocument { 10 | +export declare class YDocument implements models.ISharedDocument { 11 | get dirty(): boolean; 12 | set dirty(value: boolean); 13 | /** 14 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - . 3 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | singleQuote: true, 4 | printWidth: 100, 5 | tabWidth: 2, 6 | useTabs: false, 7 | trailingComma: 'es5', 8 | bracketSpacing: true, 9 | }; 10 | -------------------------------------------------------------------------------- /src/auth.ts: -------------------------------------------------------------------------------- 1 | import { createPromiseClient } from '@connectrpc/connect'; 2 | import { createConnectTransport } from '@connectrpc/connect-web'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | 5 | import { getGeneralProfileUrl, getStorageItem } from './storage'; 6 | import { PUBLIC_API_SERVER } from './urls'; 7 | import { SeatManagementService } from '../proto/exa/seat_management_pb/seat_management_connect'; 8 | 9 | // Runs in popup. 10 | // TODO(prem): Move to a popup-specific source file. 11 | export async function openAuthTab(): Promise { 12 | const uuid = uuidv4(); 13 | await chrome.runtime.sendMessage({ 14 | type: 'state', 15 | payload: { 16 | state: uuid, 17 | }, 18 | }); 19 | const profileUrl = await getGeneralProfileUrl(); 20 | if (profileUrl === undefined) { 21 | return; 22 | } 23 | 24 | await chrome.tabs.create({ 25 | url: `${profileUrl}?redirect_uri=chrome-extension://${chrome.runtime.id}&state=${uuid}`, 26 | }); 27 | } 28 | 29 | async function getApiServerUrl(): Promise { 30 | const portalUrl = (await getStorageItem('portalUrl'))?.trim(); 31 | if (portalUrl === undefined || portalUrl === '') { 32 | if (CODEIUM_ENTERPRISE) { 33 | return undefined; 34 | } 35 | return PUBLIC_API_SERVER; 36 | } 37 | return `${portalUrl.replace(/\/$/, '')}/_route/api_server`; 38 | } 39 | 40 | // Runs in service worker. 41 | // TODO(prem): Move to a service worker-specific source file. 42 | export async function registerUser(token: string): Promise<{ api_key: string; name: string }> { 43 | const apiServerUrl = await getApiServerUrl(); 44 | if (apiServerUrl === undefined) { 45 | throw new Error('apiServerUrl is undefined'); 46 | } 47 | const client = createPromiseClient( 48 | SeatManagementService, 49 | createConnectTransport({ 50 | baseUrl: apiServerUrl, 51 | useBinaryFormat: true, 52 | defaultTimeoutMs: 5000, 53 | }) 54 | ); 55 | const response = await client.registerUser({ 56 | firebaseIdToken: token, 57 | }); 58 | return { 59 | api_key: response.apiKey, 60 | name: response.name, 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/codemirror.ts: -------------------------------------------------------------------------------- 1 | import { IDisposable } from '@lumino/disposable'; 2 | import type CodeMirror from 'codemirror'; 3 | 4 | import { editorLanguage, language } from './codemirrorLanguages'; 5 | import { CODEIUM_DEBUG, IdeInfo, KeyCombination, LanguageServerClient } from './common'; 6 | import { TextAndOffsets, computeTextAndOffsets } from './notebook'; 7 | import { numUtf8BytesToNumCodeUnits } from './utf'; 8 | import { EditorOptions } from '../proto/exa/codeium_common_pb/codeium_common_pb'; 9 | import { 10 | CompletionItem, 11 | CompletionPartType, 12 | GetCompletionsRequest, 13 | } from '../proto/exa/language_server_pb/language_server_pb'; 14 | 15 | function computeTextAndOffsetsForCodeMirror( 16 | isNotebook: boolean, 17 | textModels: CodeMirror.Doc[], 18 | currentTextModel: CodeMirror.Doc, 19 | currentTextModelWithOutput: CodeMirror.Doc | undefined 20 | ): TextAndOffsets { 21 | return computeTextAndOffsets({ 22 | textModels, 23 | currentTextModel, 24 | currentTextModelWithOutput: currentTextModelWithOutput, 25 | isNotebook: isNotebook, 26 | utf16CodeUnitOffset: currentTextModel.indexFromPos(currentTextModel.getCursor()), 27 | getText: (model) => model.getValue(), 28 | getLanguage: (model) => language(model, undefined), 29 | }); 30 | } 31 | 32 | interface TextMarker { 33 | pos: CodeMirror.Position; 34 | marker: CodeMirror.TextMarker; 35 | spanElement: HTMLSpanElement; 36 | } 37 | 38 | // Helps simulate a typing as completed effect. Will only work on the same line. 39 | function maybeUpdateTextMarker( 40 | textMarker: TextMarker, 41 | ch: string, 42 | cursor: CodeMirror.Position, 43 | characterBeforeCursor: string 44 | ): boolean { 45 | if (cursor.line != textMarker.pos.line || cursor.ch != textMarker.pos.ch) { 46 | return false; 47 | } 48 | if (ch === 'Backspace') { 49 | if (characterBeforeCursor === '') { 50 | return false; 51 | } 52 | textMarker.spanElement.innerText = characterBeforeCursor + textMarker.spanElement.innerText; 53 | return true; 54 | } 55 | if (ch.length > 1 || ch === '\n') { 56 | return false; 57 | } 58 | const innerText = textMarker.spanElement.innerText; 59 | if (innerText.length === 1) { 60 | // TODO(prem): Why is this necessary? 61 | // This was necessary for the following case: 62 | // In GitHub, type "def fib(n)" and accept the completion. 63 | // Then go to a new line in the function and type "fib(5)". 64 | // On the ")", it should freeze. 65 | return false; 66 | } 67 | if (!innerText.startsWith(ch)) { 68 | return false; 69 | } 70 | textMarker.spanElement.innerText = textMarker.spanElement.innerText.substring(1); 71 | return true; 72 | } 73 | 74 | export class CodeMirrorManager { 75 | private client: LanguageServerClient; 76 | private ideInfo: IdeInfo; 77 | private currentCompletion?: { 78 | completionItem: CompletionItem; 79 | lineWidgets: CodeMirror.LineWidget[]; 80 | textMarkers: TextMarker[]; 81 | disposables: IDisposable[]; 82 | doc: CodeMirror.Doc; 83 | start: CodeMirror.Position; 84 | end: CodeMirror.Position; 85 | docState: string; 86 | }; 87 | 88 | constructor(extensionId: string, ideInfo: IdeInfo) { 89 | this.client = new LanguageServerClient(extensionId); 90 | this.ideInfo = ideInfo; 91 | } 92 | 93 | documentMatchesCompletion(): boolean { 94 | if (this.currentCompletion?.doc.getValue() !== this.currentCompletion?.docState) { 95 | return false; 96 | } 97 | return true; 98 | } 99 | 100 | anyTextMarkerUpdated( 101 | ch: string, 102 | cursor: CodeMirror.Position, 103 | characterBeforeCursor: string 104 | ): boolean { 105 | return ( 106 | this.currentCompletion?.textMarkers.find((textMarker) => 107 | maybeUpdateTextMarker(textMarker, ch, cursor, characterBeforeCursor) 108 | ) !== undefined 109 | ); 110 | } 111 | 112 | async triggerCompletion( 113 | isNotebook: boolean, 114 | textModels: CodeMirror.Doc[], 115 | currentTextModel: CodeMirror.Doc, 116 | currentTextModelWithOutput: CodeMirror.Doc | undefined, 117 | editorOptions: EditorOptions, 118 | relativePath: string | undefined, 119 | createDisposables: (() => IDisposable[]) | undefined 120 | ): Promise { 121 | const cursor = currentTextModel.getCursor(); 122 | const { text, utf8ByteOffset, additionalUtf8ByteOffset } = computeTextAndOffsetsForCodeMirror( 123 | isNotebook, 124 | textModels, 125 | currentTextModel, 126 | currentTextModelWithOutput 127 | ); 128 | const numUtf8Bytes = additionalUtf8ByteOffset + utf8ByteOffset; 129 | const request = new GetCompletionsRequest({ 130 | metadata: this.client.getMetadata(this.ideInfo), 131 | document: { 132 | text, 133 | editorLanguage: editorLanguage(currentTextModel), 134 | language: language(currentTextModel, relativePath), 135 | cursorOffset: BigInt(numUtf8Bytes), 136 | lineEnding: '\n', 137 | // We could use the regular path which could have a drive: prefix, but 138 | // this is probably unusual. 139 | absoluteUri: `file:///${relativePath}`, 140 | }, 141 | editorOptions, 142 | }); 143 | const response = await this.client.getCompletions(request); 144 | if (response === undefined) { 145 | return; 146 | } 147 | 148 | // No more await allowed below this point, given that we've checked for 149 | // abort, so this must be the latest debounced request. 150 | this.clearCompletion( 151 | "about to replace completions if the cursor hasn't moved and we got completions" 152 | ); 153 | const newCursor = currentTextModel.getCursor(); 154 | if (newCursor.ch !== cursor.ch || newCursor.line !== cursor.line) { 155 | // TODO(prem): Is this check necessary? 156 | return; 157 | } 158 | if (response.completionItems.length === 0) { 159 | return; 160 | } 161 | const completionItem = response.completionItems[0]; 162 | this.renderCompletion( 163 | currentTextModel, 164 | completionItem, 165 | additionalUtf8ByteOffset, 166 | createDisposables ? createDisposables : () => [] 167 | ); 168 | } 169 | 170 | clearCompletion(reason: string): boolean { 171 | const currentCompletion = this.currentCompletion; 172 | if (currentCompletion === undefined) { 173 | return false; 174 | } 175 | if (CODEIUM_DEBUG) { 176 | console.log('Clearing completions because', reason); 177 | } 178 | currentCompletion.disposables.forEach((disposable) => { 179 | disposable.dispose(); 180 | }); 181 | currentCompletion.lineWidgets.forEach((widget) => { 182 | widget.clear(); 183 | }); 184 | currentCompletion.textMarkers.forEach((marker) => { 185 | marker.marker.clear(); 186 | }); 187 | this.currentCompletion = undefined; 188 | return true; 189 | } 190 | 191 | renderCompletion( 192 | doc: CodeMirror.Doc, 193 | completionItem: CompletionItem, 194 | additionalUtf8ByteOffset: number, 195 | createDisposables: () => IDisposable[] 196 | ): void { 197 | this.clearCompletion('about to render new completions'); 198 | const startOffsetUtf8Bytes = 199 | Number(completionItem.range?.startOffset ?? 0) - additionalUtf8ByteOffset; 200 | const endOffsetUtf8Bytes = 201 | Number(completionItem.range?.endOffset ?? 0) - additionalUtf8ByteOffset; 202 | const currentCompletion: typeof this.currentCompletion = { 203 | completionItem, 204 | lineWidgets: [], 205 | textMarkers: [], 206 | disposables: createDisposables(), 207 | doc, 208 | start: doc.posFromIndex(numUtf8BytesToNumCodeUnits(doc.getValue(), startOffsetUtf8Bytes)), 209 | end: doc.posFromIndex(numUtf8BytesToNumCodeUnits(doc.getValue(), endOffsetUtf8Bytes)), 210 | docState: doc.getValue(), 211 | }; 212 | const cursor = doc.getCursor(); 213 | let createdInlineAtCursor = false; 214 | completionItem.completionParts.forEach((part) => { 215 | if (part.type === CompletionPartType.INLINE) { 216 | const bookmarkElement = document.createElement('span'); 217 | bookmarkElement.classList.add('codeium-ghost'); 218 | bookmarkElement.innerText = part.text; 219 | const partOffsetBytes = Number(part.offset) - additionalUtf8ByteOffset; 220 | const partOffset = numUtf8BytesToNumCodeUnits(doc.getValue(), partOffsetBytes); 221 | const pos = doc.posFromIndex(partOffset); 222 | const bookmarkWidget = doc.setBookmark(pos, { 223 | widget: bookmarkElement, 224 | insertLeft: true, 225 | // We need all widgets to have handleMouseEvents true for the glitches 226 | // where the completion doesn't disappear. 227 | handleMouseEvents: true, 228 | }); 229 | currentCompletion.textMarkers.push({ 230 | marker: bookmarkWidget, 231 | pos, 232 | spanElement: bookmarkElement, 233 | }); 234 | if (pos.line === cursor.line && pos.ch === cursor.ch) { 235 | createdInlineAtCursor = true; 236 | } 237 | } else if (part.type === CompletionPartType.BLOCK) { 238 | // We use CodeMirror's LineWidget feature to render the block ghost text element. 239 | const lineElement = document.createElement('div'); 240 | lineElement.classList.add('codeium-ghost'); 241 | part.text.split('\n').forEach((line) => { 242 | const preElement = document.createElement('pre'); 243 | preElement.classList.add('CodeMirror-line', 'codeium-ghost-line'); 244 | if (line === '') { 245 | line = ' '; 246 | } 247 | preElement.innerText = line; 248 | lineElement.appendChild(preElement); 249 | }); 250 | const lineWidget = doc.addLineWidget(cursor.line, lineElement, { handleMouseEvents: true }); 251 | currentCompletion.lineWidgets.push(lineWidget); 252 | } 253 | }); 254 | if (!createdInlineAtCursor) { 255 | // This is to handle the edge case of faking typing as completed but with 256 | // a backspace at the end of the line, where there might not be an INLINE 257 | // completion part. 258 | const bookmarkElement = document.createElement('span'); 259 | bookmarkElement.classList.add('codeium-ghost'); 260 | bookmarkElement.innerText = ''; 261 | const bookmarkWidget = doc.setBookmark(cursor, { 262 | widget: bookmarkElement, 263 | insertLeft: true, 264 | }); 265 | currentCompletion.textMarkers.push({ 266 | marker: bookmarkWidget, 267 | pos: cursor, 268 | spanElement: bookmarkElement, 269 | }); 270 | } 271 | this.currentCompletion = currentCompletion; 272 | } 273 | 274 | acceptCompletion(): boolean { 275 | const completion = this.currentCompletion; 276 | if (completion === undefined) { 277 | return false; 278 | } 279 | this.clearCompletion('about to accept completions'); 280 | const completionProto = completion.completionItem.completion; 281 | if (completionProto === undefined) { 282 | console.error('Empty completion'); 283 | return true; 284 | } 285 | const doc = completion.doc; 286 | // This is a hack since we have the fake typing as completed logic. 287 | doc.setCursor(completion.start); 288 | doc.replaceRange(completionProto.text, completion.start, completion.end); 289 | if ( 290 | completion.completionItem.suffix !== undefined && 291 | completion.completionItem.suffix.text.length > 0 292 | ) { 293 | doc.replaceRange(completion.completionItem.suffix.text, doc.getCursor()); 294 | const currentCursor = doc.getCursor(); 295 | const newOffset = 296 | doc.indexFromPos(currentCursor) + 297 | Number(completion.completionItem.suffix.deltaCursorOffset); 298 | doc.setCursor(doc.posFromIndex(newOffset)); 299 | } 300 | this.client.acceptedLastCompletion(this.ideInfo, completionProto.completionId); 301 | return true; 302 | } 303 | 304 | // If this returns false, don't consume the event. 305 | // If true, consume the event. 306 | // Otherwise, keep going with other logic. 307 | beforeMainKeyHandler( 308 | doc: CodeMirror.Doc, 309 | event: KeyboardEvent, 310 | alsoHandle: { tab: boolean; escape: boolean }, 311 | keyCombination?: KeyCombination 312 | ): { consumeEvent: boolean | undefined; forceTriggerCompletion: boolean } { 313 | let forceTriggerCompletion = false; 314 | if (event.ctrlKey && event.key === ' ') { 315 | forceTriggerCompletion = true; 316 | } 317 | 318 | // Classic notebook may autocomplete these. 319 | if ('"\')}]'.includes(event.key)) { 320 | forceTriggerCompletion = true; 321 | } 322 | 323 | if (event.isComposing) { 324 | this.clearCompletion('composing'); 325 | return { consumeEvent: false, forceTriggerCompletion }; 326 | } 327 | 328 | // Accept completion logic 329 | if (keyCombination) { 330 | const matchesKeyCombination = 331 | event.key.toLowerCase() === keyCombination.key.toLowerCase() && 332 | !!event.ctrlKey === !!keyCombination.ctrl && 333 | !!event.altKey === !!keyCombination.alt && 334 | !!event.shiftKey === !!keyCombination.shift && 335 | !!event.metaKey === !!keyCombination.meta; 336 | 337 | if (matchesKeyCombination && this.acceptCompletion()) { 338 | return { consumeEvent: true, forceTriggerCompletion }; 339 | } 340 | } 341 | 342 | // TODO(kevin): clean up autoHandle logic 343 | // Currently we have: 344 | // Jupyter Notebook: tab = true, escape = false 345 | // Code Mirror Websites: tab = true, escape = true 346 | // Jupyter Lab: tab = false, escape = false 347 | 348 | // Code Mirror Websites only 349 | if ( 350 | !event.metaKey && 351 | !event.ctrlKey && 352 | !event.altKey && 353 | !event.shiftKey && 354 | alsoHandle.escape && 355 | event.key === 'Escape' && 356 | this.clearCompletion('user dismissed') 357 | ) { 358 | return { consumeEvent: true, forceTriggerCompletion }; 359 | } 360 | 361 | // Shift-tab in jupyter notebooks shows documentation. 362 | if (event.key === 'Tab' && event.shiftKey) { 363 | return { consumeEvent: false, forceTriggerCompletion }; 364 | } 365 | 366 | switch (event.key) { 367 | case 'Delete': 368 | case 'ArrowDown': 369 | case 'ArrowUp': 370 | case 'ArrowLeft': 371 | case 'ArrowRight': 372 | case 'Home': 373 | case 'End': 374 | case 'PageDown': 375 | case 'PageUp': 376 | this.clearCompletion(`key: ${event.key}`); 377 | return { consumeEvent: false, forceTriggerCompletion }; 378 | } 379 | const cursor = doc.getCursor(); 380 | const characterBeforeCursor = 381 | cursor.ch === 0 ? '' : doc.getRange({ line: cursor.line, ch: cursor.ch - 1 }, cursor); 382 | const anyTextMarkerUpdated = this.anyTextMarkerUpdated( 383 | event.key, 384 | cursor, 385 | characterBeforeCursor 386 | ); 387 | // We don't want caps lock to trigger a clearing of the completion, for example. 388 | if (!anyTextMarkerUpdated && event.key.length === 1) { 389 | this.clearCompletion("didn't update text marker and key is a single character"); 390 | } 391 | if (event.key === 'Enter') { 392 | this.clearCompletion('enter'); 393 | } 394 | return { consumeEvent: undefined, forceTriggerCompletion }; 395 | } 396 | 397 | clearCompletionInitHook(): (editor: CodeMirror.Editor) => void { 398 | const editors = new WeakSet(); 399 | return (editor: CodeMirror.Editor) => { 400 | if (editors.has(editor)) { 401 | return; 402 | } 403 | editors.add(editor); 404 | const el = editor.getInputField().closest('.CodeMirror'); 405 | if (el === null) { 406 | return; 407 | } 408 | const div = el as HTMLDivElement; 409 | div.addEventListener('focusout', () => { 410 | this.clearCompletion('focusout'); 411 | }); 412 | div.addEventListener('mousedown', () => { 413 | this.clearCompletion('mousedown'); 414 | }); 415 | const mutationObserver = new MutationObserver(() => { 416 | // Check for jupyterlab-vim command mode. 417 | if (div.classList.contains('cm-fat-cursor')) { 418 | this.clearCompletion('vim'); 419 | } 420 | }); 421 | mutationObserver.observe(div, { 422 | attributes: true, 423 | attributeFilter: ['class'], 424 | }); 425 | const completer = document.body.querySelector('.jp-Completer'); 426 | if (completer !== null) { 427 | const completerMutationObserver = new MutationObserver(() => { 428 | if (!completer?.classList.contains('lm-mod-hidden')) { 429 | this.clearCompletion('completer'); 430 | } 431 | }); 432 | completerMutationObserver.observe(completer, { 433 | attributes: true, 434 | attributeFilter: ['class'], 435 | }); 436 | } 437 | }; 438 | } 439 | } 440 | 441 | export function addListeners( 442 | cm: typeof import('codemirror'), 443 | codeMirrorManager: CodeMirrorManager 444 | ) { 445 | cm.defineInitHook(codeMirrorManager.clearCompletionInitHook()); 446 | } 447 | -------------------------------------------------------------------------------- /src/codemirrorInject.ts: -------------------------------------------------------------------------------- 1 | import { CodeMirrorManager } from './codemirror'; 2 | import { EditorOptions } from '../proto/exa/codeium_common_pb/codeium_common_pb'; 3 | 4 | declare type CodeMirror = typeof import('codemirror'); 5 | 6 | export class CodeMirrorState { 7 | codeMirrorManager: CodeMirrorManager; 8 | docs: CodeMirror.Doc[] = []; 9 | debounceMs: number = 0; 10 | hookedEditors = new WeakSet(); 11 | constructor( 12 | extensionId: string, 13 | cm: CodeMirror | undefined, 14 | readonly multiplayer: boolean, 15 | debounceMs?: number 16 | ) { 17 | this.codeMirrorManager = new CodeMirrorManager(extensionId, { 18 | ideName: 'codemirror', 19 | ideVersion: `${cm?.version ?? 'unknown'}-${window.location.hostname}`, 20 | }); 21 | if (cm !== undefined) { 22 | cm.defineInitHook(this.editorHook()); 23 | } 24 | this.debounceMs = debounceMs ?? 0; 25 | } 26 | 27 | editorHook(): (editor: CodeMirror.Editor) => void { 28 | const hook = this.codeMirrorManager.clearCompletionInitHook(); 29 | return (editor) => { 30 | if (this.hookedEditors.has(editor)) { 31 | return; 32 | } 33 | this.hookedEditors.add(editor); 34 | this.addKeydownListener(editor, this.multiplayer); 35 | hook(editor); 36 | }; 37 | } 38 | 39 | addKeydownListener(editor: CodeMirror.Editor, multiplayer: boolean) { 40 | const el = editor.getInputField().closest('.CodeMirror'); 41 | if (el === null) { 42 | return; 43 | } 44 | if (multiplayer) { 45 | // This isn't always turned on because it blocks the visual improvements 46 | // from maybeUpdateTextMarker. 47 | editor.on('change', () => { 48 | if (!this.codeMirrorManager.documentMatchesCompletion()) { 49 | this.codeMirrorManager.clearCompletion('document changed'); 50 | } 51 | }); 52 | } 53 | editor.on('keydown', (editor: CodeMirror.Editor, event: KeyboardEvent) => { 54 | const { consumeEvent, forceTriggerCompletion } = this.codeMirrorManager.beforeMainKeyHandler( 55 | editor.getDoc(), 56 | event, 57 | { tab: true, escape: true }, 58 | { key: 'Tab', ctrl: false, alt: false, shift: false, meta: false } 59 | ); 60 | if (consumeEvent !== undefined) { 61 | if (consumeEvent) { 62 | event.preventDefault(); 63 | } 64 | return; 65 | } 66 | const doc = editor.getDoc(); 67 | const oldString = doc.getValue(); 68 | setTimeout(async () => { 69 | if (!forceTriggerCompletion) { 70 | const newString = doc.getValue(); 71 | if (newString === oldString) { 72 | // Cases like arrow keys, page up/down, etc. should fall here. 73 | return; 74 | } 75 | } 76 | 77 | await this.codeMirrorManager.triggerCompletion( 78 | false, // isNotebook 79 | this.docs, 80 | editor.getDoc(), 81 | undefined, 82 | new EditorOptions({ 83 | tabSize: BigInt(editor.getOption('tabSize') ?? 4), 84 | insertSpaces: !(editor.getOption('indentWithTabs') ?? false), 85 | }), 86 | undefined, 87 | undefined 88 | ); 89 | }, this.debounceMs); 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/codemirrorLanguages.ts: -------------------------------------------------------------------------------- 1 | import type CodeMirror from 'codemirror'; 2 | 3 | import { Language } from '../proto/exa/codeium_common_pb/codeium_common_pb'; 4 | 5 | // https://github.com/codemirror/codemirror5/blob/9e864a1bb7c4c452f462d7f8d8be111c8bb8ad6f/mode/meta.js 6 | 7 | const MIME_MAP = new Map([ 8 | // mode: clike 9 | ['text/x-csrc', Language.C], 10 | ['text/x-c++src', Language.CPP], 11 | ['text/x-csharp', Language.CSHARP], 12 | ['text/x-java', Language.JAVA], 13 | ['text/x-kotlin', Language.KOTLIN], 14 | ['text/x-objectivec', Language.OBJECTIVEC], 15 | ['text/x-objectivec++', Language.OBJECTIVECPP], 16 | ['text/x-scala', Language.SCALA], 17 | // mode: css 18 | ['text/css', Language.CSS], 19 | ['text/x-less', Language.LESS], 20 | ['text/x-sass', Language.SASS], 21 | ['text/x-scss', Language.SCSS], 22 | // mode: javascript 23 | ['application/json', Language.JSON], 24 | ['application/x-json', Language.JSON], 25 | ['application/ld+json', Language.JSON], 26 | ['application/typescript', Language.TYPESCRIPT], 27 | // mode: jsx 28 | ['text/jsx', Language.JAVASCRIPT], // We (and tree-sitter) don't have a separate JSX. 29 | ['text/typescript-jsx', Language.TSX], 30 | // mode: mllike 31 | ['text/x-ocaml', Language.OCAML], 32 | // Jupyterlab specific 33 | ['text/x-ipython', Language.PYTHON], 34 | ]); 35 | 36 | const MODE_MAP = new Map([ 37 | ['clojure', Language.CLOJURE], 38 | ['coffeescript', Language.COFFEESCRIPT], 39 | ['python', Language.PYTHON], // Includes Cython. 40 | ['sql', Language.SQL], // Includes Cassandra, MariaDB, MS SQL, MySQL, PLSQL, PostgreSQL, SQL, SQLite. 41 | ['dart', Language.DART], 42 | ['gfm', Language.MARKDOWN], 43 | ['go', Language.GO], 44 | ['groovy', Language.GROOVY], 45 | ['haskell', Language.HASKELL], 46 | ['haskell-literate', Language.HASKELL], // TODO(prem): Should this be different? 47 | ['htmlmixed', Language.HTML], // Includes handlebars. 48 | ['javascript', Language.JAVASCRIPT], 49 | ['julia', Language.JULIA], 50 | ['lua', Language.LUA], 51 | ['markdown', Language.MARKDOWN], 52 | ['perl', Language.PERL], 53 | ['php', Language.PHP], 54 | ['null', Language.PLAINTEXT], 55 | ['protobuf', Language.PROTOBUF], 56 | ['r', Language.R], 57 | ['rst', Language.RST], 58 | ['ruby', Language.RUBY], 59 | ['rust', Language.RUST], 60 | ['shell', Language.SHELL], 61 | ['swift', Language.SWIFT], 62 | ['stex', Language.LATEX], 63 | ['toml', Language.TOML], 64 | ['vue', Language.VUE], 65 | ['xml', Language.XML], 66 | ['yaml', Language.YAML], 67 | // Special cases. 68 | ['ipython', Language.PYTHON], 69 | ['ipythongfm', Language.MARKDOWN], 70 | ]); 71 | 72 | const FILENAME_MAP = new Map([ 73 | // These are special entries because the mime/mode are the same as Python. 74 | [/^BUILD$/, Language.STARLARK], 75 | [/^.+\.bzl$/, Language.STARLARK], 76 | ]); 77 | 78 | function getMode(doc: CodeMirror.Doc): { name: string } { 79 | if (doc.getMode() !== undefined) { 80 | return doc.getMode() as { name: string }; 81 | } 82 | return doc.modeOption as { name: string }; 83 | } 84 | 85 | // Note that this cannot be mapped directly into the Language enum. 86 | export function editorLanguage(doc: CodeMirror.Doc): string { 87 | return getMode(doc).name; 88 | } 89 | 90 | export function language(doc: CodeMirror.Doc, path: string | undefined): Language { 91 | if (path !== undefined) { 92 | const basename = path.split('/').pop() ?? ''; 93 | // Iterate over FILENAME_MAP for a match. 94 | for (const [regex, language] of FILENAME_MAP) { 95 | if (regex.test(basename)) { 96 | return language; 97 | } 98 | } 99 | } 100 | const mime = doc.getEditor()?.getOption('mode') ?? doc.modeOption; 101 | if (typeof mime === 'string') { 102 | const language = MIME_MAP.get(mime); 103 | if (language !== undefined) { 104 | return language; 105 | } 106 | } 107 | return MODE_MAP.get(getMode(doc).name) ?? Language.UNSPECIFIED; 108 | } 109 | -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | import { PartialMessage } from '@bufbuild/protobuf'; 2 | import { Code, ConnectError, PromiseClient, createPromiseClient } from '@connectrpc/connect'; 3 | import { createConnectTransport } from '@connectrpc/connect-web'; 4 | import { v4 as uuidv4 } from 'uuid'; 5 | 6 | import { getStorageItems } from './storage'; 7 | import { Metadata } from '../proto/exa/codeium_common_pb/codeium_common_pb'; 8 | import { LanguageServerService } from '../proto/exa/language_server_pb/language_server_connect'; 9 | import { 10 | AcceptCompletionRequest, 11 | GetCompletionsRequest, 12 | GetCompletionsResponse, 13 | } from '../proto/exa/language_server_pb/language_server_pb'; 14 | 15 | const EXTENSION_NAME = 'chrome'; 16 | const EXTENSION_VERSION = '1.26.3'; 17 | 18 | export const CODEIUM_DEBUG = false; 19 | export const DEFAULT_PATH = 'unknown_url'; 20 | 21 | export interface ClientSettings { 22 | apiKey?: string; 23 | defaultModel?: string; 24 | } 25 | 26 | export interface KeyCombination { 27 | key: string; 28 | ctrl?: boolean; 29 | alt?: boolean; 30 | shift?: boolean; 31 | meta?: boolean; 32 | } 33 | 34 | export interface JupyterLabKeyBindings { 35 | accept: KeyCombination; 36 | dismiss: KeyCombination; 37 | } 38 | 39 | export interface JupyterNotebookKeyBindings { 40 | accept: KeyCombination; 41 | } 42 | 43 | async function getClientSettings(): Promise { 44 | const storageItems = await getStorageItems(['user', 'enterpriseDefaultModel']); 45 | return { 46 | apiKey: storageItems.user?.apiKey, 47 | defaultModel: storageItems.enterpriseDefaultModel, 48 | }; 49 | } 50 | 51 | function languageServerClient(baseUrl: string): PromiseClient { 52 | const transport = createConnectTransport({ 53 | baseUrl, 54 | useBinaryFormat: true, 55 | }); 56 | return createPromiseClient(LanguageServerService, transport); 57 | } 58 | 59 | class ClientSettingsPoller { 60 | // This is initialized to a promise at construction, then updated to a 61 | // non-promise later. 62 | clientSettings: Promise | ClientSettings; 63 | constructor() { 64 | this.clientSettings = getClientSettings(); 65 | setInterval(async () => { 66 | this.clientSettings = await getClientSettings(); 67 | }, 500); 68 | } 69 | } 70 | 71 | export interface IdeInfo { 72 | ideName: string; 73 | ideVersion: string; 74 | } 75 | 76 | export class LanguageServerServiceWorkerClient { 77 | // Note that the URL won't refresh post-initialization. 78 | client: Promise | undefined>; 79 | private abortController?: AbortController; 80 | clientSettingsPoller: ClientSettingsPoller; 81 | 82 | constructor(baseUrlPromise: Promise, private readonly sessionId: string) { 83 | this.client = (async (): Promise | undefined> => { 84 | const baseUrl = await baseUrlPromise; 85 | if (baseUrl === undefined) { 86 | return undefined; 87 | } 88 | return languageServerClient(baseUrl); 89 | })(); 90 | this.clientSettingsPoller = new ClientSettingsPoller(); 91 | } 92 | 93 | getHeaders(apiKey: string | undefined): Record { 94 | if (apiKey === undefined) { 95 | return {}; 96 | } 97 | const Authorization = `Basic ${apiKey}-${this.sessionId}`; 98 | return { Authorization }; 99 | } 100 | 101 | async getCompletions( 102 | request: GetCompletionsRequest 103 | ): Promise { 104 | this.abortController?.abort(); 105 | this.abortController = new AbortController(); 106 | const clientSettings = await this.clientSettingsPoller.clientSettings; 107 | if (clientSettings.apiKey === undefined || request.metadata === undefined) { 108 | return; 109 | } 110 | request.metadata.apiKey = clientSettings.apiKey; 111 | request.modelName = clientSettings.defaultModel ?? ''; 112 | const signal = this.abortController.signal; 113 | const getCompletionsPromise = (await this.client)?.getCompletions(request, { 114 | signal, 115 | headers: this.getHeaders(request.metadata?.apiKey), 116 | }); 117 | try { 118 | return await getCompletionsPromise; 119 | } catch (err) { 120 | if (signal.aborted) { 121 | return; 122 | } 123 | if (err instanceof ConnectError) { 124 | if (err.code != Code.Canceled) { 125 | console.log(err.message); 126 | void chrome.runtime.sendMessage(chrome.runtime.id, { 127 | type: 'error', 128 | message: err.message, 129 | }); 130 | } 131 | } else { 132 | console.log((err as Error).message); 133 | void chrome.runtime.sendMessage(chrome.runtime.id, { 134 | type: 'error', 135 | message: (err as Error).message, 136 | }); 137 | } 138 | return; 139 | } 140 | } 141 | 142 | async acceptedLastCompletion( 143 | acceptCompletionRequest: PartialMessage 144 | ): Promise { 145 | if (acceptCompletionRequest.metadata === undefined) { 146 | return; 147 | } 148 | try { 149 | const clientSettings = await this.clientSettingsPoller.clientSettings; 150 | acceptCompletionRequest.metadata.apiKey = clientSettings.apiKey; 151 | await ( 152 | await this.client 153 | )?.acceptCompletion(acceptCompletionRequest, { 154 | headers: this.getHeaders(acceptCompletionRequest.metadata?.apiKey), 155 | }); 156 | } catch (err) { 157 | console.log((err as Error).message); 158 | } 159 | } 160 | } 161 | 162 | interface GetCompletionsRequestMessage { 163 | kind: 'getCompletions'; 164 | requestId: number; 165 | request: string; 166 | } 167 | 168 | interface AcceptCompletionRequestMessage { 169 | kind: 'acceptCompletion'; 170 | request: string; 171 | } 172 | 173 | export type LanguageServerWorkerRequest = 174 | | GetCompletionsRequestMessage 175 | | AcceptCompletionRequestMessage; 176 | 177 | export interface GetCompletionsResponseMessage { 178 | kind: 'getCompletions'; 179 | requestId: number; 180 | response?: string; 181 | } 182 | 183 | export type LanguageServerWorkerResponse = GetCompletionsResponseMessage; 184 | 185 | export class LanguageServerClient { 186 | private sessionId = uuidv4(); 187 | private port: chrome.runtime.Port; 188 | private requestId = 0; 189 | private promiseMap = new Map void>(); 190 | 191 | constructor(readonly extensionId: string) { 192 | this.port = this.createPort(); 193 | } 194 | 195 | createPort(): chrome.runtime.Port { 196 | const port = chrome.runtime.connect(this.extensionId, { name: this.sessionId }); 197 | port.onDisconnect.addListener(() => { 198 | this.port = this.createPort(); 199 | }); 200 | port.onMessage.addListener(async (message: LanguageServerWorkerResponse) => { 201 | if (message.kind === 'getCompletions') { 202 | let res: GetCompletionsResponse | undefined = undefined; 203 | if (message.response !== undefined) { 204 | res = GetCompletionsResponse.fromJsonString(message.response); 205 | } 206 | this.promiseMap.get(message.requestId)?.(res); 207 | this.promiseMap.delete(message.requestId); 208 | } 209 | }); 210 | return port; 211 | } 212 | 213 | getMetadata(ideInfo: IdeInfo): Metadata { 214 | return new Metadata({ 215 | ideName: ideInfo.ideName, 216 | ideVersion: ideInfo.ideVersion, 217 | extensionName: EXTENSION_NAME, 218 | extensionVersion: EXTENSION_VERSION, 219 | locale: navigator.language, 220 | sessionId: this.sessionId, 221 | requestId: BigInt(++this.requestId), 222 | userAgent: navigator.userAgent, 223 | url: window.location.href, 224 | }); 225 | } 226 | 227 | async getCompletions( 228 | request: GetCompletionsRequest 229 | ): Promise { 230 | const requestId = Number(request.metadata?.requestId); 231 | const prom = new Promise((resolve) => { 232 | this.promiseMap.set(requestId, resolve); 233 | }); 234 | const message: GetCompletionsRequestMessage = { 235 | kind: 'getCompletions', 236 | requestId, 237 | request: request.toJsonString(), 238 | }; 239 | this.port.postMessage(message); 240 | return prom; 241 | } 242 | 243 | acceptedLastCompletion(ideInfo: IdeInfo, completionId: string): void { 244 | const request = new AcceptCompletionRequest({ 245 | metadata: this.getMetadata(ideInfo), 246 | completionId, 247 | }); 248 | const message: AcceptCompletionRequestMessage = { 249 | kind: 'acceptCompletion', 250 | request: request.toJsonString(), 251 | }; 252 | this.port.postMessage(message); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/component/Options.tsx: -------------------------------------------------------------------------------- 1 | import LoginIcon from '@mui/icons-material/Login'; 2 | import OpenInNewIcon from '@mui/icons-material/OpenInNew'; 3 | import RestartAltIcon from '@mui/icons-material/RestartAlt'; 4 | import SaveAltIcon from '@mui/icons-material/SaveAlt'; 5 | import SettingsIcon from '@mui/icons-material/Settings'; 6 | import { Alert, Button, Link, Snackbar, TextField, Typography } from '@mui/material'; 7 | import Box from '@mui/material/Box'; 8 | import Divider from '@mui/material/Divider'; 9 | import React, { createRef, useEffect, useMemo, useState } from 'react'; 10 | import { v4 as uuidv4 } from 'uuid'; 11 | 12 | import { 13 | computeAllowlist, 14 | defaultAllowlist, 15 | getGeneralProfileUrl, 16 | getStorageItem, 17 | setStorageItem, 18 | } from '../storage'; 19 | import { PUBLIC_WEBSITE } from '../urls'; 20 | 21 | const EditableList = () => { 22 | const [text, setText] = useState(''); 23 | const [open, setOpen] = useState(false); 24 | const [severity, setSeverity] = useState<'success' | 'error'>('success'); 25 | const [message, setMessage] = useState(''); 26 | 27 | useEffect(() => { 28 | (async () => { 29 | const allowlist = computeAllowlist(await getStorageItem('allowlist')); 30 | setText(allowlist.join('\n')); 31 | })().catch((e) => { 32 | console.error(e); 33 | }); 34 | }, []); 35 | 36 | return ( 37 | <> 38 | Allowlist 39 | 40 | Domains to allow auto-completion. Use one regex per line. 41 | 42 | 43 | setText(e.target.value)} 56 | > 57 | 58 | 88 | 89 | 114 | 115 | setOpen(false)} 119 | anchorOrigin={{ vertical: 'top', horizontal: 'center' }} 120 | > 121 | setOpen(false)}> 122 | {message} 123 | 124 | 125 | 126 | ); 127 | }; 128 | 129 | const openTokenPage = async () => { 130 | const profileUrl = await getGeneralProfileUrl(); 131 | if (profileUrl === undefined) { 132 | return; 133 | } 134 | const params = new URLSearchParams({ 135 | response_type: 'token', 136 | redirect_uri: 'chrome-show-auth-token', 137 | scope: 'openid profile email', 138 | prompt: 'login', 139 | redirect_parameters_type: 'query', 140 | state: uuidv4(), 141 | }); 142 | await chrome.tabs.create({ url: `${profileUrl}?${params}` }); 143 | }; 144 | 145 | const Options = () => { 146 | const tokenRef = createRef(); 147 | const portalUrlRef = createRef(); 148 | const [portalUrlText, setPortalUrlText] = useState(''); 149 | const modelRef = createRef(); 150 | const [modelText, setModelText] = useState(''); 151 | const [jupyterlabKeybindingAcceptText, setJupyterlabKeybindingAcceptText] = useState(''); 152 | const [jupyterlabKeybindingDismissText, setJupyterlabKeybindingDismissText] = useState(''); 153 | const [jupyterNotebookKeybindingAcceptText, setJupyterNotebookKeybindingAcceptText] = 154 | useState(''); 155 | const [jupyterDebounceMs, setJupyterDebounceMs] = useState(0); 156 | const jupyterDebounceMsRef = createRef(); 157 | const [currentKey, setCurrentKey] = useState({ 158 | key: '', 159 | ctrl: false, 160 | alt: false, 161 | shift: false, 162 | meta: false, 163 | }); 164 | const [jupyterlabAcceptInput, setJupyterlabAcceptInput] = useState(false); 165 | const [jupyterlabDismissInput, setJupyterlabDismissInput] = useState(false); 166 | const [notebookAcceptInput, setNotebookAcceptInput] = useState(false); 167 | 168 | const formatKeyCombination = (key: any) => { 169 | const modifiers = []; 170 | if (key.ctrl) modifiers.push('Ctrl'); 171 | if (key.alt) modifiers.push('Alt'); 172 | if (key.shift) modifiers.push('Shift'); 173 | if (key.meta) modifiers.push('Meta'); 174 | return [...modifiers, key.key.toUpperCase()].join('+'); 175 | }; 176 | 177 | const handleKeyDown = (e: React.KeyboardEvent) => { 178 | e.preventDefault(); 179 | const key = e.key; 180 | if (key !== 'Control' && key !== 'Alt' && key !== 'Shift' && key !== 'Meta') { 181 | const ctrl = e.ctrlKey; 182 | const alt = e.altKey; 183 | const shift = e.shiftKey; 184 | const meta = e.metaKey; 185 | setCurrentKey({ key, ctrl, alt, shift, meta }); 186 | 187 | // Force blur using setTimeout to ensure it happens after state update 188 | setTimeout(() => { 189 | if (e.currentTarget) { 190 | e.currentTarget.blur(); 191 | // Also try to remove focus from the document 192 | (document.activeElement as HTMLElement)?.blur(); 193 | } 194 | }, 0); 195 | } 196 | }; 197 | 198 | useEffect(() => { 199 | (async () => { 200 | setPortalUrlText((await getStorageItem('portalUrl')) ?? ''); 201 | })().catch((e) => { 202 | console.error(e); 203 | }); 204 | (async () => { 205 | setModelText((await getStorageItem('enterpriseDefaultModel')) ?? ''); 206 | })().catch((e) => { 207 | console.error(e); 208 | }); 209 | (async () => { 210 | setJupyterlabKeybindingAcceptText( 211 | (await getStorageItem('jupyterlabKeybindingAccept')) ?? 'Tab' 212 | ); 213 | })().catch((e) => { 214 | console.error(e); 215 | }); 216 | (async () => { 217 | setJupyterlabKeybindingDismissText( 218 | (await getStorageItem('jupyterlabKeybindingDismiss')) ?? 'Escape' 219 | ); 220 | })().catch((e) => { 221 | console.error(e); 222 | }); 223 | (async () => { 224 | setJupyterNotebookKeybindingAcceptText( 225 | (await getStorageItem('jupyterNotebookKeybindingAccept')) ?? 'Tab' 226 | ); 227 | })().catch((e) => { 228 | console.error(e); 229 | }); 230 | (async () => { 231 | setJupyterDebounceMs((await getStorageItem('jupyterDebounceMs')) ?? 0); 232 | })().catch((e) => { 233 | console.error(e); 234 | }); 235 | }, []); 236 | // TODO(prem): Deduplicate with serviceWorker.ts/storage.ts. 237 | const resolvedPortalUrl = useMemo(() => { 238 | if (portalUrlText !== '' || CODEIUM_ENTERPRISE) { 239 | return portalUrlText; 240 | } 241 | return PUBLIC_WEBSITE; 242 | }, []); 243 | 244 | return ( 245 | 246 | {!CODEIUM_ENTERPRISE && ( 247 | <> 248 | 249 | {' '} 258 | Edit telemetry settings at the{' '} 259 | 260 | Codeium website 261 | 267 | 268 | 269 | 274 | 275 | )} 276 | 277 | Alternative ways to log in 278 | 286 | 287 | 290 | 291 | 302 | 303 | 304 | 309 | 310 | Enterprise settings 311 | setPortalUrlText(e.target.value)} 320 | /> 321 | 322 | 332 | 333 | setModelText(e.target.value)} 341 | /> 342 | 343 | 353 | 354 | 355 | 360 | 361 | 362 | 363 | 368 | 369 | Jupyter Settings 370 | 371 | Press the desired key combination in the input field. For example, press "Ctrl+Tab" for a 372 | Ctrl+Tab shortcut. 373 | 374 | 375 | 376 | JupyterLab 377 | 378 | setJupyterlabAcceptInput(true)} 385 | onBlur={async () => { 386 | setJupyterlabAcceptInput(false); 387 | if (currentKey.key) { 388 | const formatted = formatKeyCombination(currentKey); 389 | setJupyterlabKeybindingAcceptText(formatted); 390 | await setStorageItem('jupyterlabKeybindingAccept', formatted); 391 | setCurrentKey({ key: '', ctrl: false, alt: false, shift: false, meta: false }); 392 | } 393 | }} 394 | onKeyDown={handleKeyDown} 395 | /> 396 | setJupyterlabDismissInput(true)} 405 | onBlur={async () => { 406 | setJupyterlabDismissInput(false); 407 | if (currentKey.key) { 408 | const formatted = formatKeyCombination(currentKey); 409 | setJupyterlabKeybindingDismissText(formatted); 410 | await setStorageItem('jupyterlabKeybindingDismiss', formatted); 411 | setCurrentKey({ key: '', ctrl: false, alt: false, shift: false, meta: false }); 412 | } 413 | }} 414 | onKeyDown={handleKeyDown} 415 | /> 416 | 417 | 418 | Jupyter Notebook 419 | 420 | setNotebookAcceptInput(true)} 429 | onBlur={async () => { 430 | setNotebookAcceptInput(false); 431 | if (currentKey.key) { 432 | const formatted = formatKeyCombination(currentKey); 433 | setJupyterNotebookKeybindingAcceptText(formatted); 434 | await setStorageItem('jupyterNotebookKeybindingAccept', formatted); 435 | setCurrentKey({ key: '', ctrl: false, alt: false, shift: false, meta: false }); 436 | } 437 | }} 438 | onKeyDown={handleKeyDown} 439 | /> 440 | 441 | 442 | Performance 443 | 444 | setJupyterDebounceMs(Number(e.target.value))} 453 | /> 454 | 455 | 465 | 466 | 467 | 468 | ); 469 | }; 470 | 471 | export default Options; 472 | -------------------------------------------------------------------------------- /src/contentScript.ts: -------------------------------------------------------------------------------- 1 | if (document.contentType === 'text/html') { 2 | const s = document.createElement('script'); 3 | s.src = chrome.runtime.getURL('script.js?') + new URLSearchParams({ id: chrome.runtime.id }); 4 | s.onload = function () { 5 | (this as HTMLScriptElement).remove(); 6 | }; 7 | (document.head || document.documentElement).prepend(s); 8 | } 9 | -------------------------------------------------------------------------------- /src/enterprise.d.ts: -------------------------------------------------------------------------------- 1 | // Defined in webpack.common.js. 2 | declare const CODEIUM_ENTERPRISE: boolean; 3 | -------------------------------------------------------------------------------- /src/jupyterInject.ts: -------------------------------------------------------------------------------- 1 | import type CodeMirror from 'codemirror'; 2 | 3 | import { CodeMirrorManager } from './codemirror'; 4 | import { DEFAULT_PATH, JupyterNotebookKeyBindings } from './common'; 5 | import { EditorOptions } from '../proto/exa/codeium_common_pb/codeium_common_pb'; 6 | 7 | // Note: this file only deals with jupyter notebook 6 (not Jupyter Lab) 8 | 9 | declare class Cell { 10 | code_mirror: CodeMirror.Editor; 11 | notebook: Notebook; 12 | get_text(): string; 13 | cell_type: 'raw' | 'markdown' | 'code'; 14 | cell_id: string; 15 | handle_codemirror_keyevent(this: Cell, editor: CodeMirror.Editor, event: KeyboardEvent): void; 16 | output_area?: { 17 | outputs: { 18 | // Currently, we only look at execute_result 19 | output_type: 'execute_result' | 'error' | 'stream' | 'display_data'; 20 | name?: string; 21 | data?: { 22 | 'text/plain': string; 23 | }; 24 | text?: string; 25 | }[]; 26 | }; 27 | } 28 | 29 | declare class CodeCell extends Cell { 30 | cell_type: 'code'; 31 | } 32 | 33 | declare class TextCell extends Cell { 34 | cell_type: 'markdown'; 35 | } 36 | 37 | interface Notebook { 38 | get_cells(): Cell[]; 39 | } 40 | 41 | declare class ShortcutManager { 42 | call_handler(this: ShortcutManager, event: KeyboardEvent): void; 43 | } 44 | 45 | interface Jupyter { 46 | CodeCell: typeof CodeCell; 47 | TextCell: typeof TextCell; 48 | version: string; 49 | keyboard: { 50 | ShortcutManager: typeof ShortcutManager; 51 | }; 52 | } 53 | 54 | class JupyterState { 55 | jupyter: Jupyter; 56 | codeMirrorManager: CodeMirrorManager; 57 | keybindings: JupyterNotebookKeyBindings; 58 | 59 | constructor(extensionId: string, jupyter: Jupyter, keybindings: JupyterNotebookKeyBindings) { 60 | this.jupyter = jupyter; 61 | this.codeMirrorManager = new CodeMirrorManager(extensionId, { 62 | ideName: 'jupyter_notebook', 63 | ideVersion: jupyter.version, 64 | }); 65 | this.keybindings = keybindings; 66 | } 67 | 68 | patchCellKeyEvent(debounceMs?: number) { 69 | const beforeMainHandler = (doc: CodeMirror.Doc, event: KeyboardEvent) => { 70 | return this.codeMirrorManager.beforeMainKeyHandler( 71 | doc, 72 | event, 73 | { tab: true, escape: false }, 74 | this.keybindings.accept 75 | ); 76 | }; 77 | const replaceOriginalHandler = ( 78 | handler: (this: Cell, editor: CodeMirror.Editor, event: KeyboardEvent) => void 79 | ) => { 80 | const codeMirrorManager = this.codeMirrorManager; 81 | return function (this: Cell, editor: CodeMirror.Editor, event: KeyboardEvent) { 82 | const { consumeEvent, forceTriggerCompletion } = beforeMainHandler(editor.getDoc(), event); 83 | if (consumeEvent !== undefined) { 84 | if (consumeEvent) { 85 | event.preventDefault(); 86 | } else { 87 | handler.call(this, editor, event); 88 | } 89 | return; 90 | } 91 | const doc = editor.getDoc(); 92 | const oldString = doc.getValue(); 93 | setTimeout(async () => { 94 | if (!forceTriggerCompletion) { 95 | const newString = doc.getValue(); 96 | if (newString === oldString) { 97 | // Cases like arrow keys, page up/down, etc. should fall here. 98 | return; 99 | } 100 | } 101 | const textModels: CodeMirror.Doc[] = []; 102 | 103 | const editableCells = [...this.notebook.get_cells()]; 104 | let currentModelWithOutput; 105 | for (const cell of editableCells) { 106 | let outputText = ''; 107 | if (cell.output_area !== undefined && cell.output_area.outputs.length > 0) { 108 | const output = cell.output_area.outputs[0]; 109 | if ( 110 | output.output_type === 'execute_result' && 111 | output.data !== undefined && 112 | output.data['text/plain'] !== undefined 113 | ) { 114 | outputText = output.data['text/plain']; 115 | } else if ( 116 | output.output_type === 'stream' && 117 | output.name === 'stdout' && 118 | output.text !== undefined 119 | ) { 120 | outputText = output.text; 121 | } 122 | 123 | const lines = outputText.split('\n'); 124 | if (lines.length > 10) { 125 | lines.length = 10; 126 | outputText = lines.join('\n'); 127 | } 128 | if (outputText.length > 500) { 129 | outputText = outputText.slice(0, 500); 130 | } 131 | } 132 | outputText = outputText ? '\nOUTPUT:\n' + outputText : ''; 133 | if (cell.code_mirror.getDoc() === doc) { 134 | textModels.push(doc); 135 | currentModelWithOutput = cell.code_mirror.getDoc().copy(false); 136 | currentModelWithOutput.setValue(cell.get_text() + outputText); 137 | } else { 138 | const docCopy = cell.code_mirror.getDoc().copy(false); 139 | let docText = docCopy.getValue(); 140 | docText += outputText; 141 | docCopy.setValue(docText); 142 | textModels.push(docCopy); 143 | } 144 | } 145 | 146 | const url = window.location.href; 147 | // URLs are usually of the form, http://localhost:XXXX/notebooks/path/to/notebook.ipynb 148 | // We only want path/to/notebook.ipynb 149 | // If the URL is not of this form, we just use "unknown_url" 150 | const path = new URL(url).pathname; 151 | const relativePath = path.endsWith('.ipynb') ? path.replace(/^\//, '') : undefined; 152 | 153 | await codeMirrorManager.triggerCompletion( 154 | true, // isNotebook 155 | textModels, 156 | this.code_mirror.getDoc(), 157 | currentModelWithOutput, 158 | new EditorOptions({ 159 | tabSize: BigInt(editor.getOption('tabSize') ?? 4), 160 | insertSpaces: !(editor.getOption('indentWithTabs') ?? false), 161 | }), 162 | relativePath ?? DEFAULT_PATH, 163 | undefined // create_disposables 164 | ); 165 | }, debounceMs ?? 0); 166 | }; 167 | }; 168 | this.jupyter.CodeCell.prototype.handle_codemirror_keyevent = replaceOriginalHandler( 169 | this.jupyter.CodeCell.prototype.handle_codemirror_keyevent 170 | ); 171 | this.jupyter.TextCell.prototype.handle_codemirror_keyevent = replaceOriginalHandler( 172 | this.jupyter.TextCell.prototype.handle_codemirror_keyevent 173 | ); 174 | } 175 | 176 | patchShortcutManagerHandler() { 177 | const origHandler = this.jupyter.keyboard.ShortcutManager.prototype.call_handler; 178 | const clearCompletion = () => this.codeMirrorManager.clearCompletion('shortcut manager'); 179 | this.jupyter.keyboard.ShortcutManager.prototype.call_handler = function ( 180 | this: ShortcutManager, 181 | event: KeyboardEvent 182 | ) { 183 | if (event.key === 'Escape' && clearCompletion()) { 184 | event.preventDefault(); 185 | } else { 186 | origHandler.call(this, event); 187 | } 188 | }; 189 | } 190 | } 191 | 192 | export function inject( 193 | extensionId: string, 194 | jupyter: Jupyter, 195 | keybindings: JupyterNotebookKeyBindings, 196 | debounceMs?: number 197 | ): JupyterState { 198 | const jupyterState = new JupyterState(extensionId, jupyter, keybindings); 199 | jupyterState.patchCellKeyEvent(debounceMs); 200 | jupyterState.patchShortcutManagerHandler(); 201 | return jupyterState; 202 | } 203 | -------------------------------------------------------------------------------- /src/jupyterlabPlugin.ts: -------------------------------------------------------------------------------- 1 | import type { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; 2 | import { type CodeEditor } from '@jupyterlab/codeeditor'; 3 | import { type CodeMirrorEditor } from '@jupyterlab/codemirror'; 4 | import { type IDocumentManager } from '@jupyterlab/docmanager'; 5 | import { type IEditorTracker } from '@jupyterlab/fileeditor'; 6 | import { type INotebookTracker } from '@jupyterlab/notebook'; 7 | import { type IDisposable } from '@lumino/disposable'; 8 | import { type Widget } from '@lumino/widgets'; 9 | import type CodeMirror from 'codemirror'; 10 | 11 | import { CodeMirrorManager } from './codemirror'; 12 | import type { JupyterLabKeyBindings, KeyCombination } from './common'; 13 | import { EditorOptions } from '../proto/exa/codeium_common_pb/codeium_common_pb'; 14 | 15 | function formatJupyterLabKeyCombination(keyCombination: KeyCombination): string { 16 | const parts: string[] = []; 17 | if (keyCombination.ctrl) parts.push('Ctrl'); 18 | if (keyCombination.alt) parts.push('Alt'); 19 | if (keyCombination.shift) parts.push('Shift'); 20 | if (keyCombination.meta) parts.push('Meta'); 21 | parts.push(keyCombination.key); 22 | return parts.join(' '); 23 | } 24 | 25 | const COMMAND_ACCEPT = 'codeium:accept-completion'; 26 | const COMMAND_DISMISS = 'codeium:dismiss-completion'; 27 | 28 | declare class CellJSON { 29 | cell_type: 'raw' | 'markdown' | 'code'; 30 | source: string; 31 | outputs: { 32 | // Currently, we only look at execute_result 33 | output_type: 'execute_result' | 'error' | 'stream' | 'display_data'; 34 | name?: string; 35 | data?: { 36 | 'text/html': string; 37 | 'text/plain': string; 38 | }; 39 | text?: string; 40 | }[]; 41 | } 42 | 43 | async function getKeybindings(extensionId: string): Promise { 44 | const allowed = await new Promise((resolve) => { 45 | chrome.runtime.sendMessage( 46 | extensionId, 47 | { type: 'jupyterlab' }, 48 | (response: JupyterLabKeyBindings) => { 49 | resolve(response); 50 | } 51 | ); 52 | }); 53 | return allowed; 54 | } 55 | 56 | class CodeiumPlugin { 57 | app: JupyterFrontEnd; 58 | notebookTracker: INotebookTracker; 59 | editorTracker: IEditorTracker; 60 | documentManager: IDocumentManager; 61 | 62 | previousCellHandler?: IDisposable; 63 | nonNotebookWidget = new Set(); 64 | 65 | codeMirrorManager: CodeMirrorManager; 66 | keybindings: Promise; 67 | 68 | debounceMs: number; 69 | 70 | constructor( 71 | readonly extensionId: string, 72 | app: JupyterFrontEnd, 73 | notebookTracker: INotebookTracker, 74 | editorTracker: IEditorTracker, 75 | documentManager: IDocumentManager, 76 | debounceMs: number 77 | ) { 78 | this.app = app; 79 | this.notebookTracker = notebookTracker; 80 | this.editorTracker = editorTracker; 81 | this.documentManager = documentManager; 82 | this.debounceMs = debounceMs; 83 | this.codeMirrorManager = new CodeMirrorManager(extensionId, { 84 | ideName: 'jupyterlab', 85 | ideVersion: `${app.name.toLowerCase()} ${app.version}`, 86 | }); 87 | // The keyboard shortcuts for these commands are added and removed depending 88 | // on the presence of ghost text, since they cannot defer to a shortcut on a 89 | // parent element. 90 | app.commands.addCommand(COMMAND_ACCEPT, { 91 | execute: () => { 92 | this.codeMirrorManager.acceptCompletion(); 93 | }, 94 | }); 95 | app.commands.addCommand(COMMAND_DISMISS, { 96 | execute: () => { 97 | this.codeMirrorManager.clearCompletion('user dismissed'); 98 | }, 99 | }); 100 | const clearCompletionInitHook = this.codeMirrorManager.clearCompletionInitHook(); 101 | const keyboardHandler = this.keydownHandler.bind(this); 102 | // There is no cellAdded listener, so resort to maintaining a single 103 | // listener for all cells. 104 | notebookTracker.activeCellChanged.connect((_notebookTracker, cell) => { 105 | this.previousCellHandler?.dispose(); 106 | this.previousCellHandler = undefined; 107 | if (cell === null) { 108 | return; 109 | } 110 | clearCompletionInitHook((cell.editor as CodeMirrorEditor).editor ?? null); 111 | this.previousCellHandler = cell.editor.addKeydownHandler(keyboardHandler); 112 | }, this); 113 | editorTracker.widgetAdded.connect((_editorTracker, widget) => { 114 | clearCompletionInitHook((widget.content.editor as CodeMirrorEditor).editor); 115 | widget.content.editor.addKeydownHandler(keyboardHandler); 116 | this.nonNotebookWidget.add(widget.id); 117 | widget.disposed.connect(this.removeNonNotebookWidget, this); 118 | }, this); 119 | this.keybindings = getKeybindings(extensionId); 120 | } 121 | 122 | removeNonNotebookWidget(w: Widget) { 123 | this.nonNotebookWidget.delete(w.id); 124 | } 125 | 126 | keydownHandler(editor: CodeEditor.IEditor, event: KeyboardEvent): boolean { 127 | // To support the Ctrl+Space shortcut. 128 | // TODO(prem): Make this a command. 129 | const codeMirrorEditor = editor as CodeMirrorEditor; 130 | const { consumeEvent, forceTriggerCompletion } = this.codeMirrorManager.beforeMainKeyHandler( 131 | codeMirrorEditor.doc, 132 | event, 133 | { tab: false, escape: false } 134 | ); 135 | if (consumeEvent !== undefined) { 136 | return consumeEvent; 137 | } 138 | const oldString = codeMirrorEditor.doc.getValue(); 139 | // We need to run the rest of the code after the normal DOM handler. 140 | // TODO(prem): Does this need debouncing? 141 | setTimeout(async () => { 142 | const keybindings = await this.keybindings; 143 | if (!forceTriggerCompletion) { 144 | const newString = codeMirrorEditor.doc.getValue(); 145 | if (newString === oldString) { 146 | // Cases like arrow keys, page up/down, etc. should fall here. 147 | return; 148 | } 149 | } 150 | const textModels: CodeMirror.Doc[] = []; 151 | const isNotebook = codeMirrorEditor === this.notebookTracker.activeCell?.editor; 152 | const widget = isNotebook 153 | ? this.notebookTracker.currentWidget 154 | : this.editorTracker.currentWidget; 155 | let currentTextModelWithOutput = undefined; 156 | if (isNotebook) { 157 | const cells = this.notebookTracker.currentWidget?.content.widgets; 158 | if (cells !== undefined) { 159 | for (const cell of cells) { 160 | const doc = (cell.editor as CodeMirrorEditor).doc; 161 | const cellJSON = cell.model.toJSON() as CellJSON; 162 | if (cellJSON.outputs !== undefined && cellJSON.outputs.length > 0) { 163 | const isCurrentCell = cell === this.notebookTracker.currentWidget?.content.activeCell; 164 | const cellText = cellJSON.source; 165 | let outputText = ''; 166 | 167 | for (const output of cellJSON.outputs) { 168 | if (output.output_type === 'execute_result' && output.data !== undefined) { 169 | const data = output.data; 170 | if (data['text/plain'] !== undefined) { 171 | outputText = output.data['text/plain']; 172 | } else if (data['text/html'] !== undefined) { 173 | outputText = output.data['text/html']; 174 | } 175 | } 176 | if ( 177 | output.output_type === 'stream' && 178 | output.name === 'stdout' && 179 | output.text !== undefined 180 | ) { 181 | outputText = output.text; 182 | } 183 | } 184 | 185 | // Limit output text to 10 lines and 500 characters 186 | // Add the OUTPUT: prefix if it exists 187 | outputText = outputText 188 | .split('\n') 189 | .slice(0, 10) 190 | .map((line) => line.slice(0, 500)) 191 | .join('\n'); 192 | outputText = outputText ? '\nOUTPUT:\n' + outputText : ''; 193 | 194 | const docCopy = doc.copy(false); 195 | docCopy.setValue(cellText + outputText); 196 | 197 | if (isCurrentCell) { 198 | currentTextModelWithOutput = docCopy; 199 | textModels.push(doc); 200 | } else { 201 | textModels.push(docCopy); 202 | } 203 | } else { 204 | textModels.push(doc); 205 | } 206 | } 207 | } 208 | } 209 | const context = widget !== null ? this.documentManager.contextForWidget(widget) : undefined; 210 | const currentTextModel = codeMirrorEditor.doc; 211 | await this.codeMirrorManager.triggerCompletion( 212 | true, // isNotebook 213 | textModels, 214 | currentTextModel, 215 | currentTextModelWithOutput, 216 | new EditorOptions({ 217 | tabSize: BigInt(codeMirrorEditor.getOption('tabSize')), 218 | insertSpaces: codeMirrorEditor.getOption('insertSpaces'), 219 | }), 220 | context?.localPath, 221 | () => { 222 | const keybindingDisposables = [ 223 | this.app.commands.addKeyBinding({ 224 | command: COMMAND_ACCEPT, 225 | keys: [formatJupyterLabKeyCombination(keybindings.accept)], 226 | selector: '.CodeMirror', 227 | }), 228 | ]; 229 | if (!this.app.hasPlugin('@axlair/jupyterlab_vim')) { 230 | keybindingDisposables.push( 231 | this.app.commands.addKeyBinding({ 232 | command: COMMAND_DISMISS, 233 | keys: [formatJupyterLabKeyCombination(keybindings.dismiss)], 234 | selector: '.CodeMirror', 235 | }) 236 | ); 237 | } 238 | return keybindingDisposables; 239 | } 240 | ); 241 | }, this.debounceMs); 242 | void chrome.runtime.sendMessage(this.extensionId, { type: 'success' }); 243 | return false; 244 | } 245 | } 246 | 247 | export function getPlugin( 248 | extensionId: string, 249 | jupyterapp: JupyterFrontEnd, 250 | debounceMs: number 251 | ): JupyterFrontEndPlugin { 252 | return { 253 | id: 'codeium:plugin', 254 | autoStart: true, 255 | activate: ( 256 | app: JupyterFrontEnd, 257 | notebookTracker: INotebookTracker, 258 | editorTracker: IEditorTracker, 259 | documentManager: IDocumentManager 260 | ) => { 261 | // This indirection is necessary to get us a `this` to store state in. 262 | new CodeiumPlugin( 263 | extensionId, 264 | app, 265 | notebookTracker, 266 | editorTracker, 267 | documentManager, 268 | debounceMs 269 | ); 270 | }, 271 | requires: [ 272 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 273 | // @ts-expect-error 274 | jupyterapp._pluginMap['@jupyterlab/notebook-extension:tracker'].provides, 275 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 276 | // @ts-expect-error 277 | jupyterapp._pluginMap['@jupyterlab/fileeditor-extension:plugin'].provides, 278 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 279 | // @ts-expect-error 280 | jupyterapp._pluginMap['@jupyterlab/docmanager-extension:plugin'].provides, 281 | ], 282 | }; 283 | } 284 | -------------------------------------------------------------------------------- /src/monacoCompletionProvider.ts: -------------------------------------------------------------------------------- 1 | import type * as monaco from 'monaco-editor'; 2 | 3 | import { IdeInfo, LanguageServerClient } from './common'; 4 | import { getLanguage } from './monacoLanguages'; 5 | import { TextAndOffsets, computeTextAndOffsets } from './notebook'; 6 | import { numUtf8BytesToNumCodeUnits } from './utf'; 7 | import { sleep } from './utils'; 8 | import { Language } from '../proto/exa/codeium_common_pb/codeium_common_pb'; 9 | import { 10 | CompletionItem, 11 | GetCompletionsRequest, 12 | } from '../proto/exa/language_server_pb/language_server_pb'; 13 | 14 | interface DatabricksModel { 15 | attributes: { 16 | type: 'commmand'; 17 | // The text of the cell. 18 | command: string; 19 | position: number; 20 | }; 21 | } 22 | 23 | declare global { 24 | interface Window { 25 | colab?: { 26 | global: { 27 | notebookModel: { 28 | fileId: { 29 | fileId: string; 30 | source: string; 31 | }; 32 | singleDocument: { models: readonly monaco.editor.ITextModel[] }; 33 | cells: { 34 | outputs: { 35 | currentOutput?: { 36 | outputItems: { 37 | data?: { 38 | // There may be other fields as well 39 | 'text/plain'?: string[]; 40 | 'text/html'?: string[]; 41 | }; 42 | output_type: string; 43 | }[]; 44 | }; 45 | }; 46 | type: 'code' | 'text'; 47 | textModel: monaco.editor.ITextModel; 48 | }[]; 49 | }; 50 | }; 51 | }; 52 | colabVersionTag?: string; 53 | // Databricks 54 | notebook?: { 55 | commandCollection(): { 56 | models: readonly (any | DatabricksModel)[]; 57 | }; 58 | }; 59 | } 60 | } 61 | 62 | declare module 'monaco-editor' { 63 | // eslint-disable-next-line @typescript-eslint/no-namespace 64 | namespace languages { 65 | interface InlineCompletion { 66 | text?: string; 67 | } 68 | } 69 | // eslint-disable-next-line @typescript-eslint/no-namespace 70 | namespace editor { 71 | interface ICodeEditor { 72 | _commandService: { executeCommand(command: string): unknown }; 73 | } 74 | interface ITextModel { 75 | // Seems to exist in certain versions of monaco. 76 | getLanguageIdentifier?: () => { language: string; id: number }; 77 | } 78 | } 79 | } 80 | 81 | export const OMonacoSite = { 82 | UNSPECIFIED: 0, 83 | COLAB: 1, 84 | STACKBLITZ: 2, 85 | DEEPNOTE: 3, 86 | DATABRICKS: 4, 87 | QUADRATIC: 5, 88 | CUSTOM: 6, 89 | } as const; 90 | export type MonacoSite = (typeof OMonacoSite)[keyof typeof OMonacoSite]; 91 | 92 | function getEditorLanguage(model: monaco.editor.ITextModel) { 93 | if (model.getLanguageIdentifier !== undefined) { 94 | return model.getLanguageIdentifier().language; 95 | } 96 | return model.getLanguageId(); 97 | } 98 | 99 | class MonacoRange { 100 | startLineNumber: number; 101 | startColumn: number; 102 | endLineNumber: number; 103 | endColumn: number; 104 | 105 | constructor(start: monaco.IPosition, end: monaco.IPosition) { 106 | this.startLineNumber = start.lineNumber; 107 | this.startColumn = start.column; 108 | this.endLineNumber = end.lineNumber; 109 | this.endColumn = end.column; 110 | } 111 | } 112 | 113 | // Some environments like Databricks will include extraneous text, such as %sql 114 | // as the first line. We need to trim this out. 115 | function getValueAndStartOffset( 116 | monacoSite: MonacoSite, 117 | model: monaco.editor.ITextModel | string 118 | ): { value: string; utf16Offset: number } { 119 | const originalValue = typeof model === 'string' ? model : model.getValue(); 120 | if (monacoSite !== OMonacoSite.DATABRICKS || !originalValue.startsWith('%')) { 121 | return { value: originalValue, utf16Offset: 0 }; 122 | } 123 | const indexofNewline = originalValue.indexOf('\n'); 124 | const indexOfSecondLine = indexofNewline === -1 ? originalValue.length : indexofNewline + 1; 125 | // TODO(prem): This is going to let completions start at the end of %python lines, etc. 126 | // https://github.com/Exafunction/Exafunction/pull/3652#discussion_r1102165558 127 | return { value: originalValue.substring(indexOfSecondLine), utf16Offset: indexOfSecondLine }; 128 | } 129 | 130 | function createInlineCompletionItem( 131 | monacoSite: MonacoSite, 132 | completionItem: CompletionItem, 133 | document: monaco.editor.ITextModel, 134 | additionalUtf8ByteOffset: number, 135 | editor?: monaco.editor.ICodeEditor 136 | ): monaco.languages.InlineCompletion | undefined { 137 | if (!completionItem.completion || !completionItem.range) { 138 | return undefined; 139 | } 140 | 141 | // Create and return inlineCompletionItem. 142 | const { value: text, utf16Offset } = getValueAndStartOffset(monacoSite, document); 143 | const startPosition = document.getPositionAt( 144 | utf16Offset + 145 | numUtf8BytesToNumCodeUnits( 146 | text, 147 | Number(completionItem.range.startOffset) - additionalUtf8ByteOffset 148 | ) 149 | ); 150 | const endPosition = document.getPositionAt( 151 | utf16Offset + 152 | numUtf8BytesToNumCodeUnits( 153 | text, 154 | Number(completionItem.range.endOffset) - additionalUtf8ByteOffset 155 | ) 156 | ); 157 | const range = new MonacoRange(startPosition, endPosition); 158 | let completionText = completionItem.completion.text; 159 | let callback: (() => void) | undefined = undefined; 160 | if (editor && completionItem.suffix && completionItem.suffix.text.length > 0) { 161 | // Add suffix to the completion text. 162 | completionText += completionItem.suffix.text; 163 | // Create callback to move cursor after accept. 164 | // Note that this is a hack to get around Monaco's API limitations. 165 | // There's no need to convert to code units since we only use simple characters. 166 | const deltaCursorOffset = Number(completionItem.suffix.deltaCursorOffset); 167 | callback = () => { 168 | const selection = editor.getSelection(); 169 | if (selection === null) { 170 | console.warn('Unexpected, no selection'); 171 | return; 172 | } 173 | const newPosition = document.getPositionAt( 174 | document.getOffsetAt(selection.getPosition()) + deltaCursorOffset 175 | ); 176 | editor.setSelection(new MonacoRange(newPosition, newPosition)); 177 | editor._commandService.executeCommand('editor.action.inlineSuggest.trigger'); 178 | }; 179 | } 180 | 181 | const inlineCompletionItem: monaco.languages.InlineCompletion = { 182 | insertText: completionText, 183 | text: completionText, 184 | range, 185 | command: { 186 | id: 'codeium.acceptCompletion', 187 | title: 'Accept Completion', 188 | arguments: [completionItem.completion.completionId, callback], 189 | }, 190 | }; 191 | return inlineCompletionItem; 192 | } 193 | 194 | // We need to create a path that includes `.ipynb` as the suffix to trigger special logic in the language server. 195 | function colabRelativePath(): string | undefined { 196 | const fileId = window.colab?.global.notebookModel.fileId; 197 | if (fileId === undefined) { 198 | return undefined; 199 | } 200 | if (fileId.source === 'drive') { 201 | let fileIdString = fileId.fileId; 202 | fileIdString = fileIdString.replace(/^\//, ''); 203 | return `${fileIdString}.ipynb`; 204 | } 205 | return fileId.fileId.replace(/^\//, ''); 206 | } 207 | 208 | function deepnoteAndDatabricksRelativePath(url: string): string | undefined { 209 | // Deepnote URLs look like 210 | // https://deepnote.com/workspace/{workspaceId}/path/to/notebook/{notebookName}. 211 | 212 | // Databricks URLs look like 213 | // https://{region}.cloud.databricks.com/?o={xxxx}#notebook/{yyyy}/command/{zzzz} 214 | // where xxxx, yyyy, zzzz are 16 digit numbers. 215 | const notebookName = url.split('/').pop(); 216 | if (notebookName === undefined) { 217 | return undefined; 218 | } 219 | return `${notebookName}.ipynb`; 220 | } 221 | 222 | export class MonacoCompletionProvider implements monaco.languages.InlineCompletionsProvider { 223 | modelUriToEditor = new Map(); 224 | client: LanguageServerClient; 225 | debounceMs: number; 226 | 227 | constructor(readonly extensionId: string, readonly monacoSite: MonacoSite, debounceMs: number) { 228 | this.client = new LanguageServerClient(extensionId); 229 | this.debounceMs = debounceMs; 230 | } 231 | 232 | getIdeInfo(): IdeInfo { 233 | if (window.colab !== undefined) { 234 | return { 235 | ideName: 'colab', 236 | ideVersion: window.colabVersionTag ?? 'unknown', 237 | }; 238 | } 239 | return { 240 | ideName: 'monaco', 241 | ideVersion: `unknown-${window.location.hostname}`, 242 | }; 243 | } 244 | 245 | textModels(model: monaco.editor.ITextModel): monaco.editor.ITextModel[] { 246 | if (this.monacoSite === OMonacoSite.COLAB) { 247 | return [...(window.colab?.global.notebookModel.singleDocument.models ?? [])]; 248 | } 249 | if (this.monacoSite === OMonacoSite.DEEPNOTE) { 250 | const mainNotebookId = model.uri.toString().split(':')[0]; 251 | const relevantEditors: monaco.editor.ICodeEditor[] = []; 252 | for (const [uri, editor] of this.modelUriToEditor) { 253 | const notebookId = uri.toString().split(':')[0]; 254 | if (notebookId !== mainNotebookId) { 255 | continue; 256 | } 257 | relevantEditors.push(editor); 258 | } 259 | relevantEditors.sort( 260 | (a, b) => 261 | (a.getDomNode()?.getBoundingClientRect().top ?? 0) - 262 | (b.getDomNode()?.getBoundingClientRect().top ?? 0) 263 | ); 264 | return relevantEditors 265 | .map((editor) => editor.getModel()) 266 | .filter((item): item is monaco.editor.ITextModel => item !== null); 267 | } 268 | return []; 269 | } 270 | 271 | private relativePath(): string | undefined { 272 | if (this.monacoSite === OMonacoSite.COLAB) { 273 | return colabRelativePath(); 274 | } 275 | const url = window.location.href; 276 | if (this.monacoSite === OMonacoSite.DEEPNOTE || this.monacoSite === OMonacoSite.DATABRICKS) { 277 | return deepnoteAndDatabricksRelativePath(url); 278 | } 279 | // Perhaps add something for (Stackblitz or Quadratic)? 280 | } 281 | 282 | private isNotebook() { 283 | return ( 284 | OMonacoSite.COLAB === this.monacoSite || 285 | OMonacoSite.DATABRICKS === this.monacoSite || 286 | OMonacoSite.DEEPNOTE === this.monacoSite 287 | ); 288 | } 289 | 290 | private absolutePath(model: monaco.editor.ITextModel): string | undefined { 291 | // Given we are using path, note the docs on fsPath: https://microsoft.github.io/monaco-editor/api/classes/monaco.Uri.html#fsPath 292 | if (this.monacoSite === OMonacoSite.COLAB) { 293 | // The colab absolute path is something like the cell number (i.e. /3) 294 | return colabRelativePath(); 295 | } 296 | return model.uri.path.replace(/^\//, ''); 297 | // TODO(prem): Adopt some site-specific convention. 298 | } 299 | 300 | private computeTextAndOffsets( 301 | model: monaco.editor.ITextModel, 302 | position: monaco.Position 303 | ): TextAndOffsets { 304 | if (this.monacoSite === OMonacoSite.DATABRICKS) { 305 | // Because not all cells have models, we need to run computeTextAndOffsets on raw text. 306 | const rawTextModels = (window.notebook?.commandCollection().models ?? []).filter( 307 | (m) => m.attributes.type === 'command' 308 | ) as readonly DatabricksModel[]; 309 | if (rawTextModels.length !== 0) { 310 | const textToModelMap = new Map(); 311 | for (const editor of this.modelUriToEditor.values()) { 312 | const model = editor.getModel(); 313 | if (model === null) { 314 | continue; 315 | } 316 | const value = getValueAndStartOffset(this.monacoSite, model).value; 317 | textToModelMap.set(value, model); 318 | } 319 | const editableRawTextModels = [...rawTextModels]; 320 | editableRawTextModels.sort((a, b) => a.attributes.position - b.attributes.position); 321 | const rawTexts = editableRawTextModels.map((m) => m.attributes.command); 322 | // The raw texts haven't been updated with the most recent keystroke that triggered the completion, so let's swap in the newest string. 323 | const modelRawText = model.getValue(); 324 | let best: { idx: number; length: number } | undefined = undefined; 325 | let bestBackspace: { idx: number; length: number } | undefined = undefined; 326 | for (const [i, text] of rawTexts.entries()) { 327 | if (modelRawText.startsWith(text)) { 328 | if (best === undefined || text.length > best.length) { 329 | best = { idx: i, length: text.length }; 330 | } 331 | } 332 | if (text.startsWith(modelRawText)) { 333 | // The shortest one wins in this case. 334 | if (bestBackspace === undefined || text.length < bestBackspace.length) { 335 | bestBackspace = { idx: i, length: text.length }; 336 | } 337 | } 338 | } 339 | if (best !== undefined) { 340 | rawTexts[best.idx] = modelRawText; 341 | } else if (bestBackspace !== undefined) { 342 | rawTexts[bestBackspace.idx] = modelRawText; 343 | } 344 | // TODO(prem): We can't actually tell between two models with the same text, do something more robust here. 345 | const valueAndStartOffset = getValueAndStartOffset(this.monacoSite, model); 346 | // computeTextAndOffsets is receiving shortened strings. 347 | return computeTextAndOffsets({ 348 | isNotebook: this.isNotebook(), 349 | textModels: rawTexts.map((m) => getValueAndStartOffset(this.monacoSite, m).value), 350 | currentTextModel: valueAndStartOffset.value, 351 | utf16CodeUnitOffset: model.getOffsetAt(position) - valueAndStartOffset.utf16Offset, 352 | getText: (text) => text, 353 | getLanguage: (text, idx) => { 354 | const model = textToModelMap.get(text); 355 | if (model !== undefined) { 356 | return getLanguage(getEditorLanguage(model)); 357 | } 358 | if (idx !== undefined) { 359 | // This is useful for handling the %md case which has no model. 360 | text = rawTexts[idx]; 361 | } 362 | if (text.startsWith('%sql')) { 363 | return Language.SQL; 364 | } else if (text.startsWith('%r')) { 365 | return Language.R; 366 | } else if (text.startsWith('%python')) { 367 | return Language.PYTHON; 368 | } else if (text.startsWith('%md')) { 369 | return Language.MARKDOWN; 370 | } else if (text.startsWith('%scala')) { 371 | return Language.SCALA; 372 | } 373 | return Language.UNSPECIFIED; 374 | }, 375 | }); 376 | } 377 | } 378 | if (this.monacoSite === OMonacoSite.COLAB) { 379 | // We manually populate the cell outputs in colab, as they are not available in the monaco editor objects. 380 | const cells = window.colab?.global.notebookModel.cells; 381 | const rawTexts = []; 382 | const textToLanguageMap = new Map(); 383 | for (const cell of cells ?? []) { 384 | let text = cell.textModel.getValue(); 385 | if (cell.type === 'code') { 386 | if ( 387 | cell.outputs.currentOutput !== undefined && 388 | cell.outputs.currentOutput.outputItems.length > 0 389 | ) { 390 | const data = cell.outputs.currentOutput.outputItems[0].data; 391 | if (data !== undefined) { 392 | if (data['text/plain'] !== undefined) { 393 | text = text + '\nOUTPUT:\n' + data['text/plain'].join(); 394 | } else if (data['text/html'] !== undefined) { 395 | text = text + '\nOUTPUT:\n' + data['text/html'].join(); 396 | } 397 | } 398 | } 399 | textToLanguageMap.set(text, getLanguage(getEditorLanguage(cell.textModel))); 400 | } 401 | rawTexts.push(text); 402 | } 403 | const valueAndStartOffset = getValueAndStartOffset(this.monacoSite, model); 404 | return computeTextAndOffsets({ 405 | isNotebook: this.isNotebook(), 406 | textModels: rawTexts.map((m) => getValueAndStartOffset(this.monacoSite, m).value), 407 | currentTextModel: valueAndStartOffset.value, 408 | utf16CodeUnitOffset: model.getOffsetAt(position) - valueAndStartOffset.utf16Offset, 409 | getText: (text) => text, 410 | getLanguage: (model) => { 411 | return ( 412 | textToLanguageMap.get(getValueAndStartOffset(this.monacoSite, model).value) ?? 413 | Language.UNSPECIFIED 414 | ); 415 | }, 416 | }); 417 | } 418 | return computeTextAndOffsets({ 419 | isNotebook: this.isNotebook(), 420 | textModels: this.textModels(model), 421 | currentTextModel: model, 422 | utf16CodeUnitOffset: 423 | model.getOffsetAt(position) - getValueAndStartOffset(this.monacoSite, model).utf16Offset, 424 | getText: (model) => getValueAndStartOffset(this.monacoSite, model).value, 425 | getLanguage: (model) => getLanguage(getEditorLanguage(model)), 426 | }); 427 | } 428 | 429 | async provideInlineCompletions( 430 | model: monaco.editor.ITextModel, 431 | position: monaco.Position 432 | ): Promise { 433 | const { text, utf8ByteOffset, additionalUtf8ByteOffset } = this.computeTextAndOffsets( 434 | model, 435 | position 436 | ); 437 | const numUtf8Bytes = additionalUtf8ByteOffset + utf8ByteOffset; 438 | const request = new GetCompletionsRequest({ 439 | metadata: this.client.getMetadata(this.getIdeInfo()), 440 | document: { 441 | text: text, 442 | editorLanguage: getEditorLanguage(model), 443 | language: getLanguage(getEditorLanguage(model)), 444 | cursorOffset: BigInt(numUtf8Bytes), 445 | lineEnding: '\n', 446 | absoluteUri: 'file:///' + this.absolutePath(model), 447 | }, 448 | editorOptions: { 449 | tabSize: BigInt(model.getOptions().tabSize), 450 | insertSpaces: model.getOptions().insertSpaces, 451 | }, 452 | }); 453 | await sleep(this.debounceMs ?? 0); 454 | const response = await this.client.getCompletions(request); 455 | if (response === undefined) { 456 | return; 457 | } 458 | const items = response.completionItems 459 | .map((completionItem) => 460 | createInlineCompletionItem( 461 | this.monacoSite, 462 | completionItem, 463 | model, 464 | additionalUtf8ByteOffset, 465 | this.modelUriToEditor.get(model.uri.toString()) 466 | ) 467 | ) 468 | .filter((item): item is monaco.languages.InlineCompletion => item !== undefined); 469 | void chrome.runtime.sendMessage(this.extensionId, { type: 'success' }); 470 | return { items }; 471 | } 472 | 473 | handleItemDidShow(): void { 474 | // Do nothing. 475 | } 476 | 477 | freeInlineCompletions(): void { 478 | // Do nothing. 479 | } 480 | 481 | addEditor(editor: monaco.editor.ICodeEditor): void { 482 | if (this.monacoSite !== OMonacoSite.DATABRICKS) { 483 | editor.updateOptions({ inlineSuggest: { enabled: true } }); 484 | } 485 | const uri = editor.getModel()?.uri.toString(); 486 | if (uri !== undefined) { 487 | this.modelUriToEditor.set(uri, editor); 488 | } 489 | editor.onDidChangeModel((e) => { 490 | const oldUri = e.oldModelUrl?.toString(); 491 | if (oldUri !== undefined) { 492 | this.modelUriToEditor.delete(oldUri); 493 | } 494 | const newUri = e.newModelUrl?.toString(); 495 | if (newUri !== undefined) { 496 | this.modelUriToEditor.set(newUri, editor); 497 | } 498 | }); 499 | if (this.monacoSite === OMonacoSite.DEEPNOTE) { 500 | // Hack to intercept the key listener. 501 | (editor as any).onKeyDown = wrapOnKeyDown(editor.onKeyDown); 502 | } 503 | } 504 | 505 | async acceptedLastCompletion(completionId: string): Promise { 506 | await this.client.acceptedLastCompletion(this.getIdeInfo(), completionId); 507 | } 508 | } 509 | 510 | function wrapOnKeyDownListener( 511 | listener: (e: monaco.IKeyboardEvent) => any 512 | ): (e: monaco.IKeyboardEvent) => any { 513 | return function (e: monaco.IKeyboardEvent): any { 514 | if (e.browserEvent.key === 'Tab') { 515 | return; 516 | } 517 | return listener(e); 518 | }; 519 | } 520 | 521 | function wrapOnKeyDown( 522 | onKeyDown: ( 523 | this: monaco.editor.ICodeEditor, 524 | listener: (e: monaco.IKeyboardEvent) => any, 525 | thisArg?: any 526 | ) => void 527 | ): ( 528 | this: monaco.editor.ICodeEditor, 529 | listener: (e: monaco.IKeyboardEvent) => any, 530 | thisArg?: any 531 | ) => void { 532 | return function ( 533 | this: monaco.editor.ICodeEditor, 534 | listener: (e: monaco.IKeyboardEvent) => any, 535 | thisArg?: any 536 | ): void { 537 | onKeyDown.call(this, wrapOnKeyDownListener(listener), thisArg); 538 | }; 539 | } 540 | -------------------------------------------------------------------------------- /src/monacoLanguages.ts: -------------------------------------------------------------------------------- 1 | import { Language } from '../proto/exa/codeium_common_pb/codeium_common_pb'; 2 | 3 | // Map from VSCode language to Codeium language. 4 | // Languages from https://code.visualstudio.com/docs/languages/identifiers 5 | const LANGUAGE_MAP = new Map([ 6 | ['bazel', Language.STARLARK], 7 | ['c', Language.C], 8 | ['clojure', Language.CLOJURE], 9 | ['coffeescript', Language.COFFEESCRIPT], 10 | ['cpp', Language.CPP], 11 | ['csharp', Language.CSHARP], 12 | ['css', Language.CSS], 13 | ['cuda-cpp', Language.CUDACPP], 14 | ['dockerfile', Language.DOCKERFILE], 15 | ['go', Language.GO], 16 | ['groovy', Language.GROOVY], 17 | ['handlebars', Language.HANDLEBARS], 18 | ['haskell', Language.HASKELL], 19 | ['html', Language.HTML], 20 | ['ini', Language.INI], 21 | ['java', Language.JAVA], 22 | ['javascript', Language.JAVASCRIPT], 23 | ['javascriptreact', Language.JAVASCRIPT], 24 | ['json', Language.JSON], 25 | ['jsonc', Language.JSON], 26 | ['jsx', Language.JAVASCRIPT], 27 | ['julia', Language.JULIA], 28 | ['kotlin', Language.KOTLIN], 29 | ['latex', Language.LATEX], 30 | ['less', Language.LESS], 31 | ['lua', Language.LUA], 32 | ['makefile', Language.MAKEFILE], 33 | ['markdown', Language.MARKDOWN], 34 | ['objective-c', Language.OBJECTIVEC], 35 | ['objective-cpp', Language.OBJECTIVECPP], 36 | ['pbtxt', Language.PBTXT], 37 | ['perl', Language.PERL], 38 | ['pgsql', Language.SQL], 39 | ['php', Language.PHP], 40 | ['plaintext', Language.PLAINTEXT], 41 | ['proto3', Language.PROTOBUF], 42 | ['python', Language.PYTHON], 43 | ['r', Language.R], 44 | ['ruby', Language.RUBY], 45 | ['rust', Language.RUST], 46 | ['sass', Language.SASS], 47 | ['scala', Language.SCALA], 48 | ['scss', Language.SCSS], 49 | ['shellscript', Language.SHELL], 50 | ['sql', Language.SQL], 51 | ['swift', Language.SWIFT], 52 | ['terraform', Language.HCL], 53 | ['typescript', Language.TYPESCRIPT], 54 | ['typescriptreact', Language.TSX], 55 | ['vb', Language.VISUALBASIC], 56 | ['vue-html', Language.VUE], 57 | ['vue', Language.VUE], 58 | ['xml', Language.XML], 59 | ['xsl', Language.XSL], 60 | ['yaml', Language.YAML], 61 | // Special cases. 62 | ['notebook-python', Language.PYTHON], // colab 63 | ['notebook-python-lsp', Language.PYTHON], // colab 64 | ]); 65 | 66 | export function getLanguage(language: string): Language { 67 | return LANGUAGE_MAP.get(language) ?? Language.UNSPECIFIED; 68 | } 69 | -------------------------------------------------------------------------------- /src/notebook.ts: -------------------------------------------------------------------------------- 1 | import { numCodeUnitsToNumUtf8Bytes } from './utf'; 2 | import { Language } from '../proto/exa/codeium_common_pb/codeium_common_pb'; 3 | 4 | export interface TextAndOffsets { 5 | // The smart concatenation of all notebook cells, or just the text of the main document. 6 | text: string; 7 | // The offset into the current cell/document. 8 | utf8ByteOffset: number; 9 | // Any additional offset induced by the smart concatenation. 10 | additionalUtf8ByteOffset: number; 11 | } 12 | 13 | const NOTEBOOK_LANGUAGES = { 14 | [Language.PYTHON]: 'python', 15 | [Language.SQL]: 'sql', 16 | [Language.R]: '', // Not supported by GFM. 17 | [Language.MARKDOWN]: 'markdown', 18 | [Language.SCALA]: '', // Not supported by GFM. 19 | } as const; 20 | type AllowedLanguages = keyof typeof NOTEBOOK_LANGUAGES; 21 | 22 | function isAllowedLanguage(language: Language) { 23 | return Object.prototype.hasOwnProperty.call(NOTEBOOK_LANGUAGES, language); 24 | } 25 | 26 | // In Jupyter, we can have cells which are neither Markdown nor Python, so we 27 | // need to define both functions in the interface. 28 | export interface MaybeNotebook { 29 | readonly textModels: T[]; 30 | readonly currentTextModel: T; 31 | readonly currentTextModelWithOutput?: T; 32 | // Whether the document is a notebook. For notebooks, we will prepend with \nCELL:\n 33 | readonly isNotebook: boolean; 34 | // The offset into the value of getText(currentTextModel) at which to trigger a completion. 35 | readonly utf16CodeUnitOffset: number; 36 | getText(model: T): string; 37 | // idx is the position in the textModels array, or undefined if it's the currentTextModel. 38 | getLanguage(model: T, idx: number | undefined): Language; 39 | } 40 | 41 | // Note: Assumes that all notebooks are Python. 42 | export function computeTextAndOffsets(maybeNotebook: MaybeNotebook): TextAndOffsets { 43 | const textModels = maybeNotebook.textModels ?? []; 44 | const modelLanguage = maybeNotebook.getLanguage(maybeNotebook.currentTextModel, undefined); 45 | const modelIsMarkdown = modelLanguage === Language.MARKDOWN; 46 | const cellSplitString = modelIsMarkdown ? '\n\n' : '\nCELL:\n'; 47 | const modelIsExpected = isAllowedLanguage(modelLanguage); 48 | const relevantDocumentTexts: string[] = []; 49 | let additionalUtf8ByteOffset = 0; 50 | let found = false; 51 | for (const [idx, previousModel] of textModels.entries()) { 52 | const isCurrentCell = modelIsExpected && maybeNotebook.currentTextModel === previousModel; 53 | if (isCurrentCell) { 54 | // There is an offset for all previous cells and the newline spacing after each one. 55 | additionalUtf8ByteOffset = 56 | relevantDocumentTexts 57 | .map((el) => numCodeUnitsToNumUtf8Bytes(el)) 58 | .reduce((a, b) => a + b, 0) + 59 | cellSplitString.length * 60 | (relevantDocumentTexts.length + (maybeNotebook.isNotebook ? 1 : 0)); 61 | found = true; 62 | } 63 | const previousModelLanguage = maybeNotebook.getLanguage(previousModel, idx); 64 | if (modelIsExpected && !modelIsMarkdown) { 65 | // Don't use markdown in the Python prompt construction. 66 | // TODO(prem): Consider adding as comments. 67 | if (previousModelLanguage === Language.MARKDOWN) { 68 | continue; 69 | } else if (previousModelLanguage === modelLanguage) { 70 | if (isCurrentCell && maybeNotebook.currentTextModelWithOutput !== undefined) { 71 | relevantDocumentTexts.push( 72 | maybeNotebook.getText(maybeNotebook.currentTextModelWithOutput) 73 | ); 74 | } else { 75 | relevantDocumentTexts.push(maybeNotebook.getText(previousModel)); 76 | } 77 | } 78 | } else if (modelIsMarkdown) { 79 | if (previousModelLanguage === Language.MARKDOWN) { 80 | relevantDocumentTexts.push(maybeNotebook.getText(previousModel)); 81 | } else if (isAllowedLanguage(previousModelLanguage)) { 82 | relevantDocumentTexts.push( 83 | `\`\`\`${ 84 | NOTEBOOK_LANGUAGES[previousModelLanguage as AllowedLanguages] 85 | }\n${maybeNotebook.getText(previousModel)}\n\`\`\`` 86 | ); 87 | } 88 | } 89 | } 90 | const currentModelText = maybeNotebook.getText( 91 | maybeNotebook.currentTextModelWithOutput ?? maybeNotebook.currentTextModel 92 | ); 93 | let text = found ? `${relevantDocumentTexts.join(cellSplitString)}` : `${currentModelText}`; 94 | text = maybeNotebook.isNotebook ? `${cellSplitString}${text}` : text; 95 | const utf8ByteOffset = numCodeUnitsToNumUtf8Bytes( 96 | currentModelText, 97 | maybeNotebook.utf16CodeUnitOffset 98 | ); 99 | return { 100 | text, 101 | utf8ByteOffset, 102 | additionalUtf8ByteOffset, 103 | }; 104 | } 105 | -------------------------------------------------------------------------------- /src/options.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/options.scss'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | import Options from './component/Options'; 6 | 7 | const container = document.getElementById('codeium-options'); 8 | if (container !== null) { 9 | ReactDOM.render(, container); 10 | } 11 | -------------------------------------------------------------------------------- /src/popup.ts: -------------------------------------------------------------------------------- 1 | import '../styles/popup.scss'; 2 | import { openAuthTab } from './auth'; 3 | import { loggedOut } from './shared'; 4 | import { getStorageItem, setStorageItem } from './storage'; 5 | 6 | if (CODEIUM_ENTERPRISE) { 7 | const element = document.getElementById('extension-name'); 8 | if (element !== null) { 9 | element.textContent = 'Codeium Enterprise'; 10 | } 11 | } 12 | 13 | document.getElementById('login')?.addEventListener('click', openAuthTab); 14 | 15 | async function maybeShowPortalWarning() { 16 | const portalUrl = await getStorageItem('portalUrl'); 17 | let portalUrlWarningDisplay = 'none'; 18 | let loginButtonDisplay = 'block'; 19 | if (portalUrl === undefined || portalUrl === '') { 20 | portalUrlWarningDisplay = 'block'; 21 | loginButtonDisplay = 'none'; 22 | } 23 | const portalUrlWarning = document.getElementById('portal-url-warning'); 24 | if (portalUrlWarning !== null) { 25 | portalUrlWarning.style.display = portalUrlWarningDisplay; 26 | } 27 | const loginButton = document.getElementById('login'); 28 | if (loginButton !== null) { 29 | loginButton.style.display = loginButtonDisplay; 30 | } 31 | } 32 | if (CODEIUM_ENTERPRISE) { 33 | maybeShowPortalWarning().catch((e) => { 34 | console.error(e); 35 | }); 36 | setInterval(maybeShowPortalWarning, 1000); 37 | } 38 | 39 | document.getElementById('go-to-options')?.addEventListener('click', async () => { 40 | await chrome.tabs.create({ url: 'chrome://extensions/?options=' + chrome.runtime.id }); 41 | }); 42 | 43 | getStorageItem('user') 44 | .then((user) => { 45 | const usernameP = document.getElementById('username'); 46 | if (usernameP !== null && user !== undefined) { 47 | usernameP.textContent = `Welcome, ${user.name}`; 48 | if (user.userPortalUrl !== undefined && user.userPortalUrl !== '') { 49 | const br = document.createElement('br'); 50 | usernameP.appendChild(br); 51 | const a = document.createElement('a'); 52 | const linkText = document.createTextNode('Portal'); 53 | a.appendChild(linkText); 54 | a.title = 'Portal'; 55 | a.href = user.userPortalUrl; 56 | a.addEventListener('click', async () => { 57 | await chrome.tabs.create({ url: user.userPortalUrl }); 58 | }); 59 | usernameP.appendChild(a); 60 | } 61 | } 62 | }) 63 | .catch((error) => { 64 | console.error(error); 65 | }); 66 | 67 | document.getElementById('logout')?.addEventListener('click', async () => { 68 | await setStorageItem('user', {}); 69 | await loggedOut(); 70 | window.close(); 71 | }); 72 | 73 | getStorageItem('lastError').then( 74 | (lastError) => { 75 | const errorP = document.getElementById('error'); 76 | if (errorP == null) { 77 | return; 78 | } 79 | const message = lastError?.message; 80 | if (message === undefined) { 81 | errorP.remove(); 82 | } else { 83 | errorP.textContent = message; 84 | } 85 | }, 86 | (e) => { 87 | console.error(e); 88 | } 89 | ); 90 | -------------------------------------------------------------------------------- /src/script.ts: -------------------------------------------------------------------------------- 1 | import type { JupyterFrontEnd } from '@jupyterlab/application'; 2 | import type * as monaco from 'monaco-editor'; 3 | 4 | import { addListeners } from './codemirror'; 5 | import { CodeMirrorState } from './codemirrorInject'; 6 | import { JupyterNotebookKeyBindings } from './common'; 7 | import { inject as jupyterInject } from './jupyterInject'; 8 | import { getPlugin } from './jupyterlabPlugin'; 9 | import { MonacoCompletionProvider, MonacoSite, OMonacoSite } from './monacoCompletionProvider'; 10 | 11 | declare type Monaco = typeof import('monaco-editor'); 12 | declare type CodeMirror = typeof import('codemirror'); 13 | 14 | const params = new URLSearchParams((document.currentScript as HTMLScriptElement).src.split('?')[1]); 15 | const extensionId = params.get('id')!; 16 | 17 | async function getAllowedAndKeybindings( 18 | extensionId: string 19 | ): Promise<{ allowed: boolean; keyBindings: JupyterNotebookKeyBindings }> { 20 | const result = await new Promise<{ allowed: any; keyBindings: JupyterNotebookKeyBindings }>( 21 | (resolve) => { 22 | chrome.runtime.sendMessage( 23 | extensionId, 24 | { type: 'jupyter_notebook_allowed_and_keybindings' }, 25 | (response: { allowed: boolean; keyBindings: JupyterNotebookKeyBindings }) => { 26 | resolve(response); 27 | } 28 | ); 29 | } 30 | ); 31 | return result; 32 | } 33 | 34 | async function getDebounceMs(extensionId: string): Promise<{ debounceMs: number }> { 35 | const result = await new Promise<{ debounceMs: number }>((resolve) => { 36 | chrome.runtime.sendMessage( 37 | extensionId, 38 | { type: 'debounce_ms' }, 39 | (response: { debounceMs: number }) => { 40 | resolve(response); 41 | } 42 | ); 43 | }); 44 | return result; 45 | } 46 | 47 | // Clear any bad state from another tab. 48 | void chrome.runtime.sendMessage(extensionId, { type: 'success' }); 49 | 50 | const SUPPORTED_MONACO_SITES = new Map([ 51 | [/https:\/\/colab.research\.google\.com\/.*/, OMonacoSite.COLAB], 52 | [/https:\/\/(.*\.)?stackblitz\.com\/.*/, OMonacoSite.STACKBLITZ], 53 | [/https:\/\/(.*\.)?deepnote\.com\/.*/, OMonacoSite.DEEPNOTE], 54 | [/https:\/\/(.*\.)?(databricks\.com|azuredatabricks\.net)\/.*/, OMonacoSite.DATABRICKS], 55 | [/https:\/\/(.*\.)?quadratichq\.com\/.*/, OMonacoSite.QUADRATIC], 56 | ]); 57 | 58 | declare global { 59 | interface Window { 60 | _monaco?: Monaco; 61 | _MonacoEnvironment?: monaco.Environment; 62 | } 63 | } 64 | 65 | // Intercept creation of monaco so we don't have to worry about timing the injection. 66 | const addMonacoInject = (debounceMs: number) => 67 | Object.defineProperties(window, { 68 | MonacoEnvironment: { 69 | get() { 70 | if (this._codeium_MonacoEnvironment === undefined) { 71 | this._codeium_MonacoEnvironment = { globalAPI: true }; 72 | } 73 | return this._codeium_MonacoEnvironment; 74 | }, 75 | set(env: monaco.Environment | undefined) { 76 | if (env !== undefined) { 77 | env.globalAPI = true; 78 | } 79 | this._codeium_MonacoEnvironment = env; 80 | }, 81 | }, 82 | monaco: { 83 | get(): Monaco | undefined { 84 | return this._codeium_monaco; 85 | }, 86 | set(_monaco: Monaco) { 87 | let injectMonaco: MonacoSite = OMonacoSite.CUSTOM; 88 | for (const [sitePattern, site] of SUPPORTED_MONACO_SITES) { 89 | if (sitePattern.test(window.location.href)) { 90 | injectMonaco = site; 91 | break; 92 | } 93 | } 94 | this._codeium_monaco = _monaco; 95 | const completionProvider = new MonacoCompletionProvider( 96 | extensionId, 97 | injectMonaco, 98 | debounceMs 99 | ); 100 | if (!_monaco?.languages?.registerInlineCompletionsProvider) { 101 | return; 102 | } 103 | setTimeout(() => { 104 | _monaco.languages.registerInlineCompletionsProvider( 105 | { pattern: '**' }, 106 | completionProvider 107 | ); 108 | _monaco.editor.registerCommand( 109 | 'codeium.acceptCompletion', 110 | (_: unknown, apiKey: string, completionId: string, callback?: () => void) => { 111 | callback?.(); 112 | completionProvider.acceptedLastCompletion(completionId).catch((e) => { 113 | console.error(e); 114 | }); 115 | } 116 | ); 117 | _monaco.editor.onDidCreateEditor((editor: monaco.editor.ICodeEditor) => { 118 | completionProvider.addEditor(editor); 119 | }); 120 | console.log('Codeium: Activated Monaco'); 121 | }); 122 | }, 123 | }, 124 | }); 125 | 126 | let injectCodeMirror = false; 127 | 128 | const addJupyterLabInject = (jupyterConfigDataElement: HTMLElement, debounceMs: number) => { 129 | const config = JSON.parse(jupyterConfigDataElement.innerText); 130 | config.exposeAppInBrowser = true; 131 | jupyterConfigDataElement.innerText = JSON.stringify(config); 132 | injectCodeMirror = true; 133 | Object.defineProperty(window, 'jupyterapp', { 134 | get: function () { 135 | return this._codeium_jupyterapp; 136 | }, 137 | set: function (_jupyterapp?: JupyterFrontEnd) { 138 | if (_jupyterapp?.version.startsWith('3.')) { 139 | const p = getPlugin(extensionId, _jupyterapp, debounceMs); 140 | _jupyterapp.registerPlugin(p); 141 | _jupyterapp.activatePlugin(p.id).then( 142 | () => { 143 | console.log('Codeium: Activated JupyterLab 3.x'); 144 | }, 145 | (e) => { 146 | console.error(e); 147 | } 148 | ); 149 | } else if (_jupyterapp?.version.startsWith('4.')) { 150 | void chrome.runtime.sendMessage(extensionId, { 151 | type: 'error', 152 | message: 153 | 'Only JupyterLab 3.x is supported. Use the codeium-jupyter extension for JupyterLab 4', 154 | }); 155 | } else { 156 | void chrome.runtime.sendMessage(extensionId, { 157 | type: 'error', 158 | message: `Codeium: Unexpected JupyterLab version: ${ 159 | _jupyterapp?.version ?? '(unknown)' 160 | }. Only JupyterLab 3.x is supported`, 161 | }); 162 | } 163 | this._codeium_jupyterapp = _jupyterapp; 164 | }, 165 | }); 166 | Object.defineProperty(window, 'jupyterlab', { 167 | get: function () { 168 | return this._codeium_jupyterlab; 169 | }, 170 | set: function (_jupyterlab?: JupyterFrontEnd) { 171 | if (_jupyterlab?.version.startsWith('2.')) { 172 | const p = getPlugin(extensionId, _jupyterlab, debounceMs); 173 | _jupyterlab.registerPlugin(p); 174 | _jupyterlab.activatePlugin(p.id).then( 175 | () => { 176 | console.log('Codeium: Activated JupyterLab 2.x'); 177 | }, 178 | (e) => { 179 | console.error(e); 180 | } 181 | ); 182 | } 183 | this._codeium_jupyterlab = _jupyterlab; 184 | }, 185 | }); 186 | }; 187 | 188 | const SUPPORTED_CODEMIRROR_SITES = [ 189 | { name: 'JSFiddle', pattern: /https?:\/\/(.*\.)?jsfiddle\.net(\/.*)?/, multiplayer: false }, 190 | { name: 'CodePen', pattern: /https:\/\/(.*\.)?codepen\.io(\/.*)?/, multiplayer: false }, 191 | { name: 'CodeShare', pattern: /https:\/\/(.*\.)?codeshare\.io(\/.*)?/, multiplayer: true }, 192 | ]; 193 | 194 | const addCodeMirror5GlobalInject = ( 195 | keybindings: JupyterNotebookKeyBindings | undefined, 196 | debounceMs: number 197 | ) => 198 | Object.defineProperty(window, 'CodeMirror', { 199 | get: function () { 200 | return this._codeium_CodeMirror; 201 | }, 202 | set: function (cm?: { version?: string }) { 203 | this._codeium_CodeMirror = cm; 204 | if (injectCodeMirror) { 205 | return; 206 | } 207 | if (!cm?.version?.startsWith('5.')) { 208 | console.warn("Codeium: Codeium doesn't support CodeMirror 6"); 209 | return; 210 | } 211 | // We rely on the fact that the Jupyter variable is defined first. 212 | if (Object.prototype.hasOwnProperty.call(this, 'Jupyter')) { 213 | injectCodeMirror = true; 214 | if (keybindings === undefined) { 215 | console.warn('Codeium: found no keybindings for Jupyter Notebook'); 216 | return; 217 | } else { 218 | const jupyterState = jupyterInject(extensionId, this.Jupyter, keybindings, debounceMs); 219 | addListeners(cm as CodeMirror, jupyterState.codeMirrorManager); 220 | console.log('Codeium: Activating Jupyter Notebook'); 221 | } 222 | } else { 223 | let multiplayer = false; 224 | let name = ''; 225 | for (const pattern of SUPPORTED_CODEMIRROR_SITES) { 226 | if (pattern.pattern.test(window.location.href)) { 227 | name = pattern.name; 228 | injectCodeMirror = true; 229 | multiplayer = pattern.multiplayer; 230 | break; 231 | } 232 | } 233 | if (injectCodeMirror) { 234 | new CodeMirrorState(extensionId, cm as CodeMirror, multiplayer, debounceMs); 235 | console.log(`Codeium: Activating CodeMirror Site: ${name}`); 236 | } 237 | } 238 | }, 239 | }); 240 | 241 | // In this case, the CodeMirror 5 editor is accessible as a property of elements 242 | // with the class CodeMirror. 243 | // TODO(kevin): Do these still work? 244 | const SUPPORTED_CODEMIRROR_NONGLOBAL_SITES = [ 245 | { pattern: /https:\/\/console\.paperspace\.com\/.*\/notebook\/.*/, notebook: true }, 246 | { pattern: /https?:\/\/www\.codewars\.com(\/.*)?/, notebook: false }, 247 | { pattern: /https:\/\/(.*\.)?github\.com(\/.*)?/, notebook: false }, 248 | ]; 249 | 250 | const codeMirrorState = new CodeMirrorState(extensionId, undefined, false); 251 | const hook = codeMirrorState.editorHook(); 252 | 253 | const addCodeMirror5LocalInject = () => { 254 | const f = setInterval(() => { 255 | if (injectCodeMirror) { 256 | clearInterval(f); 257 | return; 258 | } 259 | let notebook = false; 260 | for (const pattern of SUPPORTED_CODEMIRROR_NONGLOBAL_SITES) { 261 | if (pattern.pattern.test(window.location.href)) { 262 | notebook = pattern.notebook; 263 | break; 264 | } 265 | } 266 | const docsByPosition = new Map(); 267 | for (const el of document.getElementsByClassName('CodeMirror')) { 268 | const maybeCodeMirror = el as { CodeMirror?: CodeMirror.Editor }; 269 | if (maybeCodeMirror.CodeMirror === undefined) { 270 | continue; 271 | } 272 | const editor = maybeCodeMirror.CodeMirror; 273 | hook(editor); 274 | if (notebook) { 275 | docsByPosition.set(editor.getDoc(), (el as HTMLElement).getBoundingClientRect().top); 276 | } 277 | } 278 | if (notebook) { 279 | const docs = [...docsByPosition.entries()].sort((a, b) => a[1] - b[1]).map(([doc]) => doc); 280 | codeMirrorState.docs = docs; 281 | } 282 | }, 500); 283 | }; 284 | 285 | Promise.all([getAllowedAndKeybindings(extensionId), getDebounceMs(extensionId)]).then( 286 | ([allowedAndKeybindings, debounceMs]) => { 287 | const allowed = allowedAndKeybindings.allowed; 288 | const jupyterKeyBindings = allowedAndKeybindings.keyBindings; 289 | const debounce = debounceMs.debounceMs; 290 | const validInjectTypes = ['monaco', 'codemirror5', 'none']; 291 | const metaTag = document.querySelector('meta[name="codeium:type"]'); 292 | const injectionTypes = 293 | metaTag 294 | ?.getAttribute('content') 295 | ?.split(',') 296 | .map((x) => x.toLowerCase().trim()) 297 | .filter((x) => validInjectTypes.includes(x)) ?? []; 298 | 299 | if (injectionTypes.includes('none')) { 300 | // do not inject if specifically disabled 301 | return; 302 | } 303 | 304 | // Inject jupyter lab; this is seperate from the others; this is seperate from the others 305 | const jupyterConfigDataElement = document.getElementById('jupyter-config-data'); 306 | if (jupyterConfigDataElement !== null) { 307 | addJupyterLabInject(jupyterConfigDataElement, debounce); 308 | return; 309 | } 310 | 311 | if (injectionTypes.includes('monaco')) { 312 | addMonacoInject(debounce); 313 | } 314 | 315 | if (injectionTypes.includes('codemirror5')) { 316 | addCodeMirror5GlobalInject(jupyterKeyBindings, debounce); 317 | addCodeMirror5LocalInject(); 318 | } 319 | 320 | if (injectionTypes.length === 0) { 321 | // if no meta tag is found, check the allowlist 322 | if (allowed) { 323 | // the url matches the allowlist 324 | addMonacoInject(debounce); 325 | addCodeMirror5GlobalInject(jupyterKeyBindings, debounce); 326 | addCodeMirror5LocalInject(); 327 | return; 328 | } 329 | } 330 | }, 331 | (e) => { 332 | console.error(e); 333 | } 334 | ); 335 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | import { registerUser } from './auth'; 4 | import { 5 | GetCompletionsResponseMessage, 6 | JupyterLabKeyBindings, 7 | JupyterNotebookKeyBindings, 8 | KeyCombination, 9 | LanguageServerServiceWorkerClient, 10 | LanguageServerWorkerRequest, 11 | } from './common'; 12 | import { loggedIn, loggedOut, unhealthy } from './shared'; 13 | import { 14 | computeAllowlist, 15 | defaultAllowlist, 16 | getGeneralPortalUrl, 17 | getStorageItem, 18 | getStorageItems, 19 | initializeStorageWithDefaults, 20 | setStorageItem, 21 | } from './storage'; 22 | import { PUBLIC_API_SERVER, PUBLIC_WEBSITE } from './urls'; 23 | import { 24 | AcceptCompletionRequest, 25 | GetCompletionsRequest, 26 | } from '../proto/exa/language_server_pb/language_server_pb'; 27 | 28 | const authStates: string[] = []; 29 | 30 | chrome.runtime.onInstalled.addListener(async () => { 31 | // Here goes everything you want to execute after extension initialization 32 | 33 | await initializeStorageWithDefaults({ 34 | settings: {}, 35 | allowlist: { defaults: defaultAllowlist, current: defaultAllowlist }, 36 | }); 37 | 38 | console.log('Extension successfully installed!'); 39 | 40 | if ((await getStorageItem('user'))?.apiKey === undefined) { 41 | // TODO(prem): Is this necessary? 42 | await loggedOut(); 43 | // Inline the code for openAuthTab() because we can't invoke sendMessage. 44 | const uuid = uuidv4(); 45 | authStates.push(uuid); 46 | // TODO(prem): Deduplicate with Options.tsx/storage.ts. 47 | const portalUrl = await (async (): Promise => { 48 | const url = await getGeneralPortalUrl(); 49 | if (url === undefined) { 50 | if (CODEIUM_ENTERPRISE) { 51 | return undefined; 52 | } 53 | return PUBLIC_WEBSITE; 54 | } 55 | return url; 56 | })(); 57 | if (portalUrl !== undefined) { 58 | await chrome.tabs.create({ 59 | url: `${portalUrl}/profile?redirect_uri=chrome-extension://${chrome.runtime.id}&state=${uuid}`, 60 | }); 61 | } 62 | } else { 63 | await loggedIn(); 64 | } 65 | }); 66 | 67 | const parseKeyCombination = (key: string): KeyCombination => { 68 | const parts = key.split('+').map((k) => k.trim()); 69 | return { 70 | key: parts[parts.length - 1], 71 | ctrl: parts.includes('Ctrl'), 72 | alt: parts.includes('Alt'), 73 | shift: parts.includes('Shift'), 74 | meta: parts.includes('Meta'), 75 | }; 76 | }; 77 | 78 | // The only external messages: 79 | // - website auth 80 | // - request for api key 81 | // - set icon and error message 82 | chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => { 83 | if (message.type === 'jupyter_notebook_allowed_and_keybindings') { 84 | (async () => { 85 | // If not allowed, the keybindings can be undefined. 86 | let allowed = false; 87 | const defaultKeyBindings: JupyterNotebookKeyBindings = { 88 | accept: { key: 'Tab', ctrl: false, alt: false, shift: false, meta: false }, 89 | }; 90 | if (sender.url === undefined) { 91 | sendResponse({ allowed: false, keyBindings: defaultKeyBindings }); 92 | return; 93 | } 94 | const { allowlist: allowlist, jupyterNotebookKeybindingAccept: accept } = 95 | await getStorageItems(['allowlist', 'jupyterNotebookKeybindingAccept']); 96 | for (const addr of computeAllowlist(allowlist)) { 97 | const host = new RegExp(addr); 98 | if (host.test(sender.url)) { 99 | allowed = true; 100 | break; 101 | } 102 | } 103 | 104 | sendResponse({ 105 | allowed, 106 | keyBindings: { 107 | accept: accept ? parseKeyCombination(accept) : defaultKeyBindings, 108 | }, 109 | }); 110 | })().catch((e) => { 111 | console.error(e); 112 | }); 113 | return true; 114 | } 115 | if (message.type === 'jupyterlab') { 116 | (async () => { 117 | const { jupyterlabKeybindingAccept: accept, jupyterlabKeybindingDismiss: dismiss } = 118 | await getStorageItems(['jupyterlabKeybindingAccept', 'jupyterlabKeybindingDismiss']); 119 | 120 | const keybindings: JupyterLabKeyBindings = { 121 | accept: accept 122 | ? parseKeyCombination(accept) 123 | : { key: 'Tab', ctrl: false, alt: false, shift: false, meta: false }, 124 | dismiss: dismiss 125 | ? parseKeyCombination(dismiss) 126 | : { key: 'Escape', ctrl: false, alt: false, shift: false, meta: false }, 127 | }; 128 | sendResponse(keybindings); 129 | })().catch((e) => { 130 | console.error(e); 131 | }); 132 | return true; 133 | } 134 | if (message.type === 'debounce_ms') { 135 | (async () => { 136 | const { jupyterDebounceMs: jupyterDebounceMs } = await getStorageItems(['jupyterDebounceMs']); 137 | sendResponse({ debounceMs: jupyterDebounceMs ? jupyterDebounceMs : 0 }); 138 | })().catch((e) => { 139 | console.error(e); 140 | }); 141 | return true; 142 | } 143 | if (message.type == 'error') { 144 | unhealthy(message.message).catch((e) => { 145 | console.error(e); 146 | }); 147 | // No response needed. 148 | return; 149 | } 150 | if (message.type == 'success') { 151 | loggedIn().catch((e) => { 152 | console.error(e); 153 | }); 154 | // No response needed. 155 | return; 156 | } 157 | if (typeof message.token !== 'string' || typeof message.state !== 'string') { 158 | console.log('Unexpected message:', message); 159 | return; 160 | } 161 | (async () => { 162 | const typedMessage = message as { token: string; state: string }; 163 | const user = await getStorageItem('user'); 164 | if (user?.apiKey === undefined) { 165 | await login(typedMessage.token); 166 | } 167 | })().catch((e) => { 168 | console.error(e); 169 | }); 170 | }); 171 | 172 | chrome.runtime.onStartup.addListener(async () => { 173 | if ((await getStorageItem('user'))?.apiKey === undefined) { 174 | await loggedOut(); 175 | } else { 176 | await loggedIn(); 177 | } 178 | }); 179 | 180 | chrome.runtime.onMessage.addListener((message) => { 181 | // TODO(prem): Strongly type this. 182 | if (message.type === 'state') { 183 | const payload = message.payload as { state: string }; 184 | authStates.push(payload.state); 185 | } else if (message.type === 'manual') { 186 | login(message.token).catch((e) => { 187 | console.error(e); 188 | }); 189 | } else { 190 | console.log('Unrecognized message:', message); 191 | } 192 | }); 193 | 194 | const clientMap = new Map(); 195 | 196 | // TODO(prem): Is it safe to make this listener async to simplify the LanguageServerServiceWorkerClient constructor? 197 | chrome.runtime.onConnectExternal.addListener((port) => { 198 | // TODO(prem): Technically this URL isn't synchronized with the user/API key. 199 | clientMap.set( 200 | port.name, 201 | new LanguageServerServiceWorkerClient(getLanguageServerUrl(), port.name) 202 | ); 203 | port.onDisconnect.addListener((port) => { 204 | clientMap.delete(port.name); 205 | }); 206 | port.onMessage.addListener(async (message: LanguageServerWorkerRequest, port) => { 207 | const client = clientMap.get(port.name); 208 | if (message.kind === 'getCompletions') { 209 | const response = await client?.getCompletions( 210 | GetCompletionsRequest.fromJsonString(message.request) 211 | ); 212 | const reply: GetCompletionsResponseMessage = { 213 | kind: 'getCompletions', 214 | requestId: message.requestId, 215 | response: response?.toJsonString(), 216 | }; 217 | port.postMessage(reply); 218 | } else if (message.kind == 'acceptCompletion') { 219 | await client?.acceptedLastCompletion(AcceptCompletionRequest.fromJsonString(message.request)); 220 | } else { 221 | console.log('Unrecognized message:', message); 222 | } 223 | }); 224 | }); 225 | 226 | async function login(token: string) { 227 | try { 228 | const portalUrl = await getGeneralPortalUrl(); 229 | const user = await registerUser(token); 230 | await setStorageItem('user', { 231 | apiKey: user.api_key, 232 | name: user.name, 233 | userPortalUrl: portalUrl, 234 | }); 235 | await loggedIn(); 236 | // TODO(prem): Open popup. 237 | // https://github.com/GoogleChrome/developer.chrome.com/issues/2602 238 | // await chrome.action.openPopup(); 239 | } catch (error) { 240 | console.log(error); 241 | } 242 | } 243 | 244 | async function getLanguageServerUrl(): Promise { 245 | const user = await getStorageItem('user'); 246 | const userPortalUrl = user?.userPortalUrl; 247 | if (userPortalUrl === undefined || userPortalUrl === '') { 248 | if (CODEIUM_ENTERPRISE) { 249 | return undefined; 250 | } 251 | return PUBLIC_API_SERVER; 252 | } 253 | return `${userPortalUrl}/_route/language_server`; 254 | } 255 | -------------------------------------------------------------------------------- /src/shared.ts: -------------------------------------------------------------------------------- 1 | import { getStorageItem, setStorageItem } from './storage'; 2 | 3 | // Check if lastError is empty dict and clear it if not empty. 4 | async function clearLastError(): Promise { 5 | const lastError = await getStorageItem('lastError'); 6 | if (lastError && Object.keys(lastError).length !== 0) { 7 | await setStorageItem('lastError', {}); 8 | } 9 | } 10 | 11 | export async function loggedOut(): Promise { 12 | await Promise.all([ 13 | chrome.action.setPopup({ popup: 'popup.html' }), 14 | chrome.action.setBadgeText({ text: 'Login' }), 15 | chrome.action.setIcon({ 16 | path: { 17 | 16: '/icons/16/codeium_square_inactive.png', 18 | 32: '/icons/32/codeium_square_inactive.png', 19 | 48: '/icons/48/codeium_square_inactive.png', 20 | 128: '/icons/128/codeium_square_inactive.png', 21 | }, 22 | }), 23 | chrome.action.setTitle({ title: 'Codeium' }), 24 | clearLastError(), 25 | ]); 26 | } 27 | 28 | export async function loggedIn(): Promise { 29 | await Promise.all([ 30 | chrome.action.setPopup({ popup: 'logged_in_popup.html' }), 31 | chrome.action.setBadgeText({ text: '' }), 32 | chrome.action.setIcon({ 33 | path: { 34 | 16: '/icons/16/codeium_square_logo.png', 35 | 32: '/icons/32/codeium_square_logo.png', 36 | 48: '/icons/48/codeium_square_logo.png', 37 | 128: '/icons/128/codeium_square_logo.png', 38 | }, 39 | }), 40 | chrome.action.setTitle({ title: 'Codeium' }), 41 | clearLastError(), 42 | ]); 43 | } 44 | 45 | export async function unhealthy(message: string): Promise { 46 | // We don't set the badge text on purpose. 47 | await Promise.all([ 48 | chrome.action.setPopup({ popup: 'logged_in_popup.html' }), 49 | chrome.action.setIcon({ 50 | path: { 51 | 16: '/icons/16/codeium_square_error.png', 52 | 32: '/icons/32/codeium_square_error.png', 53 | 48: '/icons/48/codeium_square_error.png', 54 | 128: '/icons/128/codeium_square_error.png', 55 | }, 56 | }), 57 | chrome.action.setTitle({ title: `Codeium (error: ${message})` }), 58 | setStorageItem('lastError', { message: message }), 59 | ]); 60 | } 61 | -------------------------------------------------------------------------------- /src/storage.ts: -------------------------------------------------------------------------------- 1 | import { ValuesType } from 'utility-types'; 2 | 3 | import { PUBLIC_WEBSITE } from './urls'; 4 | 5 | export interface Storage { 6 | user?: { 7 | apiKey?: string; 8 | name?: string; 9 | userPortalUrl?: string; 10 | }; 11 | settings: Record; 12 | lastError?: { 13 | message?: string; 14 | }; 15 | portalUrl?: string; 16 | // regexes of domains to watch 17 | allowlist?: { 18 | // Defaults at the time of saving the setting. 19 | defaults: string[]; 20 | current: string[]; 21 | }; 22 | enterpriseDefaultModel?: string; 23 | jupyterlabKeybindingAccept?: string; 24 | jupyterlabKeybindingDismiss?: string; 25 | jupyterNotebookKeybindingAccept?: string; 26 | jupyterNotebookKeybindingDismiss?: string; 27 | jupyterDebounceMs?: number; 28 | } 29 | 30 | // In case the defaults change over time, reconcile the saved setting with the 31 | // new default allowlist. 32 | export function computeAllowlist( 33 | allowlist: 34 | | { 35 | defaults: string[]; 36 | current: string[]; 37 | } 38 | | undefined 39 | ): string[] { 40 | if (allowlist === undefined) { 41 | allowlist = { 42 | defaults: [], 43 | current: [], 44 | }; 45 | } 46 | for (const newDefault of defaultAllowlist) { 47 | if (!allowlist.defaults.includes(newDefault) && !allowlist.current.includes(newDefault)) { 48 | allowlist.current.push(newDefault); 49 | } 50 | } 51 | for (const oldDefault of allowlist.defaults) { 52 | if (!defaultAllowlist.includes(oldDefault) && allowlist.current.includes(oldDefault)) { 53 | allowlist.current.splice(allowlist.current.indexOf(oldDefault), 1); 54 | } 55 | } 56 | return allowlist.current; 57 | } 58 | 59 | export async function populateFromManagedStorage(): Promise { 60 | const managedStorageItems = chrome.storage.managed.get([ 61 | 'codeiumPortalUrl', 62 | 'codeiumEnterpriseDefaultModel', 63 | 'codeiumAllowlist', 64 | ]); 65 | void managedStorageItems.then((result) => { 66 | if (result.portalUrl !== undefined) { 67 | void setStorageItem('portalUrl', result.codeiumPortalUrl); 68 | } 69 | if (result.enterpriseDefaultModel !== undefined) { 70 | void setStorageItem('enterpriseDefaultModel', result.codeiumEnterpriseDefaultModel); 71 | } 72 | if (result.allowlist !== undefined) { 73 | const lst = result.codeiumAllowlist 74 | .split('\n') 75 | .map((x: string) => x.trim()) 76 | .filter((x: string) => x !== ''); 77 | void setStorageItem('allowlist', { defaults: defaultAllowlist, current: lst }); 78 | } 79 | }); 80 | } 81 | 82 | export function getStorageData(): Promise { 83 | return new Promise((resolve, reject) => { 84 | chrome.storage.sync.get(null, (result) => { 85 | if (chrome.runtime.lastError) { 86 | return reject(chrome.runtime.lastError); 87 | } 88 | 89 | return resolve(result as Storage); 90 | }); 91 | }); 92 | } 93 | 94 | export function setStorageData(data: Storage): Promise { 95 | return new Promise((resolve, reject) => { 96 | chrome.storage.sync.set(data, () => { 97 | if (chrome.runtime.lastError) { 98 | return reject(chrome.runtime.lastError); 99 | } 100 | 101 | return resolve(); 102 | }); 103 | }); 104 | } 105 | 106 | export function getStorageItem(key: Key): Promise { 107 | return new Promise((resolve, reject) => { 108 | chrome.storage.sync.get([key], (result) => { 109 | if (chrome.runtime.lastError) { 110 | return reject(chrome.runtime.lastError); 111 | } 112 | 113 | return resolve((result as Storage)[key]); 114 | }); 115 | }); 116 | } 117 | 118 | export function getStorageItems( 119 | keys: [...Key] 120 | ): Promise>> { 121 | return new Promise((resolve, reject) => { 122 | chrome.storage.sync.get(keys, (result) => { 123 | if (chrome.runtime.lastError) { 124 | return reject(chrome.runtime.lastError); 125 | } 126 | 127 | return resolve(result as Pick>); 128 | }); 129 | }); 130 | } 131 | 132 | export function setStorageItem( 133 | key: Key, 134 | value: Storage[Key] 135 | ): Promise { 136 | return new Promise((resolve, reject) => { 137 | chrome.storage.sync.set({ [key]: value }, () => { 138 | if (chrome.runtime.lastError) { 139 | return reject(chrome.runtime.lastError); 140 | } 141 | 142 | return resolve(); 143 | }); 144 | }); 145 | } 146 | 147 | export async function initializeStorageWithDefaults(defaults: Storage) { 148 | if (CODEIUM_ENTERPRISE) { 149 | await populateFromManagedStorage(); 150 | } 151 | const currentStorageData = await getStorageData(); 152 | const newStorageData = Object.assign({}, defaults, currentStorageData); 153 | await setStorageData(newStorageData); 154 | } 155 | 156 | export async function getGeneralPortalUrl(): Promise { 157 | const portalUrl = await getStorageItem('portalUrl'); 158 | if (portalUrl === undefined || portalUrl === '') { 159 | return undefined; 160 | } 161 | try { 162 | new URL(portalUrl); 163 | } catch (error) { 164 | console.log('Invalid portal URL:', portalUrl); 165 | return undefined; 166 | } 167 | return portalUrl; 168 | } 169 | 170 | // Note that this gets you the profile URL given the current portal URL, not the 171 | // specific profile URL of the logged in account. 172 | // TODO(prem): Deduplicate with Options.tsx/serviceWorker.ts. 173 | export async function getGeneralProfileUrl(): Promise { 174 | const portalUrl = await (async (): Promise => { 175 | const url = await getGeneralPortalUrl(); 176 | if (url === undefined) { 177 | if (CODEIUM_ENTERPRISE) { 178 | return undefined; 179 | } 180 | return PUBLIC_WEBSITE; 181 | } 182 | return url; 183 | })(); 184 | if (portalUrl === undefined) { 185 | return undefined; 186 | } 187 | return `${portalUrl}/profile`; 188 | } 189 | 190 | // default allowlist 191 | export const defaultAllowlist = [ 192 | /https:\/\/colab.research\.google\.com\/.*/, 193 | /https:\/\/(.*\.)?stackblitz\.com\/.*/, 194 | /https:\/\/(.*\.)?deepnote\.com\/.*/, 195 | /https:\/\/(.*\.)?(databricks\.com|azuredatabricks\.net)\/.*/, 196 | /https:\/\/(.*\.)?quadratichq\.com\/.*/, 197 | /https?:\/\/(.*\.)?jsfiddle\.net(\/.*)?/, 198 | /https:\/\/(.*\.)?codepen\.io(\/.*)?/, 199 | /https:\/\/(.*\.)?codeshare\.io(\/.*)?/, 200 | /https:\/\/console\.paperspace\.com\/.*\/notebook\/.*/, 201 | /https?:\/\/www\.codewars\.com(\/.*)?/, 202 | /https:\/\/(.*\.)?github\.com(\/.*)?/, 203 | /http:\/\/(localhost|127\.0\.0\.1):[0-9]+\/.*\.ipynb/, 204 | /https:\/\/(.*\.)?script.google.com(\/.*)?/, 205 | ].map((reg) => reg.source); 206 | -------------------------------------------------------------------------------- /src/urls.ts: -------------------------------------------------------------------------------- 1 | export const PUBLIC_WEBSITE = 'https://codeium.com'; 2 | export const PUBLIC_API_SERVER = 'https://server.codeium.com'; 3 | -------------------------------------------------------------------------------- /src/utf.ts: -------------------------------------------------------------------------------- 1 | function numUtf8BytesForCodePoint(codePointValue: number): number { 2 | if (codePointValue < 0x80) { 3 | return 1; 4 | } 5 | if (codePointValue < 0x800) { 6 | return 2; 7 | } 8 | if (codePointValue < 0x10000) { 9 | return 3; 10 | } 11 | return 4; 12 | } 13 | 14 | /** 15 | * Calculates for some prefix of the given text, how many bytes the UTF-8 16 | * representation would be. Undefined behavior if the number of code units 17 | * doesn't correspond to a valid UTF-8 sequence. 18 | * @param text - Text to examine. 19 | * @param numCodeUnits The number of code units to look at. 20 | * @returns The number of bytes. 21 | */ 22 | export function numCodeUnitsToNumUtf8Bytes(text: string, numCodeUnits?: number): number { 23 | if (numCodeUnits === 0) { 24 | return 0; 25 | } 26 | let curNumUtf8Bytes = 0; 27 | let curNumCodeUnits = 0; 28 | for (const codePoint of text) { 29 | curNumCodeUnits += codePoint.length; 30 | // TODO(prem): Is the ! safe here? 31 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 32 | curNumUtf8Bytes += numUtf8BytesForCodePoint(codePoint.codePointAt(0)!); 33 | if (numCodeUnits !== undefined && curNumCodeUnits >= numCodeUnits) { 34 | break; 35 | } 36 | } 37 | return curNumUtf8Bytes; 38 | } 39 | 40 | export function numUtf8BytesToNumCodeUnits(text: string, numUtf8Bytes?: number): number { 41 | if (numUtf8Bytes === 0) { 42 | return 0; 43 | } 44 | let curNumCodeUnits = 0; 45 | let curNumUtf8Bytes = 0; 46 | for (const codePoint of text) { 47 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 48 | curNumUtf8Bytes += numUtf8BytesForCodePoint(codePoint.codePointAt(0)!); 49 | curNumCodeUnits += codePoint.length; 50 | if (numUtf8Bytes !== undefined && curNumUtf8Bytes >= numUtf8Bytes) { 51 | break; 52 | } 53 | } 54 | return curNumCodeUnits; 55 | } 56 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function sleep(ms: number) { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | } 4 | -------------------------------------------------------------------------------- /static/codeium.css: -------------------------------------------------------------------------------- 1 | .monaco-editor .ghost-text-decoration { 2 | font-style: italic; 3 | } 4 | 5 | .monaco-editor .ghost-text { 6 | font-style: italic; 7 | } 8 | 9 | .codeium-ghost { 10 | font-style: italic; 11 | color: #888888; 12 | } 13 | 14 | /* Deepnote */ 15 | 16 | *[class*='-ghost-text-'] { 17 | font-style: italic; 18 | } 19 | 20 | .monaco-editor .suggest-preview-text .mtk1 { 21 | font-style: italic; 22 | } 23 | -------------------------------------------------------------------------------- /static/icons/128/codeium_square_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/128/codeium_square_error.png -------------------------------------------------------------------------------- /static/icons/128/codeium_square_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/128/codeium_square_inactive.png -------------------------------------------------------------------------------- /static/icons/128/codeium_square_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/128/codeium_square_logo.png -------------------------------------------------------------------------------- /static/icons/128x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/128x.png -------------------------------------------------------------------------------- /static/icons/16/codeium_square_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/16/codeium_square_error.png -------------------------------------------------------------------------------- /static/icons/16/codeium_square_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/16/codeium_square_inactive.png -------------------------------------------------------------------------------- /static/icons/16/codeium_square_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/16/codeium_square_logo.png -------------------------------------------------------------------------------- /static/icons/16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/16x.png -------------------------------------------------------------------------------- /static/icons/32/codeium_square_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/32/codeium_square_error.png -------------------------------------------------------------------------------- /static/icons/32/codeium_square_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/32/codeium_square_inactive.png -------------------------------------------------------------------------------- /static/icons/32/codeium_square_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/32/codeium_square_logo.png -------------------------------------------------------------------------------- /static/icons/32x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/32x.png -------------------------------------------------------------------------------- /static/icons/48/codeium_square_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/48/codeium_square_error.png -------------------------------------------------------------------------------- /static/icons/48/codeium_square_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/48/codeium_square_inactive.png -------------------------------------------------------------------------------- /static/icons/48/codeium_square_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/48/codeium_square_logo.png -------------------------------------------------------------------------------- /static/icons/48x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exafunction/codeium-chrome/22d153fb5d2ea96e94c4dca07763a261c3840dcf/static/icons/48x.png -------------------------------------------------------------------------------- /static/logged_in_popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Codeium

14 | 15 |

16 | 17 | 18 | 19 | 20 | 21 |

22 |
23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Codeium: AI Code Autocompletion on all IDEs", 3 | "description": "Your modern coding superpower. Get code completions in Colab and more.", 4 | "version": "1.26.3", 5 | "manifest_version": 3, 6 | "background": { 7 | "service_worker": "serviceWorker.js" 8 | }, 9 | "content_scripts": [ 10 | { 11 | "matches": [""], 12 | "css": ["codeium.css"], 13 | "js": ["contentScript.js"], 14 | "run_at": "document_start", 15 | "all_frames": true 16 | } 17 | ], 18 | "web_accessible_resources": [ 19 | { 20 | "resources": ["script.js"], 21 | "matches": [""] 22 | } 23 | ], 24 | "permissions": ["storage"], 25 | "options_ui": { 26 | "page": "options.html", 27 | "open_in_tab": false, 28 | "browser_style": true 29 | }, 30 | "action": { 31 | "default_title": "Codeium", 32 | "default_popup": "popup.html", 33 | "default_icon": { 34 | "16": "/icons/16/codeium_square_logo.png", 35 | "32": "/icons/32/codeium_square_logo.png", 36 | "48": "/icons/48/codeium_square_logo.png", 37 | "128": "/icons/128/codeium_square_logo.png" 38 | } 39 | }, 40 | "icons": { 41 | "16": "/icons/16/codeium_square_logo.png", 42 | "32": "/icons/32/codeium_square_logo.png", 43 | "48": "/icons/48/codeium_square_logo.png", 44 | "128": "/icons/128/codeium_square_logo.png" 45 | }, 46 | "externally_connectable": { 47 | "matches": [""] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /static/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Codeium options 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /static/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Codeium

14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 |

24 |
25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /styles/common.scss: -------------------------------------------------------------------------------- 1 | @import 'normalize.css'; 2 | 3 | body { 4 | background-color: #000000; 5 | color: #ffffff; 6 | } 7 | 8 | a { 9 | color: rgb(148 255 219); 10 | } 11 | 12 | .input-group { 13 | display: flex; 14 | 15 | margin: 1rem 0; 16 | 17 | flex-direction: row; 18 | gap: 1rem; 19 | 20 | align-items: center; 21 | } 22 | -------------------------------------------------------------------------------- /styles/options.scss: -------------------------------------------------------------------------------- 1 | @import 'common.scss'; 2 | 3 | .container { 4 | min-height: 100vh; 5 | 6 | display: flex; 7 | 8 | flex-direction: column; 9 | 10 | align-items: center; 11 | justify-content: center; 12 | } 13 | -------------------------------------------------------------------------------- /styles/popup.scss: -------------------------------------------------------------------------------- 1 | @import 'common.scss'; 2 | 3 | .container { 4 | min-height: 200px; 5 | min-width: 300px; 6 | 7 | padding: 2rem; 8 | 9 | display: flex; 10 | 11 | flex-direction: column; 12 | 13 | align-items: center; 14 | justify-content: center; 15 | } 16 | 17 | #username { 18 | text-align: center; 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "target": "es6", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "allowSyntheticDefaultImports": true, 10 | "lib": ["ES2020", "DOM", "DOM.Iterable"] 11 | }, 12 | "include": ["src/**/*", "proto/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const DotenvPlugin = require('dotenv-webpack'); 4 | const ESLintPlugin = require('eslint-webpack-plugin'); 5 | const CopyPlugin = require('copy-webpack-plugin'); 6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 7 | const { DefinePlugin } = require('webpack'); 8 | 9 | /**@type {(env: any) => import('webpack').Configuration}*/ 10 | module.exports = (env) => ({ 11 | entry: { 12 | serviceWorker: './src/serviceWorker.ts', 13 | contentScript: './src/contentScript.ts', 14 | popup: './src/popup.ts', 15 | options: './src/options.tsx', 16 | // This script is loaded in contentScript.ts. 17 | script: './src/script.ts', 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.(js|ts)x?$/, 23 | use: { 24 | loader: 'babel-loader', 25 | options: { 26 | presets: ['@babel/preset-react', '@babel/preset-env', '@babel/preset-typescript'], 27 | }, 28 | }, 29 | exclude: /node_modules/, 30 | }, 31 | { 32 | test: /\.(scss|css)$/, 33 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 34 | }, 35 | { 36 | test: /\.svg$/, 37 | loader: 'svg-inline-loader', 38 | }, 39 | ], 40 | }, 41 | resolve: { 42 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 43 | }, 44 | output: { 45 | filename: '[name].js', 46 | path: path.resolve(__dirname, env.enterprise ? 'dist_enterprise' : 'dist'), 47 | clean: true, 48 | }, 49 | externals: { 'monaco-editor': 'monaco' }, 50 | plugins: [ 51 | new DotenvPlugin(), 52 | new ESLintPlugin({ 53 | extensions: ['js', 'ts'], 54 | overrideConfigFile: path.resolve(__dirname, '.eslintrc'), 55 | }), 56 | new MiniCssExtractPlugin({ 57 | filename: 'styles/[name].css', 58 | }), 59 | new CopyPlugin({ 60 | patterns: [ 61 | { 62 | from: 'static', 63 | transform: (content, resourcePath) => { 64 | if (!env.enterprise) { 65 | return content; 66 | } 67 | if (!resourcePath.endsWith('manifest.json')) { 68 | return content; 69 | } 70 | const manifest = JSON.parse(content.toString()); 71 | manifest.name = 'Codeium Enterprise'; 72 | return JSON.stringify(manifest); 73 | }, 74 | }, 75 | ], 76 | }), 77 | new DefinePlugin({ 78 | CODEIUM_ENTERPRISE: JSON.stringify(env.enterprise), 79 | }), 80 | ], 81 | experiments: { 82 | topLevelAwait: true, 83 | }, 84 | }); 85 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = (env) => 5 | merge(common(env), { 6 | mode: 'development', 7 | devtool: 'inline-source-map', 8 | devServer: { 9 | static: './dist', 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = (env) => 5 | merge(common(env), { 6 | mode: 'production', 7 | }); 8 | --------------------------------------------------------------------------------