mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 03:56:55 +03:00
fix(app): reuse mention suggestions while typing (#63572)
This commit is contained in:
@@ -2,6 +2,7 @@ import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createResource,
|
||||
createSelector,
|
||||
createSignal,
|
||||
createUniqueId,
|
||||
For,
|
||||
@@ -16,6 +17,7 @@ import { createStore } from "solid-js/store"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { Key } from "@solid-primitives/keyed"
|
||||
import { FileIcon } from "@opencode/ui/file-icon"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { IconButton } from "@opencode/ui/icon-button"
|
||||
@@ -1254,6 +1256,7 @@ function ComposerEditorPopover(props: {
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
const listID = props.id ?? createUniqueId()
|
||||
const active = createSelector(() => props.activeID)
|
||||
let container: HTMLDivElement | undefined
|
||||
const [store, setStore] = createStore({ maxHeight: COMPOSER_SUGGESTION_MAX_HEIGHT })
|
||||
|
||||
@@ -1397,34 +1400,34 @@ function ComposerEditorPopover(props: {
|
||||
when={props.items.length > 0}
|
||||
fallback={<div class="px-2 py-1 text-v2-text-text-muted">{props.emptyLabel}</div>}
|
||||
>
|
||||
<For each={props.items}>
|
||||
<Key each={props.items} by="id">
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
data-suggestion-id={item.id}
|
||||
data-active={props.activeID === item.id ? "" : undefined}
|
||||
data-suggestion-id={item().id}
|
||||
data-active={active(item().id) ? "" : undefined}
|
||||
class="flex h-7 w-full shrink-0 items-center gap-2 rounded-md px-2 py-1 text-start"
|
||||
classList={{ "bg-v2-overlay-simple-overlay-hover": props.activeID === item.id }}
|
||||
classList={{ "bg-v2-overlay-simple-overlay-hover": active(item().id) }}
|
||||
onPointerMove={(event) => {
|
||||
if (event.movementX || event.movementY) props.onActiveChange(item)
|
||||
if (event.movementX || event.movementY) props.onActiveChange(item())
|
||||
}}
|
||||
onClick={() => props.onSelect(item)}
|
||||
onClick={() => props.onSelect(item())}
|
||||
>
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
<ComposerSuggestionIcon item={item} />
|
||||
<ComposerSuggestionIcon item={item()} />
|
||||
<bdi dir="auto" class="shrink-0 text-v2-text-text-base">
|
||||
{item.label}
|
||||
{item().label}
|
||||
</bdi>
|
||||
<Show when={item.description}>
|
||||
<span class="min-w-0 truncate text-v2-text-text-muted">{item.description}</span>
|
||||
<Show when={item().description}>
|
||||
<span class="min-w-0 truncate text-v2-text-text-muted">{item().description}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={item.keybind?.length}>
|
||||
<span class="shrink-0 text-v2-text-text-muted">{item.keybind?.join("+")}</span>
|
||||
<Show when={item().keybind?.length}>
|
||||
<span class="shrink-0 text-v2-text-text-muted">{item().keybind?.join("+")}</span>
|
||||
</Show>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</Key>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -287,9 +287,12 @@ export function createComposerEditor(input: {
|
||||
})
|
||||
onCleanup(() => dispatchMentionSearch({ type: "disposed" }))
|
||||
|
||||
const contextOpen = createMemo(() => state.popover.type === "context")
|
||||
const fileRows = createMemo(() => selectMentionFiles(mentionSearch()))
|
||||
|
||||
const contextList = useFilteredList<ComposerSuggestion>({
|
||||
items: (query) => {
|
||||
if (state.popover.type !== "context") return []
|
||||
if (!contextOpen()) return []
|
||||
|
||||
const sessions = input.searchSessions?.(query) ?? []
|
||||
|
||||
@@ -301,12 +304,7 @@ export function createComposerEditor(input: {
|
||||
|
||||
const seen = new Set(recent.map((item) => item.id))
|
||||
|
||||
return [
|
||||
...fixed,
|
||||
...sessions,
|
||||
...recent,
|
||||
...selectMentionFiles(mentionSearch()).filter((item) => !seen.has(item.id)),
|
||||
]
|
||||
return [...fixed, ...sessions, ...recent, ...fileRows().filter((item) => !seen.has(item.id))]
|
||||
},
|
||||
key: (item) => item.id,
|
||||
filterKeys: ["label"],
|
||||
|
||||
@@ -88,9 +88,37 @@ test("closing @ aborts its session search, and reopening on the same word waits
|
||||
}
|
||||
})
|
||||
|
||||
test("moving the @ highlight reuses the current query results", async () => {
|
||||
const searches: string[] = []
|
||||
|
||||
const value = setup(
|
||||
{ se: ["src/server.ts", "src/session.ts"] },
|
||||
{
|
||||
searchSessions: (query) => {
|
||||
searches.push(query)
|
||||
|
||||
return []
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
try {
|
||||
await value.type("@se")
|
||||
searches.length = 0
|
||||
value.editor.onKeyDown(new KeyboardEvent("keydown", { key: "ArrowDown" }))
|
||||
expect(activeID(value)).toBe("file:src/session.ts")
|
||||
expect(searches).toEqual([])
|
||||
} finally {
|
||||
value.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
function setup(
|
||||
results: Record<string, string[]>,
|
||||
options?: { requestSessionSearch?: (word: string, signal: AbortSignal) => Promise<void> },
|
||||
options?: {
|
||||
searchSessions?: (query: string) => ComposerSuggestion[]
|
||||
requestSessionSearch?: (word: string, signal: AbortSignal) => Promise<void>
|
||||
},
|
||||
) {
|
||||
jest.useFakeTimers()
|
||||
|
||||
@@ -112,7 +140,7 @@ function setup(
|
||||
|
||||
return paths.map(file)
|
||||
},
|
||||
searchSessions: () => [],
|
||||
searchSessions: options?.searchSessions,
|
||||
requestSessionSearch: options?.requestSessionSearch,
|
||||
view: { submit: { stopping: () => false, onSubmit() {}, onStop() {} } },
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user