├── .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 |
3 |
4 |
5 | ---
6 |
7 | [](https://discord.gg/3XFf78nAx5)
8 | [](https://twitter.com/intent/follow?screen_name=codeiumdev)
9 | 
10 | [](https://docs.codeium.com)
11 |
12 | [](https://marketplace.visualstudio.com/items?itemName=Codeium.codeium)
13 | [](https://plugins.jetbrains.com/plugin/20540-codeium/)
14 | [](https://open-vsx.org/extension/Codeium/codeium)
15 | [](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 |
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 |
16 | Set portal URL in options to log in.
17 |
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 |
--------------------------------------------------------------------------------