diff --git a/README.md b/README.md index fb8ae46..7861883 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ [![Twitter Follow](https://img.shields.io/badge/style--blue?style=social&logo=twitter&label=Follow%20%40codeiumdev)](https://twitter.com/intent/follow?screen_name=codeiumdev) ![License](https://img.shields.io/github/license/Exafunction/codeium-chrome) [![Docs](https://img.shields.io/badge/Codeium%20Docs-09B6A2)](https://docs.codeium.com) -[![Canny Board](https://img.shields.io/badge/Feature%20Requests-6b69ff)](https://codeium.canny.io/feature-requests/) -[![built with Codeium](https://codeium.com/badges/main)](https://codeium.com?repo_name=exafunction%2Fcodeium.vim) [![Visual Studio](https://img.shields.io/visual-studio-marketplace/i/Codeium.codeium?label=Visual%20Studio&logo=visualstudio)](https://marketplace.visualstudio.com/items?itemName=Codeium.codeium) [![JetBrains](https://img.shields.io/jetbrains/plugin/d/20540?label=JetBrains)](https://plugins.jetbrains.com/plugin/20540-codeium/) diff --git a/buf.gen.yaml b/buf.gen.yaml index 852c0f1..312ebda 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -1,11 +1,11 @@ version: v2 plugins: - - local: node_modules/@bufbuild/protoc-gen-es/bin/protoc-gen-es + - local: [node, node_modules/@bufbuild/protoc-gen-es/bin/protoc-gen-es] out: proto opt: - target=ts - import_extension=none - - local: node_modules/@connectrpc/protoc-gen-connect-es/bin/protoc-gen-connect-es + - local: [node, node_modules/@connectrpc/protoc-gen-connect-es/bin/protoc-gen-connect-es] out: proto opt: - target=ts diff --git a/package.json b/package.json index 2252d21..81f54cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeium-chrome", - "version": "1.20.4", + "version": "1.26.3", "description": "", "license": "MIT", "scripts": { diff --git a/src/codemirror.ts b/src/codemirror.ts index 1f9ac24..e20145b 100644 --- a/src/codemirror.ts +++ b/src/codemirror.ts @@ -2,7 +2,7 @@ import { IDisposable } from '@lumino/disposable'; import type CodeMirror from 'codemirror'; import { editorLanguage, language } from './codemirrorLanguages'; -import { CODEIUM_DEBUG, IdeInfo, LanguageServerClient } from './common'; +import { CODEIUM_DEBUG, IdeInfo, KeyCombination, LanguageServerClient } from './common'; import { TextAndOffsets, computeTextAndOffsets } from './notebook'; import { numUtf8BytesToNumCodeUnits } from './utf'; import { EditorOptions } from '../proto/exa/codeium_common_pb/codeium_common_pb'; @@ -308,27 +308,35 @@ export class CodeMirrorManager { doc: CodeMirror.Doc, event: KeyboardEvent, alsoHandle: { tab: boolean; escape: boolean }, - tabKey: string = 'Tab' + keyCombination?: KeyCombination ): { consumeEvent: boolean | undefined; forceTriggerCompletion: boolean } { let forceTriggerCompletion = false; - if (event.ctrlKey) { - if (event.key === ' ') { - forceTriggerCompletion = true; - } else { - return { consumeEvent: false, forceTriggerCompletion }; - } + if (event.ctrlKey && event.key === ' ') { + forceTriggerCompletion = true; } + // Classic notebook may autocomplete these. if ('"\')}]'.includes(event.key)) { forceTriggerCompletion = true; } + if (event.isComposing) { this.clearCompletion('composing'); return { consumeEvent: false, forceTriggerCompletion }; } - // Shift-tab in jupyter notebooks shows documentation. - if (event.key === 'Tab' && event.shiftKey) { - return { consumeEvent: false, forceTriggerCompletion }; + + // Accept completion logic + if (keyCombination) { + const matchesKeyCombination = + event.key.toLowerCase() === keyCombination.key.toLowerCase() && + !!event.ctrlKey === !!keyCombination.ctrl && + !!event.altKey === !!keyCombination.alt && + !!event.shiftKey === !!keyCombination.shift && + !!event.metaKey === !!keyCombination.meta; + + if (matchesKeyCombination && this.acceptCompletion()) { + return { consumeEvent: true, forceTriggerCompletion }; + } } // TODO(kevin): clean up autoHandle logic @@ -336,20 +344,25 @@ export class CodeMirrorManager { // Jupyter Notebook: tab = true, escape = false // Code Mirror Websites: tab = true, escape = true // Jupyter Lab: tab = false, escape = false - if (!event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey) { - 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 && !alsoHandle.escape && tabKey !== 'Tab' && event.key === 'Tab') { - return { consumeEvent: false, forceTriggerCompletion }; - } + + // Code Mirror Websites only + if ( + !event.metaKey && + !event.ctrlKey && + !event.altKey && + !event.shiftKey && + alsoHandle.escape && + event.key === 'Escape' && + this.clearCompletion('user dismissed') + ) { + return { consumeEvent: true, forceTriggerCompletion }; } + + // Shift-tab in jupyter notebooks shows documentation. + if (event.key === 'Tab' && event.shiftKey) { + return { consumeEvent: false, forceTriggerCompletion }; + } + switch (event.key) { case 'Delete': case 'ArrowDown': diff --git a/src/codemirrorInject.ts b/src/codemirrorInject.ts index 5b5f926..cbfbb06 100644 --- a/src/codemirrorInject.ts +++ b/src/codemirrorInject.ts @@ -54,7 +54,8 @@ export class CodeMirrorState { const { consumeEvent, forceTriggerCompletion } = this.codeMirrorManager.beforeMainKeyHandler( editor.getDoc(), event, - { tab: true, escape: true } + { tab: true, escape: true }, + { key: 'Tab', ctrl: false, alt: false, shift: false, meta: false } ); if (consumeEvent !== undefined) { if (consumeEvent) { diff --git a/src/common.ts b/src/common.ts index 36fc58d..76634fa 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.20.4'; +const EXTENSION_VERSION = '1.26.3'; export const CODEIUM_DEBUG = false; export const DEFAULT_PATH = 'unknown_url'; @@ -23,13 +23,21 @@ export interface ClientSettings { defaultModel?: string; } +export interface KeyCombination { + key: string; + ctrl?: boolean; + alt?: boolean; + shift?: boolean; + meta?: boolean; +} + export interface JupyterLabKeyBindings { - accept: string; - dismiss: string; + accept: KeyCombination; + dismiss: KeyCombination; } export interface JupyterNotebookKeyBindings { - accept: string; + accept: KeyCombination; } async function getClientSettings(): Promise { diff --git a/src/component/Options.tsx b/src/component/Options.tsx index 25cc2e8..4ec5c15 100644 --- a/src/component/Options.tsx +++ b/src/component/Options.tsx @@ -148,15 +148,52 @@ 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(''); const [jupyterDebounceMs, setJupyterDebounceMs] = useState(0); const jupyterDebounceMsRef = createRef(); + const [currentKey, setCurrentKey] = useState({ + key: '', + ctrl: false, + alt: false, + shift: false, + meta: false, + }); + const [jupyterlabAcceptInput, setJupyterlabAcceptInput] = useState(false); + const [jupyterlabDismissInput, setJupyterlabDismissInput] = useState(false); + const [notebookAcceptInput, setNotebookAcceptInput] = useState(false); + + const formatKeyCombination = (key: any) => { + const modifiers = []; + if (key.ctrl) modifiers.push('Ctrl'); + if (key.alt) modifiers.push('Alt'); + if (key.shift) modifiers.push('Shift'); + if (key.meta) modifiers.push('Meta'); + return [...modifiers, key.key.toUpperCase()].join('+'); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + e.preventDefault(); + const key = e.key; + if (key !== 'Control' && key !== 'Alt' && key !== 'Shift' && key !== 'Meta') { + const ctrl = e.ctrlKey; + const alt = e.altKey; + const shift = e.shiftKey; + const meta = e.metaKey; + setCurrentKey({ key, ctrl, alt, shift, meta }); + + // Force blur using setTimeout to ensure it happens after state update + setTimeout(() => { + if (e.currentTarget) { + e.currentTarget.blur(); + // Also try to remove focus from the document + (document.activeElement as HTMLElement)?.blur(); + } + }, 0); + } + }; useEffect(() => { (async () => { @@ -203,6 +240,7 @@ const Options = () => { } return PUBLIC_WEBSITE; }, []); + return ( {!CODEIUM_ENTERPRISE && ( @@ -328,95 +366,84 @@ const Options = () => { }} /> - Jupyterlab settings + Jupyter Settings - A single keystroke is supported. The syntax is described{' '} - - here - - - . + Press the desired key combination in the input field. For example, press "Ctrl+Tab" for a + Ctrl+Tab shortcut. + + + + JupyterLab setJupyterlabKeybindingAcceptText(e.target.value)} + value={jupyterlabAcceptInput ? 'Press keys...' : jupyterlabKeybindingAcceptText || 'Tab'} + onFocus={() => setJupyterlabAcceptInput(true)} + onBlur={async () => { + setJupyterlabAcceptInput(false); + if (currentKey.key) { + const formatted = formatKeyCombination(currentKey); + setJupyterlabKeybindingAcceptText(formatted); + await setStorageItem('jupyterlabKeybindingAccept', formatted); + setCurrentKey({ key: '', ctrl: false, alt: false, shift: false, meta: false }); + } + }} + onKeyDown={handleKeyDown} /> - - - setJupyterlabKeybindingDismissText(e.target.value)} + value={ + jupyterlabDismissInput ? 'Press keys...' : jupyterlabKeybindingDismissText || 'Escape' + } + onFocus={() => setJupyterlabDismissInput(true)} + onBlur={async () => { + setJupyterlabDismissInput(false); + if (currentKey.key) { + const formatted = formatKeyCombination(currentKey); + setJupyterlabKeybindingDismissText(formatted); + await setStorageItem('jupyterlabKeybindingDismiss', formatted); + setCurrentKey({ key: '', ctrl: false, alt: false, shift: false, meta: false }); + } + }} + onKeyDown={handleKeyDown} /> - - - - - - Jupyter Notebook settings + + + Jupyter Notebook + setJupyterNotebookKeybindingAcceptText(e.target.value)} + value={ + notebookAcceptInput ? 'Press keys...' : jupyterNotebookKeybindingAcceptText || 'Tab' + } + onFocus={() => setNotebookAcceptInput(true)} + onBlur={async () => { + setNotebookAcceptInput(false); + if (currentKey.key) { + const formatted = formatKeyCombination(currentKey); + setJupyterNotebookKeybindingAcceptText(formatted); + await setStorageItem('jupyterNotebookKeybindingAccept', formatted); + setCurrentKey({ key: '', ctrl: false, alt: false, shift: false, meta: false }); + } + }} + onKeyDown={handleKeyDown} /> - - - - - - Jupyter debounce time + + + Performance + {