From 30266079e9f41d1923d399e8809b2e0805ade33d Mon Sep 17 00:00:00 2001 From: Copybara Bot Date: Fri, 14 Jun 2024 13:53:18 -0700 Subject: [PATCH 1/2] Project import generated by Copybara. GitOrigin-RevId: 840f194f2bc759966cfed83e31127be32230a505 --- buf.gen.yaml | 4 +- build.sh | 2 +- package.json | 10 ++-- pnpm-lock.yaml | 80 ++++++++++++------------- src/codemirror.ts | 45 ++++++++++++-- src/codemirrorInject.ts | 1 + src/codemirrorLanguages.ts | 4 +- src/common.ts | 11 +++- src/component/Options.tsx | 119 +++++++++++++++++++++++++++++++++++++ src/contentScript.ts | 14 +++-- src/jupyterInject.ts | 69 +++++++++++++++------ src/jupyterlabPlugin.ts | 112 ++++++++++++++++++++++++++++++---- src/notebook.ts | 22 +++++-- src/script.ts | 57 +++++++++++------- src/serviceWorker.ts | 44 ++++++++++++-- src/storage.ts | 19 ++++++ static/manifest.json | 2 +- 17 files changed, 493 insertions(+), 122 deletions(-) diff --git a/buf.gen.yaml b/buf.gen.yaml index 43a5854..854e4dd 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -2,12 +2,12 @@ # For details, see https://docs.buf.build/configuration/v1/buf-gen-yaml version: v1 plugins: - - plugin: buf.build/bufbuild/es:v1.4.2 + - plugin: buf.build/bufbuild/es:v1.9.0 out: proto opt: - target=ts - import_extension=none - - plugin: buf.build/connectrpc/es:v1.1.3 + - plugin: buf.build/connectrpc/es:v1.4.0 out: proto opt: - target=ts diff --git a/build.sh b/build.sh index 7fc6942..6c9fb0d 100755 --- a/build.sh +++ b/build.sh @@ -14,7 +14,7 @@ cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" cd ../../.. && git clean -ffdx -e local.bazelrc && cd - pnpm install -# If the first arg is public, use npm run build +# If the first arg is public, use pnpm run build if [[ "$1" == "public" ]]; then pnpm run build else diff --git a/package.json b/package.json index 63ecb89..2942aff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeium-chrome", - "version": "1.8.25", + "version": "1.8.61", "description": "", "license": "MIT", "scripts": { @@ -28,9 +28,9 @@ "browserslist": "last 10 Chrome versions", "dependencies": { "@babel/runtime": "^7.18.6", - "@bufbuild/protobuf": "1.4.2", - "@connectrpc/connect": "1.1.3", - "@connectrpc/connect-web": "1.1.3", + "@bufbuild/protobuf": "1.9.0", + "@connectrpc/connect": "1.4.0", + "@connectrpc/connect-web": "1.4.0", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@mui/icons-material": "^5.11.16", @@ -47,7 +47,7 @@ "@babel/preset-env": "^7.21.4", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.18.6", - "@bufbuild/buf": "1.28.1", + "@bufbuild/buf": "1.30.1", "@jupyterlab/application": "^3.5.2", "@jupyterlab/codeeditor": "^3.5.2", "@jupyterlab/codemirror": "^3.5.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9fdccdb..87fdfbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,14 +17,14 @@ dependencies: specifier: ^7.18.6 version: 7.21.0 '@bufbuild/protobuf': - specifier: 1.4.2 - version: 1.4.2 + specifier: 1.9.0 + version: 1.9.0 '@connectrpc/connect': - specifier: 1.1.3 - version: 1.1.3(@bufbuild/protobuf@1.4.2) + specifier: 1.4.0 + version: 1.4.0(@bufbuild/protobuf@1.9.0) '@connectrpc/connect-web': - specifier: 1.1.3 - version: 1.1.3(@bufbuild/protobuf@1.4.2)(@connectrpc/connect@1.1.3) + specifier: 1.4.0 + version: 1.4.0(@bufbuild/protobuf@1.9.0)(@connectrpc/connect@1.4.0) '@emotion/react': specifier: ^11.10.6 version: 11.10.6(@types/react@17.0.52)(react@17.0.2) @@ -70,8 +70,8 @@ devDependencies: specifier: ^7.18.6 version: 7.18.6(@babel/core@7.21.4) '@bufbuild/buf': - specifier: 1.28.1 - version: 1.28.1 + specifier: 1.30.1 + version: 1.30.1 '@jupyterlab/application': specifier: ^3.5.2 version: 3.5.2(crypto@1.0.1)(react@17.0.2)(yjs@13.6.8) @@ -1540,8 +1540,8 @@ packages: tslib: 2.3.1 dev: true - /@bufbuild/buf-darwin-arm64@1.28.1: - resolution: {integrity: sha512-nAyvwKkcd8qQTExCZo5MtSRhXLK7e3vzKFKHjXfkveRakSUST2HFlFZAHfErZimN4wBrPTN0V0hNRU8PPjkMpQ==} + /@bufbuild/buf-darwin-arm64@1.30.1: + resolution: {integrity: sha512-FRgf+x4V4s9Z1wH2xHdP8+1AYtil1GCmMjzKf/4AQ+eaUpoLfipSIsVYiBrnpcRxEPe9UMVzwNjKtPak/szwPw==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] @@ -1549,8 +1549,8 @@ packages: dev: true optional: true - /@bufbuild/buf-darwin-x64@1.28.1: - resolution: {integrity: sha512-b0eT3xd3vX5a5lWAbo5h7FPuf9MsOJI4I39qs4TZnrlZ8BOuPfqzwzijiFf9UCwaX2vR1NQXexIoQ80Ci+fCHw==} + /@bufbuild/buf-darwin-x64@1.30.1: + resolution: {integrity: sha512-kE0ne45zE7lSdv9WxPVhapwu627WMbWmWCzqSxzYr8sWDLqiAuw+XvO9/mHGdPWcMhV4lMX6tutitd9PPVxK8A==} engines: {node: '>=12'} cpu: [x64] os: [darwin] @@ -1558,8 +1558,8 @@ packages: dev: true optional: true - /@bufbuild/buf-linux-aarch64@1.28.1: - resolution: {integrity: sha512-p5h9bZCVLMh8No9/7k7ulXzsFx5P7Lu6DiUMjSJ6aBXPMYo6Xl7r/6L2cQkpsZ53HMtIxCgMYS9a7zoS4K8wIw==} + /@bufbuild/buf-linux-aarch64@1.30.1: + resolution: {integrity: sha512-kVV9Sl0GwZiQkMOXJiuwuU+gIHe6AWcYBMRMmuW55sY0ePZNXBmRGt4k5W4ijy98O6pnY3ao+n9ne0KwiD9MVA==} engines: {node: '>=12'} cpu: [arm64] os: [linux] @@ -1567,8 +1567,8 @@ packages: dev: true optional: true - /@bufbuild/buf-linux-x64@1.28.1: - resolution: {integrity: sha512-fVJ3DiRigIso06jgEl+JNp59Y5t2pxDHd10d3SA4r+14sXbZ2J7Gy/wBqVXPry4x/jW567KKlvmhg7M5ZBgCQQ==} + /@bufbuild/buf-linux-x64@1.30.1: + resolution: {integrity: sha512-RacDbQJYNwqRlMESa/rLHprfUVa8Wu1/cmcqS29Fyt/cGzs0G8sNcQzQ87HYFIS9cSlSPl6vWL0x8JqQRp68lQ==} engines: {node: '>=12'} cpu: [x64] os: [linux] @@ -1576,8 +1576,8 @@ packages: dev: true optional: true - /@bufbuild/buf-win32-arm64@1.28.1: - resolution: {integrity: sha512-KJiRJpugQRK/jXC46Xjlb68UydWhCZj2jHdWLIwNtgXd1WTJ3LngChZV7Y6pPK08pwBAVz0JYeVbD5IlTCD4TQ==} + /@bufbuild/buf-win32-arm64@1.30.1: + resolution: {integrity: sha512-ndp/qb5M6yrSzcnMI0j4jjAuDKa7zHBFc187FwyDb3v63rvyQeYqncHb0leT5ZWqfNggJT4vXIH6QnH82PfDQw==} engines: {node: '>=12'} cpu: [arm64] os: [win32] @@ -1585,8 +1585,8 @@ packages: dev: true optional: true - /@bufbuild/buf-win32-x64@1.28.1: - resolution: {integrity: sha512-vMnc+7OVCkmlRWQsgYHgUqiBPRIjD8XeoRyApJ07YZzGs7DkRH4LhvmacJbLd3wORylbn6gLz3pQa8J/M61mzg==} + /@bufbuild/buf-win32-x64@1.30.1: + resolution: {integrity: sha512-1kmIY6oKLKZ4zIQVNG60GRDp+vKSZdaim7wRejOtgEDuWXhIuErlnGbpstypU8FO+OV3SeFUJNOJ8tLOYd3PvQ==} engines: {node: '>=12'} cpu: [x64] os: [win32] @@ -1594,40 +1594,40 @@ packages: dev: true optional: true - /@bufbuild/buf@1.28.1: - resolution: {integrity: sha512-WRDagrf0uBjfV9s5eyrSPJDcdI4A5Q7JMCA4aMrHRR8fo/TTjniDBjJprszhaguqsDkn/LS4QIu92HVFZCrl9A==} + /@bufbuild/buf@1.30.1: + resolution: {integrity: sha512-9VVvrXBCWUiH8ToccqDfPRuTiPXSbHmSkL8XPlMpUhpJIlm01m4/Vzbc5FJL1yuk3e1rdBGCF6I9Obs9NsILzg==} engines: {node: '>=12'} hasBin: true requiresBuild: true optionalDependencies: - '@bufbuild/buf-darwin-arm64': 1.28.1 - '@bufbuild/buf-darwin-x64': 1.28.1 - '@bufbuild/buf-linux-aarch64': 1.28.1 - '@bufbuild/buf-linux-x64': 1.28.1 - '@bufbuild/buf-win32-arm64': 1.28.1 - '@bufbuild/buf-win32-x64': 1.28.1 + '@bufbuild/buf-darwin-arm64': 1.30.1 + '@bufbuild/buf-darwin-x64': 1.30.1 + '@bufbuild/buf-linux-aarch64': 1.30.1 + '@bufbuild/buf-linux-x64': 1.30.1 + '@bufbuild/buf-win32-arm64': 1.30.1 + '@bufbuild/buf-win32-x64': 1.30.1 dev: true - /@bufbuild/protobuf@1.4.2: - resolution: {integrity: sha512-JyEH8Z+OD5Sc2opSg86qMHn1EM1Sa+zj/Tc0ovxdwk56ByVNONJSabuCUbLQp+eKN3rWNfrho0X+3SEqEPXIow==} + /@bufbuild/protobuf@1.9.0: + resolution: {integrity: sha512-W7gp8Q/v1NlCZLsv8pQ3Y0uCu/SHgXOVFK+eUluUKWXmsb6VHkpNx0apdOWWcDbB9sJoKeP8uPrjmehJz6xETQ==} dev: false - /@connectrpc/connect-web@1.1.3(@bufbuild/protobuf@1.4.2)(@connectrpc/connect@1.1.3): - resolution: {integrity: sha512-WfShOZt91duJngqivYF4wJFRbeRa4bF/fPMfDVN0MAYSX3VuaTMn8o9qgKN7tsg2H2ZClyOVQwMkZx6IdcP7Zw==} + /@connectrpc/connect-web@1.4.0(@bufbuild/protobuf@1.9.0)(@connectrpc/connect@1.4.0): + resolution: {integrity: sha512-13aO4psFbbm7rdOFGV0De2Za64DY/acMspgloDlcOKzLPPs0yZkhp1OOzAQeiAIr7BM/VOHIA3p8mF0inxCYTA==} peerDependencies: - '@bufbuild/protobuf': ^1.3.3 - '@connectrpc/connect': 1.1.3 + '@bufbuild/protobuf': ^1.4.2 + '@connectrpc/connect': 1.4.0 dependencies: - '@bufbuild/protobuf': 1.4.2 - '@connectrpc/connect': 1.1.3(@bufbuild/protobuf@1.4.2) + '@bufbuild/protobuf': 1.9.0 + '@connectrpc/connect': 1.4.0(@bufbuild/protobuf@1.9.0) dev: false - /@connectrpc/connect@1.1.3(@bufbuild/protobuf@1.4.2): - resolution: {integrity: sha512-AXkbsLQe2Nm7VuoN5nqp05GEb9mPa/f5oFzDqTbHME4i8TghTrlY03uefbhuAq4wjsnfDnmuxHZvn6ndlgXmbg==} + /@connectrpc/connect@1.4.0(@bufbuild/protobuf@1.9.0): + resolution: {integrity: sha512-vZeOkKaAjyV4+RH3+rJZIfDFJAfr+7fyYr6sLDKbYX3uuTVszhFe9/YKf5DNqrDb5cKdKVlYkGn6DTDqMitAnA==} peerDependencies: - '@bufbuild/protobuf': ^1.3.3 + '@bufbuild/protobuf': ^1.4.2 dependencies: - '@bufbuild/protobuf': 1.4.2 + '@bufbuild/protobuf': 1.9.0 dev: false /@discoveryjs/json-ext@0.5.7: diff --git a/src/codemirror.ts b/src/codemirror.ts index b10db9c..45f1b5d 100644 --- a/src/codemirror.ts +++ b/src/codemirror.ts @@ -14,11 +14,13 @@ import { function computeTextAndOffsetsForCodeMirror( textModels: CodeMirror.Doc[], - currentTextModel: CodeMirror.Doc + currentTextModel: CodeMirror.Doc, + currentTextModelWithOutput: CodeMirror.Doc | undefined ): TextAndOffsets { return computeTextAndOffsets({ textModels, currentTextModel, + currentTextModelWithOutput: currentTextModelWithOutput, utf16CodeUnitOffset: currentTextModel.indexFromPos(currentTextModel.getCursor()), getText: (model) => model.getValue(), getLanguage: (model) => language(model, undefined), @@ -108,6 +110,7 @@ export class CodeMirrorManager { async triggerCompletion( textModels: CodeMirror.Doc[], currentTextModel: CodeMirror.Doc, + currentTextModelWithOutput: CodeMirror.Doc | undefined, editorOptions: EditorOptions, relativePath: string | undefined, createDisposables: (() => IDisposable[]) | undefined @@ -115,7 +118,8 @@ export class CodeMirrorManager { const cursor = currentTextModel.getCursor(); const { text, utf8ByteOffset, additionalUtf8ByteOffset } = computeTextAndOffsetsForCodeMirror( textModels, - currentTextModel + currentTextModel, + currentTextModelWithOutput ); const numUtf8Bytes = additionalUtf8ByteOffset + utf8ByteOffset; const request = new GetCompletionsRequest({ @@ -299,7 +303,8 @@ export class CodeMirrorManager { beforeMainKeyHandler( doc: CodeMirror.Doc, event: KeyboardEvent, - alsoHandle: { tab: boolean; escape: boolean } + alsoHandle: { tab: boolean; escape: boolean }, + tabKey: string = 'Tab' ): { consumeEvent: boolean | undefined; forceTriggerCompletion: boolean } { let forceTriggerCompletion = false; if (event.ctrlKey) { @@ -330,13 +335,23 @@ export class CodeMirrorManager { this.clearCompletion(`key: ${event.key}`); return { consumeEvent: false, forceTriggerCompletion }; } + // Shift-tab in jupyter notebooks shows documentation. + if (event.key === 'Tab' && event.shiftKey) { + return { consumeEvent: false, forceTriggerCompletion }; + } if (!event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey) { - if (alsoHandle.tab && event.key === 'Tab' && this.acceptCompletion()) { + if (alsoHandle.tab && event.key === tabKey && this.acceptCompletion()) { return { consumeEvent: true, forceTriggerCompletion }; } if (alsoHandle.escape && event.key === 'Escape' && this.clearCompletion('user dismissed')) { return { consumeEvent: true, forceTriggerCompletion }; } + // Special case if we are in jupyter notebooks and the tab key has been rebinded. + // We do not want to consume the default keybinding, because it triggers the default + // jupyter completion. + if (alsoHandle.tab && tabKey != 'Tab') { + return { consumeEvent: false, forceTriggerCompletion }; + } } const cursor = doc.getCursor(); const characterBeforeCursor = @@ -374,6 +389,28 @@ export class CodeMirrorManager { div.addEventListener('mousedown', () => { this.clearCompletion('mousedown'); }); + const mutationObserver = new MutationObserver(() => { + // Check for jupyterlab-vim command mode. + if (div.classList.contains('cm-fat-cursor')) { + this.clearCompletion('vim'); + } + }); + mutationObserver.observe(div, { + attributes: true, + attributeFilter: ['class'], + }); + const completer = document.body.querySelector('.jp-Completer'); + if (completer !== null) { + const completerMutationObserver = new MutationObserver(() => { + if (!completer?.classList.contains('lm-mod-hidden')) { + this.clearCompletion('completer'); + } + }); + completerMutationObserver.observe(completer, { + attributes: true, + attributeFilter: ['class'], + }); + } }; } } diff --git a/src/codemirrorInject.ts b/src/codemirrorInject.ts index 2524940..a676936 100644 --- a/src/codemirrorInject.ts +++ b/src/codemirrorInject.ts @@ -69,6 +69,7 @@ export class CodeMirrorState { await this.codeMirrorManager.triggerCompletion( this.docs, editor.getDoc(), + undefined, new EditorOptions({ tabSize: BigInt(editor.getOption('tabSize') ?? 4), insertSpaces: !(editor.getOption('indentWithTabs') ?? false), diff --git a/src/codemirrorLanguages.ts b/src/codemirrorLanguages.ts index 2bbffd1..ae65a85 100644 --- a/src/codemirrorLanguages.ts +++ b/src/codemirrorLanguages.ts @@ -29,6 +29,8 @@ const MIME_MAP = new Map([ ['text/typescript-jsx', Language.TSX], // mode: mllike ['text/x-ocaml', Language.OCAML], + // Jupyterlab specific + ['text/x-ipython', Language.PYTHON], ]); const MODE_MAP = new Map([ @@ -95,7 +97,7 @@ export function language(doc: CodeMirror.Doc, path: string | undefined): Languag } } } - const mime = doc.getEditor()?.getOption('mode'); + const mime = doc.getEditor()?.getOption('mode') ?? doc.modeOption; if (typeof mime === 'string') { const language = MIME_MAP.get(mime); if (language !== undefined) { diff --git a/src/common.ts b/src/common.ts index 61c139a..d4bb3fa 100644 --- a/src/common.ts +++ b/src/common.ts @@ -13,7 +13,7 @@ import { } from '../proto/exa/language_server_pb/language_server_pb'; const EXTENSION_NAME = 'chrome'; -const EXTENSION_VERSION = '1.8.25'; +const EXTENSION_VERSION = '1.8.61'; export const CODEIUM_DEBUG = false; @@ -22,6 +22,15 @@ export interface ClientSettings { defaultModel?: string; } +export interface JupyterLabKeyBindings { + accept: string; + dismiss: string; +} + +export interface JupyterNotebookKeyBindings { + accept: string; +} + async function getClientSettings(): Promise { const storageItems = await getStorageItems(['user', 'enterpriseDefaultModel']); return { diff --git a/src/component/Options.tsx b/src/component/Options.tsx index 9684fb4..490b4b3 100644 --- a/src/component/Options.tsx +++ b/src/component/Options.tsx @@ -148,6 +148,14 @@ const Options = () => { const [portalUrlText, setPortalUrlText] = useState(''); const modelRef = createRef(); const [modelText, setModelText] = useState(''); + const jupyterlabKeybindingAcceptRef = createRef(); + const [jupyterlabKeybindingAcceptText, setJupyterlabKeybindingAcceptText] = useState(''); + const jupyterlabKeybindingDismissRef = createRef(); + const [jupyterlabKeybindingDismissText, setJupyterlabKeybindingDismissText] = useState(''); + const jupyterNotebookKeybindingAcceptRef = createRef(); + const [jupyterNotebookKeybindingAcceptText, setJupyterNotebookKeybindingAcceptText] = + useState(''); + useEffect(() => { (async () => { setPortalUrlText((await getStorageItem('portalUrl')) ?? ''); @@ -159,6 +167,27 @@ const Options = () => { })().catch((e) => { console.error(e); }); + (async () => { + setJupyterlabKeybindingAcceptText( + (await getStorageItem('jupyterlabKeybindingAccept')) ?? 'Tab' + ); + })().catch((e) => { + console.error(e); + }); + (async () => { + setJupyterlabKeybindingDismissText( + (await getStorageItem('jupyterlabKeybindingDismiss')) ?? 'Escape' + ); + })().catch((e) => { + console.error(e); + }); + (async () => { + setJupyterNotebookKeybindingAcceptText( + (await getStorageItem('jupyterNotebookKeybindingAccept')) ?? 'Tab' + ); + })().catch((e) => { + console.error(e); + }); }, []); // TODO(prem): Deduplicate with serviceWorker.ts/storage.ts. const resolvedPortalUrl = useMemo(() => { @@ -286,6 +315,96 @@ const Options = () => { + + + Jupyterlab settings + + A single keystroke is supported. The syntax is described{' '} + + here + + + . + + setJupyterlabKeybindingAcceptText(e.target.value)} + /> + + + + setJupyterlabKeybindingDismissText(e.target.value)} + /> + + + + + + Jupyter Notebook settings + setJupyterNotebookKeybindingAcceptText(e.target.value)} + /> + + + + ); }; diff --git a/src/contentScript.ts b/src/contentScript.ts index 6e4c358..db50dfa 100644 --- a/src/contentScript.ts +++ b/src/contentScript.ts @@ -1,6 +1,8 @@ -const s = document.createElement('script'); -s.src = chrome.runtime.getURL('script.js?') + new URLSearchParams({ id: chrome.runtime.id }); -s.onload = function () { - (this as HTMLScriptElement).remove(); -}; -(document.head || document.documentElement).prepend(s); +if (document.contentType === 'text/html') { + const s = document.createElement('script'); + s.src = chrome.runtime.getURL('script.js?') + new URLSearchParams({ id: chrome.runtime.id }); + s.onload = function () { + (this as HTMLScriptElement).remove(); + }; + (document.head || document.documentElement).prepend(s); +} diff --git a/src/jupyterInject.ts b/src/jupyterInject.ts index 09f75c8..57e6b18 100644 --- a/src/jupyterInject.ts +++ b/src/jupyterInject.ts @@ -1,6 +1,7 @@ import type CodeMirror from 'codemirror'; import { CodeMirrorManager } from './codemirror'; +import { JupyterNotebookKeyBindings } from './common'; import { EditorOptions } from '../proto/exa/codeium_common_pb/codeium_common_pb'; declare class Cell { @@ -18,6 +19,7 @@ declare class Cell { data?: { 'text/plain': string; }; + text?: string; }[]; }; } @@ -50,18 +52,26 @@ interface Jupyter { class JupyterState { jupyter: Jupyter; codeMirrorManager: CodeMirrorManager; + keybindings: JupyterNotebookKeyBindings; - constructor(extensionId: string, jupyter: Jupyter) { + constructor(extensionId: string, jupyter: Jupyter, keybindings: JupyterNotebookKeyBindings) { this.jupyter = jupyter; this.codeMirrorManager = new CodeMirrorManager(extensionId, { ideName: 'jupyter_notebook', ideVersion: jupyter.version, }); + this.keybindings = keybindings; } patchCellKeyEvent() { - const beforeMainHandler = (doc: CodeMirror.Doc, event: KeyboardEvent) => - this.codeMirrorManager.beforeMainKeyHandler(doc, event, { tab: true, escape: false }); + const beforeMainHandler = (doc: CodeMirror.Doc, event: KeyboardEvent) => { + return this.codeMirrorManager.beforeMainKeyHandler( + doc, + event, + { tab: true, escape: false }, + this.keybindings.accept + ); + }; const replaceOriginalHandler = ( handler: (this: Cell, editor: CodeMirror.Editor, event: KeyboardEvent) => void ) => { @@ -89,24 +99,44 @@ class JupyterState { const textModels = []; const editableCells = [...this.notebook.get_cells()]; + let currentModelWithOutput; for (const cell of editableCells) { + let outputText = ''; + if (cell.output_area !== undefined && cell.output_area.outputs.length > 0) { + const output = cell.output_area.outputs[0]; + if ( + output.output_type === 'execute_result' && + output.data !== undefined && + output.data['text/plain'] !== undefined + ) { + outputText = output.data['text/plain']; + } else if ( + output.output_type === 'stream' && + output.name === 'stdout' && + output.text !== undefined + ) { + outputText = output.text; + } + + const lines = outputText.split('\n'); + if (lines.length > 10) { + lines.length = 10; + outputText = lines.join('\n'); + } + if (outputText.length > 500) { + outputText = outputText.slice(0, 500); + } + } + outputText = outputText ? '\nOUTPUT:\n' + outputText : ''; if (cell.code_mirror.getDoc() === doc) { - // TODO: make this keep track of the current cell's output. textModels.push(doc); + currentModelWithOutput = cell.code_mirror.getDoc().copy(false); + currentModelWithOutput.setValue(cell.get_text() + outputText); } else { const docCopy = cell.code_mirror.getDoc().copy(false); let docText = docCopy.getValue(); - if (cell.output_area !== undefined && cell.output_area.outputs.length > 0) { - const output = cell.output_area.outputs[0]; - if ( - output.output_type === 'execute_result' && - output.data !== undefined && - output.data['text/plain'] !== undefined - ) { - docText += '\nOUTPUT:\n' + output.data['text/plain']; - docCopy.setValue(docText); - } - } + docText += outputText; + docCopy.setValue(docText); textModels.push(docCopy); } } @@ -120,6 +150,7 @@ class JupyterState { await codeMirrorManager.triggerCompletion( textModels, this.code_mirror.getDoc(), + currentModelWithOutput, new EditorOptions({ tabSize: BigInt(editor.getOption('tabSize') ?? 4), insertSpaces: !(editor.getOption('indentWithTabs') ?? false), @@ -154,8 +185,12 @@ class JupyterState { } } -export function inject(extensionId: string, jupyter: Jupyter): JupyterState { - const jupyterState = new JupyterState(extensionId, jupyter); +export function inject( + extensionId: string, + jupyter: Jupyter, + keybindings: JupyterNotebookKeyBindings +): JupyterState { + const jupyterState = new JupyterState(extensionId, jupyter, keybindings); jupyterState.patchCellKeyEvent(); jupyterState.patchShortcutManagerHandler(); return jupyterState; diff --git a/src/jupyterlabPlugin.ts b/src/jupyterlabPlugin.ts index a38bac3..cc5cdf1 100644 --- a/src/jupyterlabPlugin.ts +++ b/src/jupyterlabPlugin.ts @@ -9,11 +9,40 @@ import { type Widget } from '@lumino/widgets'; import type CodeMirror from 'codemirror'; import { CodeMirrorManager } from './codemirror'; +import type { JupyterLabKeyBindings } from './common'; import { EditorOptions } from '../proto/exa/codeium_common_pb/codeium_common_pb'; const COMMAND_ACCEPT = 'codeium:accept-completion'; const COMMAND_DISMISS = 'codeium:dismiss-completion'; +declare class CellJSON { + cell_type: 'raw' | 'markdown' | 'code'; + source: string; + outputs: { + // Currently, we only look at execute_result + output_type: 'execute_result' | 'error' | 'stream' | 'display_data'; + name?: string; + data?: { + 'text/html': string; + 'text/plain': string; + }; + text?: string; + }[]; +} + +async function getKeybindings(extensionId: string): Promise { + const allowed = await new Promise((resolve) => { + chrome.runtime.sendMessage( + extensionId, + { type: 'jupyterlab' }, + (response: JupyterLabKeyBindings) => { + resolve(response); + } + ); + }); + return allowed; +} + class CodeiumPlugin { app: JupyterFrontEnd; notebookTracker: INotebookTracker; @@ -24,6 +53,7 @@ class CodeiumPlugin { nonNotebookWidget = new Set(); codeMirrorManager: CodeMirrorManager; + keybindings: Promise; constructor( readonly extensionId: string, @@ -72,6 +102,7 @@ class CodeiumPlugin { this.nonNotebookWidget.add(widget.id); widget.disposed.connect(this.removeNonNotebookWidget, this); }, this); + this.keybindings = getKeybindings(extensionId); } removeNonNotebookWidget(w: Widget) { @@ -94,6 +125,7 @@ class CodeiumPlugin { // We need to run the rest of the code after the normal DOM handler. // TODO(prem): Does this need debouncing? setTimeout(async () => { + const keybindings = await this.keybindings; if (!forceTriggerCompletion) { const newString = codeMirrorEditor.doc.getValue(); if (newString === oldString) { @@ -106,11 +138,57 @@ class CodeiumPlugin { const widget = isNotebook ? this.notebookTracker.currentWidget : this.editorTracker.currentWidget; + let currentTextModelWithOutput = undefined; if (isNotebook) { const cells = this.notebookTracker.currentWidget?.content.widgets; if (cells !== undefined) { for (const cell of cells) { - textModels.push((cell.editor as CodeMirrorEditor).doc); + const doc = (cell.editor as CodeMirrorEditor).doc; + const cellJSON = cell.model.toJSON() as CellJSON; + if (cellJSON.outputs !== undefined && cellJSON.outputs.length > 0) { + const isCurrentCell = cell === this.notebookTracker.currentWidget?.content.activeCell; + const cellText = cellJSON.source; + let outputText = ''; + + for (const output of cellJSON.outputs) { + if (output.output_type === 'execute_result' && output.data !== undefined) { + const data = output.data; + if (data['text/plain'] !== undefined) { + outputText = output.data['text/plain']; + } else if (data['text/html'] !== undefined) { + outputText = output.data['text/html']; + } + } + if ( + output.output_type === 'stream' && + output.name === 'stdout' && + output.text !== undefined + ) { + outputText = output.text; + } + } + + // Limit output text to 10 lines and 500 characters + // Add the OUTPUT: prefix if it exists + outputText = outputText + .split('\n') + .slice(0, 10) + .map((line) => line.slice(0, 500)) + .join('\n'); + outputText = outputText ? '\nOUTPUT:\n' + outputText : ''; + + const docCopy = doc.copy(false); + docCopy.setValue(cellText + outputText); + + if (isCurrentCell) { + currentTextModelWithOutput = docCopy; + textModels.push(doc); + } else { + textModels.push(docCopy); + } + } else { + textModels.push(doc); + } } } } @@ -119,23 +197,31 @@ class CodeiumPlugin { await this.codeMirrorManager.triggerCompletion( textModels, currentTextModel, + currentTextModelWithOutput, new EditorOptions({ tabSize: BigInt(codeMirrorEditor.getOption('tabSize')), insertSpaces: codeMirrorEditor.getOption('insertSpaces'), }), context?.localPath, - () => [ - this.app.commands.addKeyBinding({ - command: COMMAND_ACCEPT, - keys: ['Tab'], - selector: '.CodeMirror', - }), - this.app.commands.addKeyBinding({ - command: COMMAND_DISMISS, - keys: ['Escape'], - selector: '.CodeMirror', - }), - ] + () => { + const keybindingDisposables = [ + this.app.commands.addKeyBinding({ + command: COMMAND_ACCEPT, + keys: [keybindings.accept], + selector: '.CodeMirror', + }), + ]; + if (!this.app.hasPlugin('@axlair/jupyterlab_vim')) { + keybindingDisposables.push( + this.app.commands.addKeyBinding({ + command: COMMAND_DISMISS, + keys: [keybindings.dismiss], + selector: '.CodeMirror', + }) + ); + } + return keybindingDisposables; + } ); }); void chrome.runtime.sendMessage(this.extensionId, { type: 'success' }); diff --git a/src/notebook.ts b/src/notebook.ts index 527fe23..64a1847 100644 --- a/src/notebook.ts +++ b/src/notebook.ts @@ -28,6 +28,7 @@ function isAllowedLanguage(language: Language) { export interface MaybeNotebook { readonly textModels: T[]; readonly currentTextModel: T; + readonly currentTextModelWithOutput?: T; // The offset into the value of getText(currentTextModel) at which to trigger a completion. readonly utf16CodeUnitOffset: number; getText(model: T): string; @@ -46,13 +47,14 @@ export function computeTextAndOffsets(maybeNotebook: MaybeNotebook): TextA let additionalUtf8ByteOffset = 0; let found = false; for (const [idx, previousModel] of textModels.entries()) { - if (modelIsExpected && maybeNotebook.currentTextModel === previousModel) { + const isCurrentCell = modelIsExpected && maybeNotebook.currentTextModel === previousModel; + if (isCurrentCell) { // There is an offset for all previous cells and the newline spacing after each one. additionalUtf8ByteOffset = relevantDocumentTexts .map((el) => numCodeUnitsToNumUtf8Bytes(el)) .reduce((a, b) => a + b, 0) + - cellSplitString.length * relevantDocumentTexts.length; + cellSplitString.length * (relevantDocumentTexts.length + 1); found = true; } const previousModelLanguage = maybeNotebook.getLanguage(previousModel, idx); @@ -62,7 +64,13 @@ export function computeTextAndOffsets(maybeNotebook: MaybeNotebook): TextA if (previousModelLanguage === Language.MARKDOWN) { continue; } else if (previousModelLanguage === modelLanguage) { - relevantDocumentTexts.push(maybeNotebook.getText(previousModel)); + if (isCurrentCell && maybeNotebook.currentTextModelWithOutput !== undefined) { + relevantDocumentTexts.push( + maybeNotebook.getText(maybeNotebook.currentTextModelWithOutput) + ); + } else { + relevantDocumentTexts.push(maybeNotebook.getText(previousModel)); + } } } else if (modelIsMarkdown) { if (previousModelLanguage === Language.MARKDOWN) { @@ -76,8 +84,12 @@ export function computeTextAndOffsets(maybeNotebook: MaybeNotebook): TextA } } } - const currentModelText = maybeNotebook.getText(maybeNotebook.currentTextModel); - const text = found ? relevantDocumentTexts.join(cellSplitString) : currentModelText; + const currentModelText = maybeNotebook.getText( + maybeNotebook.currentTextModelWithOutput ?? maybeNotebook.currentTextModel + ); + const text = found + ? `${cellSplitString}${relevantDocumentTexts.join(cellSplitString)}` + : `${cellSplitString}${currentModelText}`; const utf8ByteOffset = numCodeUnitsToNumUtf8Bytes( currentModelText, maybeNotebook.utf16CodeUnitOffset diff --git a/src/script.ts b/src/script.ts index 08ddafe..c43c209 100644 --- a/src/script.ts +++ b/src/script.ts @@ -3,6 +3,7 @@ import type * as monaco from 'monaco-editor'; import { addListeners } from './codemirror'; import { CodeMirrorState } from './codemirrorInject'; +import { JupyterNotebookKeyBindings } from './common'; import { inject as jupyterInject } from './jupyterInject'; import { getPlugin } from './jupyterlabPlugin'; import { MonacoCompletionProvider, MonacoSite, OMonacoSite } from './monacoCompletionProvider'; @@ -13,13 +14,21 @@ declare type CodeMirror = typeof import('codemirror'); const params = new URLSearchParams((document.currentScript as HTMLScriptElement).src.split('?')[1]); const extensionId = params.get('id')!; -async function getAllowed(extensionId: string): Promise { - const allowed = await new Promise((resolve) => { - chrome.runtime.sendMessage(extensionId, { type: 'allowed' }, (response: boolean) => { - resolve(response); - }); - }); - return allowed; +async function getAllowedAndKeybindings( + extensionId: string +): Promise<{ allowed: boolean; keyBindings: JupyterNotebookKeyBindings }> { + const result = await new Promise<{ allowed: any; keyBindings: JupyterNotebookKeyBindings }>( + (resolve) => { + chrome.runtime.sendMessage( + extensionId, + { type: 'allowed_and_keybindings' }, + (response: { allowed: boolean; keyBindings: JupyterNotebookKeyBindings }) => { + resolve(response); + } + ); + } + ); + return result; } // Clear any bad state from another tab. @@ -91,7 +100,7 @@ const addMonacoInject = () => _monaco.editor.onDidCreateEditor((editor: monaco.editor.ICodeEditor) => { completionProvider.addEditor(editor); }); - console.log('Activated Codeium: Monaco'); + console.log('Codeium: Activated Monaco'); }); }, }, @@ -115,7 +124,7 @@ if (jupyterConfigDataElement !== null) { _jupyterapp.registerPlugin(p); _jupyterapp.activatePlugin(p.id).then( () => { - console.log('Activated Codeium: Jupyter 3.x'); + console.log('Codeium: Activated JupyterLab 3.x'); }, (e) => { console.error(e); @@ -140,7 +149,7 @@ if (jupyterConfigDataElement !== null) { _jupyterlab.registerPlugin(p); _jupyterlab.activatePlugin(p.id).then( () => { - console.log('Activated Codeium: Jupyter 2.x'); + console.log('Codeium: Activated JupyterLab 2.x'); }, (e) => { console.error(e); @@ -158,7 +167,7 @@ const SUPPORTED_CODEMIRROR_SITES = [ { pattern: /https:\/\/(.*\.)?codeshare\.io(\/.*)?/, multiplayer: true }, ]; -const addCodeMirror5GlobalInject = () => +const addCodeMirror5GlobalInject = (keybindings: JupyterNotebookKeyBindings | undefined) => Object.defineProperty(window, 'CodeMirror', { get: function () { return this._codeium_CodeMirror; @@ -175,14 +184,19 @@ const addCodeMirror5GlobalInject = () => // We rely on the fact that the Jupyter variable is defined first. if (Object.prototype.hasOwnProperty.call(this, 'Jupyter')) { injectCodeMirror = true; - const jupyterState = jupyterInject(extensionId, this.Jupyter); - addListeners(cm as CodeMirror, jupyterState.codeMirrorManager); - console.log('Activated Codeium'); + if (keybindings === undefined) { + console.warn('Codeium found no keybindings for Jupyter Notebook'); + return; + } else { + const jupyterState = jupyterInject(extensionId, this.Jupyter, keybindings); + addListeners(cm as CodeMirror, jupyterState.codeMirrorManager); + console.log('Activated Codeium for Jupyter Notebook'); + } } else { let multiplayer = false; for (const pattern of SUPPORTED_CODEMIRROR_SITES) { if (pattern.pattern.test(window.location.href)) { - console.log('Codeium: Activating CodeMirror'); + console.log('Codeium: Activating CodeMirror Site'); injectCodeMirror = true; multiplayer = pattern.multiplayer; break; @@ -193,7 +207,7 @@ const addCodeMirror5GlobalInject = () => } if (injectCodeMirror) { new CodeMirrorState(extensionId, cm as CodeMirror, multiplayer); - console.log('Activated Codeium'); + console.log('Codeium: Activating CodeMirror'); } } }, @@ -242,8 +256,10 @@ const addCodeMirror5LocalInject = () => { }, 500); }; -getAllowed(extensionId).then( - (allowed) => { +getAllowedAndKeybindings(extensionId).then( + (allowedAndKeybindings) => { + const allowed = allowedAndKeybindings.allowed; + const jupyterKeyBindings = allowedAndKeybindings.keyBindings; const validInjectTypes = ['monaco', 'codemirror5', 'none']; const metaTag = document.querySelector('meta[name="codeium:type"]'); const injectionTypes = @@ -263,16 +279,17 @@ getAllowed(extensionId).then( } if (injectionTypes.includes('codemirror5')) { - addCodeMirror5GlobalInject(); + addCodeMirror5GlobalInject(jupyterKeyBindings); addCodeMirror5LocalInject(); } if (injectionTypes.length === 0) { // if no meta tag is found, check the allowlist if (allowed) { + console.log('Injecting everything'); // the url matches the allowlist addMonacoInject(); - addCodeMirror5GlobalInject(); + addCodeMirror5GlobalInject(jupyterKeyBindings); addCodeMirror5LocalInject(); return; } diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts index 2466788..c84cbd0 100644 --- a/src/serviceWorker.ts +++ b/src/serviceWorker.ts @@ -3,6 +3,8 @@ import { v4 as uuidv4 } from 'uuid'; import { registerUser } from './auth'; import { GetCompletionsResponseMessage, + JupyterLabKeyBindings, + JupyterNotebookKeyBindings, LanguageServerServiceWorkerClient, LanguageServerWorkerRequest, } from './common'; @@ -12,6 +14,7 @@ import { defaultAllowlist, getGeneralPortalUrl, getStorageItem, + getStorageItems, initializeStorageWithDefaults, setStorageItem, } from './storage'; @@ -65,21 +68,50 @@ chrome.runtime.onInstalled.addListener(async () => { // - request for api key // - set icon and error message chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => { - if (message.type === 'allowed') { + if (message.type === 'allowed_and_keybindings') { (async () => { + // If not allowed, the keybindings can be undefined. + let allowed = false; + const defaultKeyBindings: JupyterNotebookKeyBindings = { + accept: 'Tab', + }; if (sender.url === undefined) { - sendResponse(false); + sendResponse({ allowed: false, keyBindings: defaultKeyBindings }); return; } - const allowlist = await getStorageItem('allowlist'); + const { allowlist: allowlist, jupyterNotebookKeybindingAccept: accept } = + await getStorageItems(['allowlist', 'jupyterNotebookKeybindingAccept']); for (const addr of computeAllowlist(allowlist)) { const host = new RegExp(addr); if (host.test(sender.url)) { - sendResponse(true); - return; + allowed = true; + break; } } - sendResponse(false); + + if (!allowed) { + sendResponse({ allowed: false, keyBindings: defaultKeyBindings }); + } + + const keybindings: JupyterNotebookKeyBindings = { + accept: accept ? accept : 'Tab', + }; + + sendResponse({ allowed: allowed, keyBindings: keybindings }); + })().catch((e) => { + console.error(e); + }); + return true; + } + if (message.type === 'jupyterlab') { + (async () => { + const { jupyterlabKeybindingAccept: accept, jupyterlabKeybindingDismiss: dismiss } = + await getStorageItems(['jupyterlabKeybindingAccept', 'jupyterlabKeybindingDismiss']); + const keybindings: JupyterLabKeyBindings = { + accept: accept ? accept : 'Tab', + dismiss: dismiss ? dismiss : 'Escape', + }; + sendResponse(keybindings); })().catch((e) => { console.error(e); }); diff --git a/src/storage.ts b/src/storage.ts index b333eb8..dd73705 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -20,6 +20,10 @@ export interface Storage { current: string[]; }; enterpriseDefaultModel?: string; + jupyterlabKeybindingAccept?: string; + jupyterlabKeybindingDismiss?: string; + jupyterNotebookKeybindingAccept?: string; + jupyterNotebookKeybindingDismiss?: string; } // In case the defaults change over time, reconcile the saved setting with the @@ -51,6 +55,18 @@ export function computeAllowlist( return allowlist.current; } +export async function populateFromManagedStorage(): Promise { + const managedStorageItems = chrome.storage.managed.get(['portalUrl', 'enterpriseDefaultModel']); + void managedStorageItems.then((result) => { + if (result.portalUrl !== undefined) { + void setStorageItem('portalUrl', result.portalUrl); + } + if (result.enterpriseDefaultModel !== undefined) { + void setStorageItem('enterpriseDefaultModel', result.enterpriseDefaultModel); + } + }); +} + export function getStorageData(): Promise { return new Promise((resolve, reject) => { chrome.storage.sync.get(null, (result) => { @@ -117,6 +133,9 @@ export function setStorageItem( } export async function initializeStorageWithDefaults(defaults: Storage) { + if (CODEIUM_ENTERPRISE) { + await populateFromManagedStorage(); + } const currentStorageData = await getStorageData(); const newStorageData = Object.assign({}, defaults, currentStorageData); await setStorageData(newStorageData); diff --git a/static/manifest.json b/static/manifest.json index ef244ad..588f4dc 100644 --- a/static/manifest.json +++ b/static/manifest.json @@ -1,7 +1,7 @@ { "name": "Codeium: AI Code Autocompletion on all IDEs", "description": "Your modern coding superpower. Get code completions in Colab and more.", - "version": "1.8.25", + "version": "1.8.61", "manifest_version": 3, "background": { "service_worker": "serviceWorker.js" From d08cc824c0508c1e14de039c3ba1b892ed70ce66 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 17 Jun 2024 14:36:38 -0700 Subject: [PATCH 2/2] version 1.8.58 --- package.json | 2 +- src/script.ts | 1 - static/manifest.json | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 2942aff..3ce9cba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeium-chrome", - "version": "1.8.61", + "version": "1.8.58", "description": "", "license": "MIT", "scripts": { diff --git a/src/script.ts b/src/script.ts index c43c209..f09e751 100644 --- a/src/script.ts +++ b/src/script.ts @@ -286,7 +286,6 @@ getAllowedAndKeybindings(extensionId).then( if (injectionTypes.length === 0) { // if no meta tag is found, check the allowlist if (allowed) { - console.log('Injecting everything'); // the url matches the allowlist addMonacoInject(); addCodeMirror5GlobalInject(jupyterKeyBindings); diff --git a/static/manifest.json b/static/manifest.json index 588f4dc..376c8ff 100644 --- a/static/manifest.json +++ b/static/manifest.json @@ -1,7 +1,7 @@ { "name": "Codeium: AI Code Autocompletion on all IDEs", "description": "Your modern coding superpower. Get code completions in Colab and more.", - "version": "1.8.61", + "version": "1.8.58", "manifest_version": 3, "background": { "service_worker": "serviceWorker.js"