diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e230f59 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": true, + "useTabs": false, + "tabWidth": 2, + "printWidth": 80, + "trailingComma": "all" +} \ No newline at end of file diff --git a/docs/codeium_playground.gif b/docs/codeium_playground.gif new file mode 100644 index 0000000..2d95dd7 Binary files /dev/null and b/docs/codeium_playground.gif differ diff --git a/exa/language_server_pb/language_server.proto b/exa/language_server_pb/language_server.proto index a324f08..e620593 100644 --- a/exa/language_server_pb/language_server.proto +++ b/exa/language_server_pb/language_server.proto @@ -15,6 +15,12 @@ service LanguageServerService { rpc GetAuthToken(GetAuthTokenRequest) returns (GetAuthTokenResponse) {} } +message MultilineConfig { + // Multiline model threshold. 0-1, higher = more single line, lower = more multiline, + // 0.0 = only_multiline, default is 0.5 + float threshold = 1; +} + // Next ID: 9, Previous field: disable_cache. message GetCompletionsRequest { codeium_common_pb.Metadata metadata = 1 [(validate.rules).message.required = true]; @@ -24,6 +30,7 @@ message GetCompletionsRequest { ExperimentConfig experiment_config = 7; string model_name = 10; + MultilineConfig multiline_config = 13; } // Next ID: 5, Previous field: latency_info. diff --git a/package.json b/package.json index 8df1e3c..504987a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@codeium/react-code-editor", - "version": "1.0.5", + "version": "1.0.12", "description": "AI-powered React component", "main": "dist/index.js", "type": "module", @@ -10,7 +10,7 @@ ], "types": "dist/index.d.ts", "engines": { - "node": ">=20" + "node": ">=16" }, "packageManager": "pnpm@8.9.0", "scripts": { @@ -20,7 +20,8 @@ "test": "echo \"Error: no test specified\" && exit 1", "storybook": "storybook dev -p 6006 --no-open", "build-storybook": "storybook build", - "prepare": "npm run rollup" + "format": "prettier --write 'src/**/*.{ts,tsx}'", + "prepare": "pnpm run generate && pnpm run rollup" }, "repository": { "type": "git", @@ -34,9 +35,7 @@ "@connectrpc/connect": "1.1.3", "@connectrpc/connect-web": "1.1.3", "@monaco-editor/react": "^4.6.0", - "dotenv": "^16.3.1", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "dotenv": "^16.3.1" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.7", @@ -52,6 +51,7 @@ "@storybook/test": "^7.6.4", "@swc/core": "^1.3.104", "@types/react": "^18.2.42", + "prettier": "^3.2.5", "rollup": "^4.7.0", "rollup-plugin-banner2": "^1.2.2", "rollup-plugin-dts": "^6.1.0", @@ -60,6 +60,8 @@ "typescript": "^5.3.3" }, "peerDependencies": { - "monaco-editor": "^0.45.0" + "monaco-editor": "^0.45.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4193b4e..e66840d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,6 +73,9 @@ devDependencies: '@types/react': specifier: ^18.2.42 version: 18.2.42 + prettier: + specifier: ^3.2.5 + version: 3.2.5 rollup: specifier: ^4.7.0 version: 4.7.0 @@ -6603,6 +6606,12 @@ packages: hasBin: true dev: true + /prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + engines: {node: '>=14'} + hasBin: true + dev: true + /pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} diff --git a/readme.md b/readme.md index 4c61c40..f7ea055 100644 --- a/readme.md +++ b/readme.md @@ -1,16 +1,16 @@ # Codeium Editor -[![built with Codeium](https://codeium.com/badges/main)](https://codeium.com/badges/main) +[![built with Codeium](https://codeium.com/badges/main)](https://codeium.com?referrer=github) -[![NPM](https://nodei.co/npm/@codeium/react-code-editor.png?downloads=true)](https://www.npmjs.com/package/@codeium/react-code-editor) +[![NPM](https://nodei.co/npm/@codeium/react-code-editor.png?downloads=true)](https://www.npmjs.com/package/@codeium/react-code-editor) -Codeium React Editor is a free, open-source code editor with unlimited autocomplete. Brought to you by the team at [Codeium](https://www.codeium.com/). **Free with no account required.** +Codeium React Code Editor is a free, open-source code editor as a React component with unlimited AI autocomplete. Brought to you by the team at [Codeium](https://www.codeium.com/). **Free with no account required.**. All you need to do is install our NPM package, add it to your website and you're good to go! -![codeium demo](docs/codeium_demo.gif) +![codeium demo](docs/codeium_playground.gif) ## Features -- Unlimited autocomplete (no account required) +- Unlimited AI autocomplete (no account required) - Customizable API extended from [Monaco React](https://github.com/suren-atoyan/monaco-react?tab=readme-ov-file#editor) ## Demo @@ -32,7 +32,7 @@ yarn add @codeium/react-code-editor pnpm install @codeium/react-code-editor ``` -Now import the `CodeiumEditor` and enjoy lightning fast autocomplete, directly in your browser, 100% for free! +Now import the `CodeiumEditor` and enjoy lightning fast AI autocomplete, directly in your browser, 100% for free! ```tsx import { CodeiumEditor } from "@codeium/react-code-editor"; @@ -47,6 +47,47 @@ export const IdeWithAutocomplete = () => { }; ``` +Here's an advanced example that uses multi-document context to provide more intelligent autocompletion: + +```tsx +import { CodeiumEditor, Document, Language } from "@codeium/react-code-editor"; + +export const JavaScriptEditorWithContext = () => { + const html = ` + +

