Skip to content

Commit

Permalink
fix mentions menu
Browse files Browse the repository at this point in the history
  • Loading branch information
fkling committed Dec 20, 2024
1 parent 2948fa8 commit a07dc37
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 48 deletions.
2 changes: 0 additions & 2 deletions lib/prompt-editor/src/v2/MentionsMenu.module.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
.popover-dimensions {
margin-top: 0.5rem;
width: clamp(300px, 65vw, 440px);

/*
Expand All @@ -14,6 +13,5 @@
border-radius: 0.25rem;
box-shadow: 0px 20px 25px -5px var(--color-shadow-10, rgba(15, 17, 26, 0.10)), 0px 8px 10px -6px var(--color-shadow-10, rgba(15, 17, 26, 0.10));
max-height: 300px;
overflow: auto;
z-index: 1;
}
19 changes: 11 additions & 8 deletions lib/prompt-editor/src/v2/MentionsMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { shift, size, useFloating } from '@floating-ui/react'
import { offset, shift, size, useFloating } from '@floating-ui/react'
import clsx from 'clsx'
import { type MouseEventHandler, useCallback, useEffect, useLayoutEffect, useRef } from 'react'
import styles from './MentionsMenu.module.css'
Expand Down Expand Up @@ -51,14 +51,18 @@ export const MentionsMenu = <T,>({
open: true,
placement: 'bottom-start',
middleware: [
shift(),
shift({
padding: 8,
}),
offset({ mainAxis: 4 }),
size({
apply({ availableWidth, availableHeight, elements }) {
Object.assign(elements.floating.style, {
maxWidth: `${availableWidth}px`,
maxHeight: `${availableHeight}px`,
})
},
padding: 8,
}),
],
})
Expand Down Expand Up @@ -96,7 +100,7 @@ export const MentionsMenu = <T,>({
const handleClick: MouseEventHandler = useCallback(
event => {
const target = event.target as HTMLElement | null
const listNode = target?.closest('li') as HTMLLIElement | null
const listNode = target?.closest('[role=option]') as HTMLElement | null
if (listNode?.parentNode) {
const options = listNode.parentNode.querySelectorAll('[role="option"]')
// @ts-expect-error - TS doesn't like this but it's OK
Expand Down Expand Up @@ -138,9 +142,7 @@ export const MentionsMenu = <T,>({
))}
</div>
{items.length === 0 && (
<div className={itemClass} data-disabled="true">
{getEmptyLabel()}
</div>
<div className="tw-py-3 tw-px-2 tw-text-md tw-opacity-50">{getEmptyLabel()}</div>
)}
</div>
)
Expand All @@ -149,7 +151,8 @@ export const MentionsMenu = <T,>({
const headerClass =
'!tw-p-0 !tw-border-b-0 !tw-p-3 !tw-text-md !tw-leading-[1.2] !tw-h-[30px] tw-opacity-50'

const menuClass = 'tw-overflow-hidden tw-rounded-md tw-bg-popover tw-text-popover-foreground'
const menuClass =
'tw-overflow-hidden tw-overflow-y-auto tw-rounded-md tw-bg-popover tw-text-popover-foreground'

const itemClass =
'tw-relative tw-flex tw-cursor-pointer tw-select-none tw-items-center tw-py-3 tw-px-2 tw-text-md tw-outline-none aria-selected:tw-bg-accent aria-selected:tw-text-accent-foreground hover:tw-bg-accent hover:tw-text-accent-foreground data-[disabled=true]:tw-pointer-events-none data-[disabled=true]:tw-opacity-50 !tw-p-3 !tw-text-md !tw-leading-[1.2] !tw-h-[30px] !tw-rounded-none'
'w-relative tw-cursor-pointer tw-select-none tw-items-center tw-py-3 tw-px-2 tw-text-md tw-outline-none aria-selected:tw-bg-accent aria-selected:tw-text-accent-foreground hover:tw-bg-accent hover:tw-text-accent-foreground data-[disabled=true]:tw-pointer-events-none data-[disabled=true]:tw-opacity-50 !tw-p-3 !tw-text-md !tw-leading-[1.2] !tw-h-[30px] !tw-rounded-none'
8 changes: 5 additions & 3 deletions lib/prompt-editor/src/v2/PromptEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@ export const PromptEditor: FunctionComponent<Props> = ({
const mentionSettings = useContext(ChatMentionContext)

const fetchMenuData = useCallback(
({ query, parent }: { query: string; parent?: ContextMentionProviderMetadata }) => {
({ query, provider }: { query: string; provider?: ContextMentionProviderMetadata }) => {
const initialContext = [...defaultContext.initialContext, ...defaultContext.corpusContext]
const queryLower = query.toLowerCase().trim()
const filteredInitialContextItems = parent
const filteredInitialContextItems = provider
? []
: initialContext.filter(item =>
queryLower
Expand All @@ -118,7 +118,7 @@ export const PromptEditor: FunctionComponent<Props> = ({

return Observable.of(filteredInitialContextItems).concat(
mentionMenuData({
...parseMentionQuery(query, parent ?? null),
...parseMentionQuery(query, provider ?? null),
interactionID: interactionID.current,
contextRemoteRepositoriesNames: mentionSettings.remoteRepositoriesNames,
}).map(result => [
Expand Down Expand Up @@ -222,6 +222,8 @@ export const PromptEditor: FunctionComponent<Props> = ({
[styles.disabled]: disabled,
[styles.seamless]: seamless,
})}
//For compatibility with the CSS rules that target this attribute
data-lexical-editor="true"
>
<div className={clsx(styles.input, contentEditableClassName)} ref={api.ref} />
{show && (
Expand Down
2 changes: 1 addition & 1 deletion lib/prompt-editor/src/v2/plugins/atMention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export function createAtMentionPlugin(): Plugin[] {
// The current cursor position is after the '@' character
position - 1,
position,
{ class: styles.active },
{ class: styles.atMention },
// This is necessary so that mapping changes will 'grow' the decoration, which
// also acts as marker for the mention value
{ inclusiveEnd: true }
Expand Down
86 changes: 52 additions & 34 deletions lib/prompt-editor/src/v2/promptInput.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
type ContextItem,
ContextItemSource,
type ContextMentionProviderMetadata,
REMOTE_DIRECTORY_PROVIDER_URI,
REMOTE_FILE_PROVIDER_URI,
type SerializedContextItem,
Expand Down Expand Up @@ -107,10 +108,10 @@ function createAtMention(editor: PromptInputActor): { type: (value: string) => v
}
}

function mockFetchMenuDataResult(items: MenuItem[]): void {
fetchMenuData.mockImplementation(({ input }) => {
function mockFetchMenu(items: MenuItem[]): (args: { input: DataLoaderInput }) => void {
return ({ input }) => {
input.parent.send({ type: 'mentionsMenu.results.set', items })
})
}
}

beforeEach(() => {
Expand Down Expand Up @@ -223,10 +224,12 @@ describe('mentions menu', () => {
})

test('calls fetch function and updates available items', () => {
mockFetchMenuDataResult([
{ type: 'file', uri: URI.parse('file:///file1.txt') },
{ type: 'file', uri: URI.parse('file:///file2.txt') },
])
fetchMenuData.mockImplementation(
mockFetchMenu([
{ type: 'file', uri: URI.parse('file:///file1.txt') },
{ type: 'file', uri: URI.parse('file:///file2.txt') },
])
)

const editor = createInput(['test '])
const mention = createAtMention(editor)
Expand Down Expand Up @@ -291,26 +294,39 @@ describe('mentions menu', () => {
})

test('apply provider', () => {
let receivedInput: string | undefined
let receivedProviderID: string | undefined

fetchMenuData.mockImplementation(({ input }) => {
receivedInput = input.query
receivedProviderID = input.context?.id
})
const provider: ContextMentionProviderMetadata = {
id: 'some-provider',
title: 'provider',
queryLabel: 'query',
emptyLabel: 'empty',
}
const item: ContextItem = { type: 'file', uri: URI.parse('file:///file.txt') }
fetchMenuData
.mockImplementationOnce(mockFetchMenu([provider]))
.mockImplementationOnce(mockFetchMenu([provider]))
.mockImplementationOnce(mockFetchMenu([item]))
const editor = createInput(['test '])
createAtMention(editor)
createAtMention(editor).type('file')
clock.increment(DEBOUNCE_TIME)

editor.send({
type: 'atMention.apply',
item: { id: 'some-provider', title: 'provider', queryLabel: 'query', emptyLabel: 'empty' },
type: 'mentionsMenu.apply',
index: 0,
})

expect.soft(getText(editor), 'selection is cleared').toBe('test @')
expect(getText(editor), 'selection is cleared').toBe('test @')

expect(fetchMenuData).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ input: expect.objectContaining({ query: 'file' }) })
)
expect(fetchMenuData).toHaveBeenNthCalledWith(
3,
expect.objectContaining({ input: expect.objectContaining({ query: '', context: provider }) })
)

expect.soft(receivedInput).toBe('')
expect.soft(receivedProviderID).toBe('some-provider')
expect(hasAtMention(getEditorState(editor))).toBe(true)
expect(editor.getSnapshot().context.mentionsMenu.items).toEqual([item])
})

test('apply large file without range', () => {
Expand Down Expand Up @@ -347,20 +363,22 @@ describe('mentions menu', () => {
})

test('menu items are updated according to currently available context size', () => {
mockFetchMenuDataResult([
{
type: 'file',
uri: URI.parse('file:///file1.txt'),
size: 5,
source: ContextItemSource.User,
},
{
type: 'file',
uri: URI.parse('file:///file2.txt'),
size: 2,
source: ContextItemSource.User,
},
])
fetchMenuData.mockImplementation(
mockFetchMenu([
{
type: 'file',
uri: URI.parse('file:///file1.txt'),
size: 5,
source: ContextItemSource.User,
},
{
type: 'file',
uri: URI.parse('file:///file2.txt'),
size: 2,
source: ContextItemSource.User,
},
])
)

const editor = createInput(
[
Expand Down

0 comments on commit a07dc37

Please sign in to comment.