Skip to content

Commit

Permalink
extract PromptEditor to @sourcegraph/prompt-editor internal lib (#5031)
Browse files Browse the repository at this point in the history
This makes it possible to reuse this component on https://openctx.org
and in the [Prompt Library](https://sourcegraph.com/prompts) editing UI.

No behavioral change.

~85% of the diff additions are just pnpm-lock.yaml.

## Test plan

CI
  • Loading branch information
sqs authored Jul 28, 2024
1 parent 921b68a commit 50d9a01
Show file tree
Hide file tree
Showing 96 changed files with 1,094 additions and 495 deletions.
2 changes: 1 addition & 1 deletion .config/viteShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const defaultProjectConfig: UserWorkspaceConfig = {
// Build from TypeScript sources so we don't need to run `tsc -b` in the background
// during dev.
{
find: /^(@sourcegraph\/cody-[\w-]+)$/,
find: /^(@sourcegraph\/(?:cody-[\w-]|prompt-editor)+)$/,
replacement: '$1/src/index.ts',
},
],
Expand Down
7 changes: 7 additions & 0 deletions lib/prompt-editor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Shared prompt editor UI component

The `@sourcegraph/prompt-editor` package contains the code for the prompt editor UI component.

## Development notes

- Put [UI component storybooks](https://storybook.js.org/) in `vscode/webviews/promptEditor`, not here, so that these components' storybooks can use the VS Code theme switching that we have for those storybooks.
47 changes: 47 additions & 0 deletions lib/prompt-editor/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@sourcegraph/prompt-editor",
"version": "0.0.0-dev",
"description": "Shared prompt editor UI component",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/sourcegraph/cody",
"directory": "lib/prompt-editor"
},
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist", "src", "!**/*.test.*"],
"sideEffects": false,
"scripts": {
"build": "tsc --build",
"test": "vitest",
"prepublishOnly": "tsc --build --clean && pnpm run build"
},
"dependencies": {
"@floating-ui/react": "^0.26.9",
"@lexical/code": "^0.16.0",
"@lexical/react": "^0.16.0",
"@lexical/text": "^0.16.0",
"@lexical/utils": "^0.16.0",
"@sourcegraph/cody-shared": "workspace:*",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"lexical": "^0.16.0",
"lodash": "^4.17.21",
"lru-cache": "^10.0.0",
"lucide-react": "^0.378.0",
"vscode-uri": "^3.0.7"
},
"devDependencies": {
"@types/lodash": "^4.17.7",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"peerDependencies": {
"react": "^16.8.0 ^17 ^18",
"react-dom": "^16.8.0 ^17 ^18"
}
}
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { EditorState, LexicalEditor, SerializedEditorState } from 'lexical'
import { type FunctionComponent, type RefObject, useMemo } from 'react'
import styles from './BaseEditor.module.css'
import { RICH_EDITOR_NODES } from './nodes'
import MentionsPlugin from './plugins/atMentions/atMentions'
import { MentionsPlugin } from './plugins/atMentions/atMentions'
import CodeHighlightPlugin from './plugins/codeHighlight'
import { DisableEscapeKeyBlursPlugin } from './plugins/disableEscapeKeyBlurs'
import { KeyboardEventPlugin, type KeyboardEventPluginProps } from './plugins/keyboardEvent'
Expand All @@ -21,6 +21,7 @@ interface Props extends KeyboardEventPluginProps {
initialEditorState: SerializedEditorState | null
onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set<string>) => void
onFocusChange?: (focused: boolean) => void
contextWindowSizeInTokens?: number
editorRef?: RefObject<LexicalEditor>
placeholder?: string
disabled?: boolean
Expand All @@ -36,6 +37,7 @@ export const BaseEditor: FunctionComponent<Props> = ({
initialEditorState,
onChange,
onFocusChange,
contextWindowSizeInTokens,
editorRef,
placeholder,
disabled,
Expand Down Expand Up @@ -86,7 +88,7 @@ export const BaseEditor: FunctionComponent<Props> = ({
// our tests using JSDOM. It doesn't hurt to enable otherwise.
ignoreHistoryMergeTagChange={false}
/>
<MentionsPlugin />
<MentionsPlugin contextWindowSizeInTokens={contextWindowSizeInTokens} />
<CodeHighlightPlugin />
{onFocusChange && <OnFocusChangePlugin onFocusChange={onFocusChange} />}
{editorRef && <EditorRefPlugin editorRef={editorRef} />}
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@ import { $createTextNode, $getRoot, $getSelection, $insertNodes, type LexicalEdi
import type { EditorState, SerializedEditorState, SerializedLexicalNode } from 'lexical'
import { isEqual } from 'lodash'
import { type FunctionComponent, useCallback, useEffect, useImperativeHandle, useRef } from 'react'
import {
isEditorContentOnlyInitialContext,
lexicalNodesForContextItems,
} from '../chat/cells/messageCell/human/editor/initialContext'
import { BaseEditor } from './BaseEditor'
import styles from './PromptEditor.module.css'
import { useSetGlobalPromptEditorConfig } from './config'
import { isEditorContentOnlyInitialContext, lexicalNodesForContextItems } from './initialContext'
import { $selectAfter, $selectEnd } from './lexicalUtils'
import type { KeyboardEventPluginProps } from './plugins/keyboardEvent'

Expand All @@ -29,6 +27,8 @@ interface Props extends KeyboardEventPluginProps {
onChange?: (value: SerializedPromptEditorValue) => void
onFocusChange?: (focused: boolean) => void

contextWindowSizeInTokens?: number

disabled?: boolean

editorRef?: React.RefObject<PromptEditorRefAPI>
Expand All @@ -54,6 +54,7 @@ export const PromptEditor: FunctionComponent<Props> = ({
initialEditorState,
onChange,
onFocusChange,
contextWindowSizeInTokens,
disabled,
editorRef: ref,
onEnterKey,
Expand Down Expand Up @@ -161,6 +162,8 @@ export const PromptEditor: FunctionComponent<Props> = ({
[]
)

useSetGlobalPromptEditorConfig()

const onBaseEditorChange = useCallback(
(_editorState: EditorState, editor: LexicalEditor, tags: Set<string>): void => {
if (onChange) {
Expand Down Expand Up @@ -193,6 +196,7 @@ export const PromptEditor: FunctionComponent<Props> = ({
initialEditorState={initialEditorState?.lexicalEditorState ?? null}
onChange={onBaseEditorChange}
onFocusChange={onFocusChange}
contextWindowSizeInTokens={contextWindowSizeInTokens}
editorRef={editorRef}
placeholder={placeholder}
disabled={disabled}
Expand Down
17 changes: 17 additions & 0 deletions lib/prompt-editor/src/clientState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { ClientStateForWebview } from '@sourcegraph/cody-shared'
import { createContext, useContext } from 'react'

const ClientStateContext = createContext<ClientStateForWebview | null>(null)

export const ClientStateContextProvider = ClientStateContext.Provider

/**
* Get the {@link ClientState} stored in React context.
*/
export function useClientState(): ClientStateForWebview {
const clientState = useContext(ClientStateContext)
if (!clientState) {
throw new Error('no clientState')
}
return clientState
}
81 changes: 81 additions & 0 deletions lib/prompt-editor/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { SerializedContextItem } from '@sourcegraph/cody-shared'
import type { Command } from 'cmdk'
import { type ComponentProps, type ComponentType, createContext, useContext } from 'react'

/**
* The configuration for the prompt editor and related components.
*/
export interface PromptEditorConfig {
onContextItemMentionNodeMetaClick?: (contextItem: SerializedContextItem) => void

/**
* The [shadcn Tooltip components](https://ui.shadcn.com/docs/components/tooltip).
*/
tooltipComponents?: {
Tooltip: React.ComponentType<{ children: React.ReactNode }>
TooltipContent: React.ComponentType<{ children: React.ReactNode }>
TooltipTrigger: React.ComponentType<{ asChild?: boolean; children: React.ReactNode }>
}

/**
* The [shadcn Command components](https://ui.shadcn.com/docs/components/command).
*/
commandComponents: {
Command: ComponentType<ComponentProps<typeof Command>>
CommandInput: typeof Command.Input
CommandList: typeof Command.List
CommandEmpty: typeof Command.Empty
CommandLoading: typeof Command.Loading
CommandGroup: typeof Command.Group
CommandSeparator: typeof Command.Separator
CommandItem: typeof Command.Item
}
}

const PromptEditorConfigContext = createContext<PromptEditorConfig | undefined>(undefined)

/**
* React hook for setting the configuration for the prompt editor and related components.
*/
export const PromptEditorConfigProvider = PromptEditorConfigContext.Provider

export function usePromptEditorConfig(): PromptEditorConfig {
const config = useContext(PromptEditorConfigContext)
if (!config) {
throw new Error('usePromptEditorConfig must be called within a PromptEditorConfigProvider')
}
return config
}

/**
* This hook must be called somewhere in the render tree. It is to apply config that can't be passed
* via React context. Lexical nodes are rendered in disconnected React DOM trees, so the context
* won't pass down.
*/
export function useSetGlobalPromptEditorConfig(): void {
const config = useContext(PromptEditorConfigContext)
if (!config) {
throw new Error('useApplyPromptEditorConfig must be called within a PromptEditorConfigProvider')
}
setGlobalPromptEditorConfig(config)
}

/** The subset of the config that must be accessed globally (i.e. not passed via React context). */
type GlobalConfig = Pick<PromptEditorConfig, 'onContextItemMentionNodeMetaClick' | 'tooltipComponents'>

let globalConfig: GlobalConfig | undefined

function setGlobalPromptEditorConfig(config: GlobalConfig): void {
globalConfig = config
}

/**
* Return the global prompt editor config. Use {@link usePromptEditorConfig} from React. Only use
* this if you need to access it outside of a React render tree.
*/
export function getGlobalPromptEditorConfig(): GlobalConfig {
if (!globalConfig) {
throw new Error('getGlobalPromptEditorConfig must be called after setGlobalPromptEditorConfig')
}
return globalConfig
}
10 changes: 10 additions & 0 deletions lib/prompt-editor/src/globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
declare module '*.module.css' {
const classes: { readonly [key: string]: string }
export default classes
}

declare module '*.svg?react' {
// The path to the resource
const component: React.ComponentType<React.SVGProps<SVGSVGElement>>
export default component
}
15 changes: 15 additions & 0 deletions lib/prompt-editor/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export { PromptEditor, type PromptEditorRefAPI } from './PromptEditor'
export { ContextItemMentionNode, MENTION_CLASS_NAME } from './nodes/ContextItemMentionNode'
export { MentionMenu } from './mentions/mentionMenu/MentionMenu'
export { BaseEditor } from './BaseEditor'
export { type MentionMenuData, type MentionMenuParams } from './mentions/mentionMenu/useMentionMenuData'
export {
ChatContextClientProvider,
type ChatContextClient,
useChatContextMentionProviders,
ChatMentionContext,
type ChatMentionsSettings,
} from './plugins/atMentions/chatContextClient'
export { dummyChatContextClient } from './plugins/atMentions/fixtures'
export { type PromptEditorConfig, PromptEditorConfigProvider } from './config'
export { useClientState, ClientStateContextProvider } from './clientState'
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import type { ContextItem } from '@sourcegraph/cody-shared'
import { $createTextNode, $getRoot, type LexicalEditor, TextNode } from 'lexical'
import {
$createContextItemMentionNode,
ContextItemMentionNode,
} from '../../../../../promptEditor/nodes/ContextItemMentionNode'
import { $createContextItemMentionNode, ContextItemMentionNode } from './nodes/ContextItemMentionNode'

export function lexicalNodesForContextItems(
items: ContextItem[],
Expand Down
File renamed without changes.
Loading

0 comments on commit 50d9a01

Please sign in to comment.