Contact Us

+
+ + + + +
+ +`; + + return ( +
+

This editor has context awareness of a neighboring HTML file and can provide better autocompletion suggestions.

+ +
+ ); +}; +``` + +Note that the `otherDocuments` prop has a limit of 10 documents. Within those documents, Codeium will run a reranker behind the scenes to optimize what is included in the token limit. + ### Examples Here are some examples of Codeium React Editor used in production: @@ -59,10 +100,26 @@ This project is a wrapper around Microsoft's Monaco editor which is the editor t The autocompletes are provided by analyzing the editor's content and predicting and providing suggestions based on that context. To learn more about how the autocompletion works, visit [Codeium's FAQ](https://codeium.com/faq). +## What is Codeium + +[Codeium](https://www.codeium.com?referrer=github) is a free, AI-powered developer toolkit that plugs into 70+ IDEs, including: Visual Studio Code, JetBrains IDEs, Google Colab, and Vim. Codeium provides unlimited AI context-aware autocomplete, chat assistant, intelligent search, codebase indexing, and more. Codeium also offers flexible deployments within your VPC or in on-prem, airgapped environments. Learn more at [codeium.com](https://www.codeium.com?referrer=github). + ## API The core API of the editor is the same as that of the wrapped project. You can view the editor API [here](https://github.com/suren-atoyan/monaco-react?tab=readme-ov-file#editor). +## FAQ + +#### How can I import the ESM version of this? + +To import the ESM version of this, you can use `import { CodeiumEditor } from "@codeium/react-code-editor/dist/esm";`. If you're using TypeScript, your editor might warn that the types are missing. A current workaround is: + +- Create a `codeiumeditor.d.ts` file, +- Add `declare module '@codeium/react-code-editor/dist/esm';` to the file +- Import the types file in the file using the `CodeiumEditor` component. + +This is an open issue in terms of supporting both CommonJS and ESM. If you're interested in contributing and have a fix for this, pull requests are welcome. + ## Acknowledgements This project would not have been possible without [Suren Atoyan's Monaco React project](https://github.com/suren-atoyan/monaco-react). diff --git a/rollup.config.js b/rollup.config.js index 344c7cf..f22f04e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -5,6 +5,9 @@ import dts from "rollup-plugin-dts"; import banner2 from 'rollup-plugin-banner2' import packageJson from "./package.json" assert { type: "json" }; +const version = packageJson.version ?? null; + + export default [ { input: "src/index.ts", @@ -26,7 +29,11 @@ export default [ commonjs(), typescript({ tsconfig: "./tsconfig.json" }), banner2(() => ` - "use client"; +"use client"; + +if (typeof window !== "undefined") { + window.CODEIUM_REACT_CODE_VERSION = ${version ? `${JSON.stringify(version)}` : null}; +} `) ], }, diff --git a/src/components/CodeiumEditor/CodeiumEditor.tsx b/src/components/CodeiumEditor/CodeiumEditor.tsx index 4829aa4..73911ea 100644 --- a/src/components/CodeiumEditor/CodeiumEditor.tsx +++ b/src/components/CodeiumEditor/CodeiumEditor.tsx @@ -1,49 +1,87 @@ -"use client"; +'use client'; -import React, { useEffect, useRef, useState, useMemo } from "react"; -import { createConnectTransport } from "@connectrpc/connect-web"; -import { createPromiseClient } from "@connectrpc/connect"; -import { Status } from "./Status"; -import Editor, { EditorProps, Monaco } from "@monaco-editor/react"; -import { editor } from "monaco-editor/esm/vs/editor/editor.api"; -import { getDefaultValue } from "./defaultValues"; +import React, { useEffect, useRef, useState, useMemo } from 'react'; +import { createConnectTransport } from '@connectrpc/connect-web'; +import { createPromiseClient } from '@connectrpc/connect'; +import { Status } from './Status'; +import Editor, { EditorProps, Monaco } from '@monaco-editor/react'; +import { editor } from 'monaco-editor/esm/vs/editor/editor.api'; +import { getDefaultValue } from './defaultValues'; -import { LanguageServerService } from "../../api/proto/exa/language_server_pb/language_server_connect"; -import { InlineCompletionProvider } from "./InlineCompletionProvider"; -import { CodeiumLogo } from "../CodeiumLogo/CodeiumLogo"; +import { LanguageServerService } from '../../api/proto/exa/language_server_pb/language_server_connect'; +import { InlineCompletionProvider } from './InlineCompletionProvider'; +import { CodeiumLogo } from '../CodeiumLogo/CodeiumLogo'; +import { Document } from '../../models'; +import { deepMerge } from '../../utils/merge'; -interface CodeiumEditorProps extends EditorProps { +export interface CodeiumEditorProps extends EditorProps { language: string; apiKey?: string; /** * Optional callback to detect when completions are accepted. Includes the accepted text for the completion. */ onAutocomplete?: (acceptedText: string) => void; + + /** + * Optional address of the Language Server. This should not be needed for most use cases. Defaults + * to Codeium's language server. + */ + languageServerAddress?: string; + + /** + * Optional list of other documents in the workspace. This can be used to provide additional + * context to Codeium beyond simply the current document. There is a limit of 10 medium sized + * documents. + */ + otherDocuments?: Document[]; + + /** + * Optional classname for the container. + */ + containerClassName?: string; + + /** + * Optional styles for the container. + */ + containerStyle?: React.CSSProperties; + + /** + * Optional multiline model threshold. Should not be needed for most use cases. + * Numerical value between 0-1, higher = more single line, lower = more multiline, + * 0.0 = only_multiline. + */ + multilineModelThreshold?: number; } /** * Code editor that enables Codeium AI suggestions in the editor. * The layout by default is width = 100% and height = 300px. These values can be overridden by passing in a string value to the width and/or height props. */ -export const CodeiumEditor: React.FC = (props) => { +export const CodeiumEditor: React.FC = ({ + languageServerAddress = 'https://web-backend.codeium.com', + otherDocuments = [], + containerClassName = '', + containerStyle = {}, + ...props +}) => { const editorRef = useRef(null); const monacoRef = useRef(null); const inlineCompletionsProviderRef = useRef( - null + null, ); const [acceptedCompletionCount, setAcceptedCompletionCount] = useState(-1); const [completionCount, setCompletionCount] = useState(0); const [codeiumStatus, setCodeiumStatus] = useState(Status.INACTIVE); - const [codeiumStatusMessage, setCodeiumStatusMessage] = useState(""); + const [codeiumStatusMessage, setCodeiumStatusMessage] = useState(''); const [mounted, setMounted] = useState(false); const transport = useMemo(() => { return createConnectTransport({ - baseUrl: "https://web-backend.codeium.com", + baseUrl: languageServerAddress, useBinaryFormat: true, }); - }, []); - + }, [languageServerAddress]); + const grpcClient = useMemo(() => { return createPromiseClient(LanguageServerService, transport); }, [transport]); @@ -54,22 +92,28 @@ export const CodeiumEditor: React.FC = (props) => { setCompletionCount, setCodeiumStatus, setCodeiumStatusMessage, - props.apiKey + props.apiKey, + props.multilineModelThreshold, ); }, []); useEffect(() => { - if (!editorRef?.current || !monacoRef.current || !inlineCompletionsProviderRef.current) { + if ( + !editorRef?.current || + !monacoRef.current || + !inlineCompletionsProviderRef.current + ) { return; } const monaco = monacoRef.current; - const providerDisposable = monaco.languages.registerInlineCompletionsProvider( - { pattern: "**" }, - inlineCompletionsProviderRef.current - ); + const providerDisposable = + monaco.languages.registerInlineCompletionsProvider( + { pattern: '**' }, + inlineCompletionsProviderRef.current, + ); const completionDisposable = monaco.editor.registerCommand( - "codeium.acceptCompletion", + 'codeium.acceptCompletion', (_: unknown, completionId: string, insertText: string) => { try { if (props.onAutocomplete) { @@ -77,23 +121,29 @@ export const CodeiumEditor: React.FC = (props) => { } setAcceptedCompletionCount(acceptedCompletionCount + 1); inlineCompletionsProviderRef.current?.acceptedLastCompletion( - completionId + completionId, ); } catch (err) { - console.log("Err"); + console.log('Err'); } - } + }, ); return () => { providerDisposable.dispose(); completionDisposable.dispose(); - } - }, [editorRef?.current, monacoRef?.current, inlineCompletionsProviderRef?.current, acceptedCompletionCount, mounted]) + }; + }, [ + editorRef?.current, + monacoRef?.current, + inlineCompletionsProviderRef?.current, + acceptedCompletionCount, + mounted, + ]); const handleEditorDidMount = async ( editor: editor.IStandaloneCodeEditor, - monaco: Monaco + monaco: Monaco, ) => { editorRef.current = editor; monacoRef.current = monaco; @@ -112,30 +162,43 @@ export const CodeiumEditor: React.FC = (props) => { } }; + // Keep other documents up to date. + useEffect(() => { + inlineCompletionsProviderRef.current?.updateOtherDocuments(otherDocuments); + }, [otherDocuments]); + let defaultLanguageProps: EditorProps = { defaultLanguage: props.language, defaultValue: getDefaultValue(props.language), }; const layout = { - width: props.width || "100%", + width: props.width || '100%', // The height is set to 300px by default. Otherwise, the editor when // rendered with the default value will not be visible. // The monaco editor's default height is 100% but it requires the user to // define a container with an explicit height. - height: props.height || "300px", + height: props.height || '300px', }; return ( -
- +
+ = (props) => { width={layout.width} height={layout.height} onMount={handleEditorDidMount} - options={{ - scrollBeyondLastColumn: 0, - scrollbar: { - alwaysConsumeMouseWheel: false, - vertical: "hidden", - }, - codeLens: false, - // for resizing, but apparently might have "severe performance impact" - // automaticLayout: true, - minimap: { - enabled: false, + options={deepMerge( + props.options, + { + scrollBeyondLastColumn: 0, + scrollbar: { + alwaysConsumeMouseWheel: false, + }, + codeLens: false, + // for resizing, but apparently might have "severe performance impact" + // automaticLayout: true, + minimap: { + enabled: false, + }, + quickSuggestions: false, + folding: false, + foldingHighlight: false, + foldingImportsByDefault: false, + links: false, + fontSize: 14, + wordWrap: 'on', }, - quickSuggestions: false, - folding: false, - foldingHighlight: false, - foldingImportsByDefault: false, - links: false, - fontSize: 14, - wordWrap: "on", - ...props.options - }} + )} />
); diff --git a/src/components/CodeiumEditor/CompletionProvider.ts b/src/components/CodeiumEditor/CompletionProvider.ts index 03d329a..5765362 100644 --- a/src/components/CodeiumEditor/CompletionProvider.ts +++ b/src/components/CodeiumEditor/CompletionProvider.ts @@ -1,29 +1,27 @@ -import { Code, ConnectError, PromiseClient } from "@connectrpc/connect"; -import { CancellationToken } from "./CancellationToken"; +import { Code, ConnectError, PromiseClient } from '@connectrpc/connect'; +import { CancellationToken } from './CancellationToken'; import { Document as DocumentInfo, GetCompletionsResponse, CompletionItem, -} from "../../api/proto/exa/language_server_pb/language_server_pb"; -import { Document } from "./Document"; -import { Position, Range } from "./Location"; + MultilineConfig, +} from '../../api/proto/exa/language_server_pb/language_server_pb'; +import { Document } from './Document'; +import { Position, Range } from './Location'; import { numUtf8BytesToNumCodeUnits, numCodeUnitsToNumUtf8Bytes, -} from "../../utils/utf"; -import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; -import { LanguageServerService } from "../../api/proto/exa/language_server_pb/language_server_connect"; +} from '../../utils/utf'; +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import { LanguageServerService } from '../../api/proto/exa/language_server_pb/language_server_connect'; import { Language, Metadata, -} from "../../api/proto/exa/codeium_common_pb/codeium_common_pb"; -import { Status } from "./Status"; -import { uuid } from "../../utils/uuid"; -import { - getBrowserVersion, - getCurrentURL, - getPackageVersion, -} from "../../utils/identity"; +} from '../../api/proto/exa/codeium_common_pb/codeium_common_pb'; +import { Status } from './Status'; +import { uuid } from '../../utils/uuid'; +import { getCurrentURL, getPackageVersion } from '../../utils/identity'; +import { languageIdToEnum } from '../../utils/language'; class MonacoInlineCompletion implements monaco.languages.InlineCompletion { readonly insertText: string; @@ -41,39 +39,54 @@ class MonacoInlineCompletion implements monaco.languages.InlineCompletion { this.text = insertText; this.range = range; this.command = { - id: "codeium.acceptCompletion", - title: "Accept Completion", + id: 'codeium.acceptCompletion', + title: 'Accept Completion', arguments: [completionId, insertText], }; } } -const EDITOR_API_KEY = "d49954eb-cfba-4992-980f-d8fb37f0e942" +const EDITOR_API_KEY = 'd49954eb-cfba-4992-980f-d8fb37f0e942'; /** * CompletionProvider class for Codeium. */ export class MonacoCompletionProvider { - authHeader: Record = {}; - private metadata: Metadata; private client: PromiseClient; + private sessionId: string; + + /** + * A list of other documents to include as context in the prompt. + */ + public otherDocuments: DocumentInfo[] = []; constructor( grpcClient: PromiseClient, readonly setStatus: (status: Status) => void, readonly setMessage: (message: string) => void, - readonly apiKey?: string | undefined - + readonly apiKey?: string | undefined, + readonly multilineModelThreshold?: number | undefined, ) { - this.metadata = new Metadata({ - ideName: getBrowserVersion() ?? "unknown", - ideVersion: getCurrentURL() ?? "unknown", - extensionName: "@codeium/react-code-editor", - extensionVersion: getPackageVersion() ?? "unknown", - apiKey: apiKey ?? EDITOR_API_KEY, - sessionId: `demo-${uuid()}`, - }); + this.sessionId = `react-editor-${uuid()}`; this.client = grpcClient; - const Authorization = `Basic ${this.metadata.apiKey}-${this.metadata.sessionId}`; - this.authHeader = { Authorization }; + } + + private getAuthHeader() { + const metadata = this.getMetadata(); + const headers = { + Authorization: `Basic ${metadata.apiKey}-${metadata.sessionId}`, + }; + return headers; + } + + private getMetadata(): Metadata { + const metadata = new Metadata({ + ideName: 'web', + ideVersion: getCurrentURL() ?? 'unknown', + extensionName: '@codeium/react-code-editor', + extensionVersion: getPackageVersion() ?? 'unknown', + apiKey: this.apiKey ?? EDITOR_API_KEY, + sessionId: this.sessionId, + }); + return metadata; } /** @@ -86,7 +99,7 @@ export class MonacoCompletionProvider { public async provideInlineCompletions( model: monaco.editor.ITextModel, monacoPosition: monaco.Position, - token: CancellationToken + token: CancellationToken, ): Promise< | monaco.languages.InlineCompletions | undefined @@ -105,7 +118,7 @@ export class MonacoCompletionProvider { const signal = abortController.signal; this.setStatus(Status.PROCESSING); - this.setMessage("Generating completions..."); + this.setMessage('Generating completions...'); const documentInfo = this.getDocumentInfo(document, position); const editorOptions = { @@ -113,19 +126,36 @@ export class MonacoCompletionProvider { insertSpaces: model.getOptions().insertSpaces, }; + let includedOtherDocs = this.otherDocuments; + if (includedOtherDocs.length > 10) { + console.warn( + `Too many other documents: ${includedOtherDocs.length} (max 10)`, + ); + includedOtherDocs = includedOtherDocs.slice(0, 10); + } + + let multilineConfig: MultilineConfig | undefined = undefined; + if (this.multilineModelThreshold !== undefined) { + multilineConfig = new MultilineConfig({ + threshold: this.multilineModelThreshold, + }); + } + // Get completions. let getCompletionsResponse: GetCompletionsResponse; try { getCompletionsResponse = await this.client.getCompletions( { - metadata: this.metadata, + metadata: this.getMetadata(), document: documentInfo, editorOptions: editorOptions, + otherDocuments: includedOtherDocs, + multilineConfig, }, { signal, - headers: this.authHeader, - } + headers: this.getAuthHeader(), + }, ); } catch (err) { // Handle cancellation. @@ -133,13 +163,13 @@ export class MonacoCompletionProvider { // cancelled } else { this.setStatus(Status.ERROR); - this.setMessage("Something went wrong; please try again."); + this.setMessage('Something went wrong; please try again.'); } return undefined; } if (!getCompletionsResponse.completionItems) { // TODO(nick): Distinguish warning / error states here. - const message = " No completions were generated"; + const message = ' No completions were generated'; this.setStatus(Status.SUCCESS); this.setMessage(message); return undefined; @@ -149,7 +179,7 @@ export class MonacoCompletionProvider { // Create inline completion items from completions. const inlineCompletionItems = completionItems .map((completionItem) => - this.createInlineCompletionItem(completionItem, document) + this.createInlineCompletionItem(completionItem, document), ) .filter((item) => !!item); @@ -172,17 +202,20 @@ export class MonacoCompletionProvider { */ public acceptedLastCompletion(completionId: string) { new Promise((resolve, reject) => { - this.client.acceptCompletion( - { - metadata: this.metadata, - completionId: completionId, - }, - { - headers: this.authHeader, - } - ).then(resolve).catch((err) => { - console.log("Error: ", err) - }); + this.client + .acceptCompletion( + { + metadata: this.getMetadata(), + completionId: completionId, + }, + { + headers: this.getAuthHeader(), + }, + ) + .then(resolve) + .catch((err) => { + console.log('Error: ', err); + }); }); } @@ -195,19 +228,24 @@ export class MonacoCompletionProvider { */ private getDocumentInfo( document: Document, - position: Position + position: Position, ): DocumentInfo { // The offset is measured in bytes. const text = document.getText(); const numCodeUnits = document.offsetAt(position); const offset = numCodeUnitsToNumUtf8Bytes(text, numCodeUnits); + const language = languageIdToEnum(document.languageId); + if (language === Language.UNSPECIFIED) { + console.warn(`Unknown language: ${document.languageId}`); + } + const documentInfo = new DocumentInfo({ text: text, editorLanguage: document.languageId, - language: Language.PYTHON, + language, cursorOffset: BigInt(offset), - lineEnding: "\n", + lineEnding: '\n', }); return documentInfo; @@ -222,7 +260,7 @@ export class MonacoCompletionProvider { */ private createInlineCompletionItem( completionItem: CompletionItem, - document: Document + document: Document, ): MonacoInlineCompletion | undefined { if (!completionItem.completion || !completionItem.range) { return undefined; @@ -231,17 +269,20 @@ export class MonacoCompletionProvider { // Create and return inlineCompletionItem. const text = document.getText(); const startPosition = document.positionAt( - numUtf8BytesToNumCodeUnits(text, Number(completionItem.range.startOffset)) + numUtf8BytesToNumCodeUnits( + text, + Number(completionItem.range.startOffset), + ), ); const endPosition = document.positionAt( - numUtf8BytesToNumCodeUnits(text, Number(completionItem.range.endOffset)) + numUtf8BytesToNumCodeUnits(text, Number(completionItem.range.endOffset)), ); const range = new Range(startPosition, endPosition); const inlineCompletionItem = new MonacoInlineCompletion( completionItem.completion.text, range, - completionItem.completion.completionId + completionItem.completion.completionId, ); return inlineCompletionItem; } diff --git a/src/components/CodeiumEditor/Document.ts b/src/components/CodeiumEditor/Document.ts index e384364..640d0a1 100644 --- a/src/components/CodeiumEditor/Document.ts +++ b/src/components/CodeiumEditor/Document.ts @@ -1,7 +1,7 @@ -import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; -import { Position, Range } from "./Location"; -import { Line } from "./Line"; +import { Position, Range } from './Location'; +import { Line } from './Line'; export class Document { private model: monaco.editor.ITextModel; @@ -20,7 +20,7 @@ export class Document { } public lineAt(positionOrLine: Position | number): Line { - if (typeof positionOrLine !== "number") { + if (typeof positionOrLine !== 'number') { positionOrLine = positionOrLine.line; } return new Line( @@ -29,9 +29,9 @@ export class Document { new Position(positionOrLine, 0), new Position( positionOrLine, - this.model.getLineLength(positionOrLine + 1) - ) - ) + this.model.getLineLength(positionOrLine + 1), + ), + ), ); } diff --git a/src/components/CodeiumEditor/InlineCompletionProvider.ts b/src/components/CodeiumEditor/InlineCompletionProvider.ts index 445e3b9..f8be118 100644 --- a/src/components/CodeiumEditor/InlineCompletionProvider.ts +++ b/src/components/CodeiumEditor/InlineCompletionProvider.ts @@ -1,11 +1,12 @@ -import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; -import { Dispatch, SetStateAction } from "react"; -import { PromiseClient } from "@connectrpc/connect"; -import { Status } from "./Status"; -import { MonacoCompletionProvider } from "./CompletionProvider"; -import { LanguageServerService } from "../../api/proto/exa/language_server_pb/language_server_connect"; +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import { Document as DocumentInfo } from '../../api/proto/exa/language_server_pb/language_server_pb'; +import { Dispatch, SetStateAction } from 'react'; +import { PromiseClient } from '@connectrpc/connect'; +import { Status } from './Status'; +import { MonacoCompletionProvider } from './CompletionProvider'; +import { LanguageServerService } from '../../api/proto/exa/language_server_pb/language_server_connect'; -declare module "monaco-editor" { +declare module 'monaco-editor' { namespace editor { interface ICodeEditor { _commandService: { executeCommand(command: string): unknown }; @@ -18,19 +19,22 @@ export class InlineCompletionProvider { private numCompletionsProvided: number; readonly completionProvider: MonacoCompletionProvider; + constructor( grpcClient: PromiseClient, readonly setCompletionCount: Dispatch>, setCodeiumStatus: Dispatch>, setCodeiumStatusMessage: Dispatch>, - apiKey?: string | undefined + apiKey?: string | undefined, + multilineModelThreshold?: number | undefined, ) { this.numCompletionsProvided = 0; this.completionProvider = new MonacoCompletionProvider( grpcClient, setCodeiumStatus, setCodeiumStatusMessage, - apiKey + apiKey, + multilineModelThreshold, ); } @@ -42,12 +46,12 @@ export class InlineCompletionProvider model: monaco.editor.ITextModel, position: monaco.Position, context: monaco.languages.InlineCompletionContext, - token: monaco.CancellationToken + token: monaco.CancellationToken, ) { const completions = await this.completionProvider.provideInlineCompletions( model, position, - token + token, ); // Only count completions provided if non-empty (i.e. exclude cancelled // requests). @@ -63,4 +67,8 @@ export class InlineCompletionProvider public acceptedLastCompletion(completionId: string) { this.completionProvider.acceptedLastCompletion(completionId); } + + public updateOtherDocuments(otherDocuments: DocumentInfo[]) { + this.completionProvider.otherDocuments = otherDocuments; + } } diff --git a/src/components/CodeiumEditor/Line.ts b/src/components/CodeiumEditor/Line.ts index 68332f2..474e266 100644 --- a/src/components/CodeiumEditor/Line.ts +++ b/src/components/CodeiumEditor/Line.ts @@ -1,4 +1,4 @@ -import { Range } from "./Location"; +import { Range } from './Location'; export class Line { readonly text: string; diff --git a/src/components/CodeiumEditor/Location.ts b/src/components/CodeiumEditor/Location.ts index e6a4f59..0e9f3a5 100644 --- a/src/components/CodeiumEditor/Location.ts +++ b/src/components/CodeiumEditor/Location.ts @@ -1,4 +1,4 @@ -import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; export class Position implements monaco.IPosition { readonly line: number; @@ -48,7 +48,7 @@ export class Range implements monaco.IRange { static fromMonaco(range: monaco.IRange): Range { return new Range( new Position(range.startLineNumber - 1, range.startColumn - 1), - new Position(range.endLineNumber - 1, range.endColumn - 1) + new Position(range.endLineNumber - 1, range.endColumn - 1), ); } diff --git a/src/components/CodeiumEditor/Status.ts b/src/components/CodeiumEditor/Status.ts index cc707b1..357f1f0 100644 --- a/src/components/CodeiumEditor/Status.ts +++ b/src/components/CodeiumEditor/Status.ts @@ -2,11 +2,11 @@ * Status of the Codeium AI completions generation. */ export enum Status { - INACTIVE = "inactive", - PROCESSING = "processing", - SUCCESS = "success", - WARNING = "warning", - ERROR = "error", + INACTIVE = 'inactive', + PROCESSING = 'processing', + SUCCESS = 'success', + WARNING = 'warning', + ERROR = 'error', } /** diff --git a/src/components/CodeiumEditor/defaultValues.ts b/src/components/CodeiumEditor/defaultValues.ts index 8e01b01..a931253 100644 --- a/src/components/CodeiumEditor/defaultValues.ts +++ b/src/components/CodeiumEditor/defaultValues.ts @@ -1,24 +1,24 @@ export const getDefaultValue = (language: string): string => { switch (language) { - case "typescript": - case "tsx": - case "javascript": - case "java": + case 'typescript': + case 'tsx': + case 'javascript': + case 'java': return `// Welcome to Codeium Editor! // Press Enter and use Tab to accept AI suggestions. Here's an example: // fib(n) function to calculate the n-th fibonacci number`; - case "python": + case 'python': return `# Welcome to Codeium Editor! # Press Enter and use Tab to accept AI suggestions. Here's an example: # fib(n) function to calculate the n-th fibonacci number`; - case "css": + case 'css': return `/* Welcome to Codeium Editor! Press Enter and use Tab to accept AI suggestions. Here's an example:*/ /* .action-button class with a hover effect. */`; default: - return ""; + return ''; } }; diff --git a/src/components/CodeiumEditor/types.ts b/src/components/CodeiumEditor/types.ts index 24d695c..7a17823 100644 --- a/src/components/CodeiumEditor/types.ts +++ b/src/components/CodeiumEditor/types.ts @@ -1,7 +1,7 @@ import { Completion, CompletionSource, -} from "../../api/proto/exa/codeium_common_pb/codeium_common_pb"; +} from '../../api/proto/exa/codeium_common_pb/codeium_common_pb'; export type CompletionAndRange = { completion: Completion; diff --git a/src/components/CodeiumLogo/CodeiumLogo.tsx b/src/components/CodeiumLogo/CodeiumLogo.tsx index 7252fef..02cff27 100644 --- a/src/components/CodeiumLogo/CodeiumLogo.tsx +++ b/src/components/CodeiumLogo/CodeiumLogo.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React from 'react'; type CodeiumLogoProps = { className?: string; @@ -30,7 +30,7 @@ export const CodeiumLogo: React.FC = ({ = ({ d="M75.196 77.1134C74.1902 77.1134 73.3102 76.7612 72.5559 76.0566C71.8375 75.3169 71.4783 74.4538 71.4783 73.4675C71.4783 72.4459 71.8375 71.5828 72.5559 70.8783C73.3102 70.1386 74.1902 69.7687 75.196 69.7687C76.2377 69.7687 77.1177 70.1386 77.8361 70.8783C78.5545 71.5828 78.9137 72.4459 78.9137 73.4675C78.9137 74.4538 78.5545 75.3169 77.8361 76.0566C77.1177 76.7612 76.2377 77.1134 75.196 77.1134Z" fill="white" aria-label="right-dot" - className={loading ? "animate-blink duration-1000" : ""} - style={{ animationDelay: "666ms" }} + className={loading ? 'animate-blink duration-1000' : ''} + style={{ animationDelay: '666ms' }} /> = { - title: "Example/Editor", - component: CodeiumEditor, - parameters: { - // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout - layout: "centered", - }, - // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs - tags: ["autodocs"], - // More on argTypes: https://storybook.js.org/docs/api/argtypes - argTypes: {}, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -const baseParams = { - width: "700px", - height: "500px", -} - -const PYTHON_SNIPPET = `# Need inspiration? Try adding extra constraints or context to this parse json function! -# Or scratch everything and use your imagination. - -def parse_json_lines(filename: str) -> List[Any]: - output = [] - with open(filename, "r", encoding="utf-8") as f: -` - -// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args -export const PythonEditor: Story = { - args: { - ...baseParams, - language: "python", - value: PYTHON_SNIPPET - }, -}; - -const JAVASCRIPT_SNIPPET = ` -// Need inspiration? Try common JavaScript utilities like debounce, -// date validation, or number to currency! - -// Convert HTML string to DOM object -function parseStringAsHtml(content, selector) { -` - -export const JavaScriptEditor: Story = { - args: { - ...baseParams, - language: "javascript", - value: JAVASCRIPT_SNIPPET - }, -}; - -const GO_SNIPPET = ` -// Need inspiration? See how fast you can set up and use a logger. -// Or scratch everything and use your imagination. - -package main - -import ( - "fmt" - "log" -) - -func main() { - // Configure log and create a new logger -` -export const GoEditor: Story = { - args: { - ...baseParams, - language: "go", - value: GO_SNIPPET - }, -}; - -const JAVA_SNIPPET = ` -// Need inspiration? Try adding additional conditions or other helper -// functions on "widgets." -// Or scratch everything and use your imagination. - -/** - * @return ArrayList of all visible widgets - */ -public ArrayList getVisibleWidgets() { -` - -export const JavaEditor: Story = { - args: { - ...baseParams, - language: "java", - value: JAVA_SNIPPET - }, -}; - - -const CPP_SNIPPET = ` -// Need inspiration? Try finishing this Matrix class with constructors, -// destructors, getter/setters, utilities like transpose, etc. -// Or scratch everything and use your imagination. - -// 2D Matrix class -template -class Matrix() { - public: - Matrix(int rows, int cols) { -` - -export const CppEditor: Story = { - args: { - ...baseParams, - language: "cpp", - value: CPP_SNIPPET - }, -}; - diff --git a/src/stories/CodeiumEditor.stories.tsx b/src/stories/CodeiumEditor.stories.tsx new file mode 100644 index 0000000..ae69398 --- /dev/null +++ b/src/stories/CodeiumEditor.stories.tsx @@ -0,0 +1,275 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; + +import { CodeiumEditor } from '../components'; +import { Document, Language } from '../models'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta: Meta = { + title: 'Example/Editor', + component: CodeiumEditor, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const baseParams = { + width: '700px', + height: '500px', +}; + +const PYTHON_SNIPPET = `# Need inspiration? Try adding extra constraints or context to this parse json function! +# Or scratch everything and use your imagination. + +def parse_json_lines(filename: str) -> List[Any]: + output = [] + with open(filename, "r", encoding="utf-8") as f: +`; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const PythonEditor: Story = { + args: { + ...baseParams, + language: 'python', + value: PYTHON_SNIPPET, + }, +}; + +const JAVASCRIPT_SNIPPET = ` +// Need inspiration? Try common JavaScript utilities like debounce, +// date validation, or number to currency! + +// Convert HTML string to DOM object +function parseStringAsHtml(content, selector) { +`; + +export const JavaScriptEditor: Story = { + args: { + ...baseParams, + language: 'javascript', + value: JAVASCRIPT_SNIPPET, + }, +}; + +const GO_SNIPPET = ` +// Need inspiration? See how fast you can set up and use a logger. +// Or scratch everything and use your imagination. + +package main + +import ( + "fmt" + "log" +) + +func main() { + // Configure log and create a new logger +`; +export const GoEditor: Story = { + args: { + ...baseParams, + language: 'go', + value: GO_SNIPPET, + }, +}; + +const JAVA_SNIPPET = ` +// Need inspiration? Try adding additional conditions or other helper +// functions on "widgets." +// Or scratch everything and use your imagination. + +/** + * @return ArrayList of all visible widgets + */ +public ArrayList getVisibleWidgets() { +`; + +export const JavaEditor: Story = { + args: { + ...baseParams, + language: 'java', + value: JAVA_SNIPPET, + }, +}; + +const CPP_SNIPPET = ` +// Need inspiration? Try finishing this Matrix class with constructors, +// destructors, getter/setters, utilities like transpose, etc. +// Or scratch everything and use your imagination. + +// 2D Matrix class +template +class Matrix() { + public: + Matrix(int rows, int cols) { +`; + +export const CppEditor: Story = { + args: { + ...baseParams, + language: 'cpp', + value: CPP_SNIPPET, + }, +}; + +const MARKDOWN_SNIPPET = `# Readme Template +Author: [Your Name] + +This is a template for your README.md file. + +## Features + +List of features:`; + +export const MarkdownEditor: Story = { + args: { + ...baseParams, + language: 'markdown', + value: MARKDOWN_SNIPPET, + }, +}; + +export const PlainTextEditor: Story = { + decorators: (Story) => ( +
+

Plain Text

+