diff --git a/src/editor/lsp/markdown.ts b/src/editor/lsp/markdown.ts index 7b988880..5a245a5f 100644 --- a/src/editor/lsp/markdown.ts +++ b/src/editor/lsp/markdown.ts @@ -81,35 +81,56 @@ export function languageForPath(path: string): string | null { return LanguageDescription.matchFilename(languages, name)?.name ?? null; } -/** Replaces `el`'s text with highlighted spans (no-op when the language is unknown). */ -export async function highlightInto(el: HTMLElement, text: string, language: string | null): Promise { +export interface CodeToken { + from: number; + to: number; + /** Lezer `classHighlighter` classes (`tok-string` …). */ + cls: string; +} + +/** Highlighted ranges of `text`; empty when the language is unknown or its grammar fails to load. */ +export async function highlightTokens(text: string, language: string | null): Promise { const desc = language ? findLanguage(language) : null; - if (!desc) return; + if (!desc) return []; try { const support = desc.support ?? (await desc.load()); const tree = support.language.parser.parse(text); - const frag = document.createDocumentFragment(); + const out: CodeToken[] = []; + let pos = 0; highlightCode( text, tree, classHighlighter, (t, cls) => { - if (!cls) frag.appendChild(document.createTextNode(t)); - else { - const span = document.createElement("span"); - span.className = cls; - span.textContent = t; - frag.appendChild(span); - } + if (cls) out.push({ from: pos, to: pos + t.length, cls }); + pos += t.length; }, - () => frag.appendChild(document.createTextNode("\n")), + () => pos++, ); - el.replaceChildren(frag); + return out; } catch { - // Grammar failed to load: keep the plain text. + return []; } } +/** Replaces `el`'s text with highlighted spans (no-op when the language is unknown). */ +export async function highlightInto(el: HTMLElement, text: string, language: string | null): Promise { + const tokens = await highlightTokens(text, language); + if (!tokens.length) return; + const frag = document.createDocumentFragment(); + let pos = 0; + for (const t of tokens) { + if (t.from > pos) frag.appendChild(document.createTextNode(text.slice(pos, t.from))); + const span = document.createElement("span"); + span.className = t.cls; + span.textContent = text.slice(t.from, t.to); + frag.appendChild(span); + pos = t.to; + } + if (pos < text.length) frag.appendChild(document.createTextNode(text.slice(pos))); + el.replaceChildren(frag); +} + function highlightBlocks(root: HTMLElement, fallback: string | null) { root.querySelectorAll("pre > code").forEach((code) => { const cls = [...code.classList].find((c) => c.startsWith("language-")); diff --git a/src/features/search/__tests__/dialogs.test.tsx b/src/features/search/__tests__/dialogs.test.tsx index 06ed9b62..c35f1c64 100644 --- a/src/features/search/__tests__/dialogs.test.tsx +++ b/src/features/search/__tests__/dialogs.test.tsx @@ -34,7 +34,8 @@ vi.mock("../find/windowStore", async (orig) => ({ const { allActions } = await import("@/platform/actions"); (globalThis as unknown as { __actions: typeof allActions }).__actions = allActions; const { SearchEverywhereOverlay } = await import("../everywhere/SearchEverywhere"); -const { showSearchEverywhere, hideSearchEverywhere, useSearchEverywhere } = await import("../everywhere/store"); +const { showSearchEverywhere, hideSearchEverywhere, useSearchEverywhere, pushSearchHistory, searchHistory } = + await import("../everywhere/store"); const { FindInFilesOverlay } = await import("../find/FindInFiles"); const { showFindPopup, hideFindPopup } = await import("../find/store"); @@ -62,10 +63,10 @@ describe("Search Everywhere dialog", () => { await tick(); const results = within(document.getElementById("se-results")!); expect(results.queryByText("Files")).toBeNull(); - expect(screen.getByText("src/OrderController.kt")).toBeInTheDocument(); - - // ↓ moves from the file row to the action row. + // All mixes sources by relevance: the shorter prefix hit (the action) leads. fireEvent.keyDown(input, { key: "ArrowDown" }); + expect(screen.getByText("src/OrderController.kt")).toBeInTheDocument(); + fireEvent.keyDown(input, { key: "ArrowUp" }); fireEvent.keyDown(input, { key: "Enter" }); await tick(); expect(useSearchEverywhere.getState().open).toBe(false); @@ -73,6 +74,56 @@ describe("Search Everywhere dialog", () => { dispose(); }); + it("the preview toggle is remembered", async () => { + render(); + act(() => showSearchEverywhere("all")); + await tick(); + const toggle = screen.getByRole("button", { name: "Preview" }); + expect(toggle).toHaveAttribute("aria-pressed", "false"); + fireEvent.click(toggle); + expect(useSearchEverywhere.getState().preview).toBe(true); + expect(toggle).toHaveAttribute("aria-pressed", "true"); + expect(localStorage.getItem("openidea.search.everywherePreview.v1")).toBe("true"); + fireEvent.click(toggle); + expect(useSearchEverywhere.getState().preview).toBe(false); + }); + + it("/ lists scope commands; picking one or typing '/files q' switches the tab", async () => { + render(); + act(() => showSearchEverywhere("all", { query: "" })); + await tick(); + const input = screen.getByLabelText("Search query"); + fireEvent.change(input, { target: { value: "/fi" } }); + await tick(); + expect(within(document.getElementById("se-results")!).getByText("/files")).toBeInTheDocument(); + fireEvent.keyDown(input, { key: "Enter" }); + expect(useSearchEverywhere.getState()).toMatchObject({ tab: "files", query: "", open: true }); + + fireEvent.change(input, { target: { value: "/text order" } }); + expect(useSearchEverywhere.getState()).toMatchObject({ tab: "text", query: "order" }); + }); + + it("⌥↑ / ⌥↓ browse the tab's search history and restore the draft", async () => { + localStorage.removeItem("openidea.search.everywhereHistory.v1"); + pushSearchHistory("all", "older"); + pushSearchHistory("all", "newer"); + pushSearchHistory("files", "other tab"); + render(); + act(() => showSearchEverywhere("all", { query: "draft" })); + await tick(); + const input = screen.getByLabelText("Search query"); + const key = (k: string) => fireEvent.keyDown(input, { key: k, altKey: true }); + key("ArrowUp"); + expect(useSearchEverywhere.getState().query).toBe("newer"); + key("ArrowUp"); + key("ArrowUp"); + expect(useSearchEverywhere.getState().query).toBe("older"); + key("ArrowDown"); + key("ArrowDown"); + expect(useSearchEverywhere.getState().query).toBe("draft"); + expect(searchHistory("all")).toEqual(["newer", "older"]); + }); + it("⇥ cycles scopes and repeating the scope action toggles non-project items", async () => { render(); act(() => showSearchEverywhere("all")); diff --git a/src/features/search/__tests__/everywhereModel.test.ts b/src/features/search/__tests__/everywhereModel.test.ts index 4d5e527b..6da17cd3 100644 --- a/src/features/search/__tests__/everywhereModel.test.ts +++ b/src/features/search/__tests__/everywhereModel.test.ts @@ -6,8 +6,10 @@ import { fileItem, matchActions, mergeSymbols, + parseCommand, sectionsFor, stableIndex, + symbolItem, textItems, topResultSettled, type SectionId, @@ -132,10 +134,10 @@ describe("items and rows", () => { expect(f.line).toBe(41); }); - it("textItems trims indentation and shifts ranges", () => { - const [t] = textItems("/p/a.kt", "a.kt", [{ line: 4, text: " val order = 1", ranges: [[8, 13]], textOffset: 0 }]); - expect(t!.text).toBe("val order = 1"); - expect(t!.indices).toEqual([4, 5, 6, 7, 8]); + it("textItems collapses indentation and shifts ranges", () => { + const [t] = textItems("/p/a.kt", "a.kt", [{ line: 4, text: "\t\t val order = 1", ranges: [[8, 13]], textOffset: 0 }]); + expect(t!.text).toBe(" val order = 1"); + expect(t!.indices).toEqual([6, 7, 8, 9, 10]); expect([t!.col, t!.endCol]).toEqual([8, 13]); }); @@ -150,6 +152,28 @@ describe("items and rows", () => { expect(buildRows(sections, { headers: false }).map((r) => r.kind)).toEqual(["file", "more", "action"]); }); + it("buildRows interleaves the All tab by relevance, keeping each source's order", () => { + const file = (rel: string) => fileItem({ path: `/p/${rel}`, relPath: rel, indices: [] }); + const sections = new Map([ + ["files", { items: [file("src/upload_gray.py"), file("src/upgrade.py")], hasMore: true, loading: false }], + ["symbols", { items: [symbolItem(sym("upgrade", "function", "/p/m.py", 3))], hasMore: false, loading: false }], + ["text", { items: textItems("/p/a.md", "a.md", [{ line: 0, text: "upgrade", ranges: [[0, 3]], textOffset: 0 }]), hasMore: false, loading: false }], + ]); + const rows = buildRows(sections, { headers: false, mix: "upg" }); + expect(rows.map((r) => (r.kind === "more" ? `more:${r.section}` : (r as { section: SectionId }).section))).toEqual([ + "symbols", + "files", + "files", + "text", + "more:files", + ]); + // The index ranked upload_gray first; mixing must not reorder within files. + expect(rows.filter((r) => r.kind === "file").map((r) => (r as { name: string }).name)).toEqual([ + "upload_gray.py", + "upgrade.py", + ]); + }); + it("stableIndex keeps the selected key while rows stream in", () => { const sections = new Map([ ["files", { items: [fileItem({ path: "/p/A.kt", relPath: "A.kt", indices: [] }), fileItem({ path: "/p/B.kt", relPath: "B.kt", indices: [] })], hasMore: false, loading: false }], @@ -184,7 +208,41 @@ describe("topResultSettled", () => { expect(topResultSettled(new Map([["classes", sec(false)], ["files", sec(false, 1)], ["text", sec(true)]]))).toBe(true); }); + it("with mixing waits for every source but a text search the top row already beats", () => { + const strong = { items: [fileItem({ path: "/p/upgrade.py", relPath: "upgrade.py", indices: [] })], hasMore: false, loading: false }; + expect(topResultSettled(new Map([["files", strong], ["symbols", sec(true)]]), "upg")).toBe(false); + expect(topResultSettled(new Map([["files", strong], ["text", sec(true)]]), "upg")).toBe(true); + const weak = { ...strong, items: [fileItem({ path: "/p/xuxpxg.py", relPath: "xuxpxg.py", indices: [] })] }; + expect(topResultSettled(new Map([["files", weak], ["text", sec(true)]]), "upg")).toBe(false); + }); + it("is settled when everything finished empty", () => { expect(topResultSettled(new Map([["files", sec(false)]]))).toBe(true); }); }); + +describe("parseCommand", () => { + it("lists scope commands matching the typed prefix", () => { + const c = parseCommand("/"); + expect(c?.kind === "list" && c.rows.map((r) => (r as { name: string }).name)).toEqual([ + "/classes", + "/files", + "/symbols", + "/actions", + "/text", + ]); + const fi = parseCommand("/FI"); + expect(fi?.kind === "list" && fi.rows.map((r) => r.key)).toEqual(["cmd:files"]); + }); + + it("runs a complete command followed by a space", () => { + expect(parseCommand("/files upg")).toEqual({ kind: "run", tab: "files", rest: "upg" }); + expect(parseCommand("/text ")).toEqual({ kind: "run", tab: "text", rest: "" }); + }); + + it("leaves paths and other queries alone", () => { + expect(parseCommand("/Users/me/a.py")).toBeNull(); + expect(parseCommand("/fil x")).toBeNull(); + expect(parseCommand("upg")).toBeNull(); + }); +}); diff --git a/src/features/search/everywhere/SearchEverywhere.module.css b/src/features/search/everywhere/SearchEverywhere.module.css index d8ae4c2b..eda492a8 100644 --- a/src/features/search/everywhere/SearchEverywhere.module.css +++ b/src/features/search/everywhere/SearchEverywhere.module.css @@ -1,11 +1,17 @@ .dialog { --se-bg-header: var(--bg-frame, var(--bg-app)); - max-height: 72vh; + max-height: 80vh; margin-top: 9vh; background: var(--bg-popup, var(--bg-elev)); font-family: var(--font-ui); font-size: var(--fs-ui); } +/* The list shrinks to fit (the footer stays visible) instead of the dialog body scrolling. */ +.dialog > :global([data-dialog-body]) { + display: flex; + flex-direction: column; + overflow: hidden; +} .header { flex: none; display: flex; @@ -50,6 +56,9 @@ flex: none; color: var(--fg); } +.scope { + color: var(--fg); +} .headerButton { flex: none; } @@ -72,8 +81,23 @@ } .magnifier { flex: none; + display: inline-flex; + align-items: center; + gap: 1px; + height: 22px; + padding: 0 2px; + border: none; + border-radius: var(--radius-small); + background: none; color: var(--fg-subtle); } +.magnifier:hover { + background: var(--bg-hover); + color: var(--fg); +} +.magnifier:focus-visible { + outline: 2px solid var(--focus-ring, var(--accent)); +} .query { flex: 1; min-width: 0; @@ -105,6 +129,12 @@ flex: 1; min-height: 0; } +.preview { + flex: none; + display: flex; + height: 220px; + border-top: 1px solid var(--border-popup); +} .section { font-family: var(--font-ui); } @@ -142,6 +172,45 @@ } .code { font-family: var(--font-ui); + color: var(--fg); + tab-size: 2; +} +/* Lezer classHighlighter tokens of text hits (same colours as the Markdown preview). */ +.code :global(.tok-keyword), +.code :global(.tok-modifier), +.code :global(.tok-controlKeyword), +.code :global(.tok-operatorKeyword) { + color: var(--syn-keyword); +} +.code :global(.tok-string), +.code :global(.tok-string2) { + color: var(--syn-string); +} +.code :global(.tok-number), +.code :global(.tok-bool), +.code :global(.tok-atom) { + color: var(--syn-number); +} +.code :global(.tok-typeName), +.code :global(.tok-className), +.code :global(.tok-namespace) { + color: var(--syn-type); +} +.code :global(.tok-comment) { + color: var(--syn-comment); +} +.code :global(.tok-meta), +.code :global(.tok-macroName) { + color: var(--syn-annotation); +} +.code :global(.tok-propertyName) { + color: var(--syn-property); +} +.code :global(.tok-variableName.tok-definition) { + color: var(--syn-function); +} +.repeat { + color: var(--fg-faint); } .lineSuffix { color: var(--fg-muted); diff --git a/src/features/search/everywhere/SearchEverywhere.tsx b/src/features/search/everywhere/SearchEverywhere.tsx index 9e1f6b5e..dc4daaba 100644 --- a/src/features/search/everywhere/SearchEverywhere.tsx +++ b/src/features/search/everywhere/SearchEverywhere.tsx @@ -22,8 +22,10 @@ import { T, } from "@/ui"; import { useFileDecoration } from "@/workbench/contributions"; +import { highlightTokens, languageForPath, type CodeToken } from "@/editor/lsp/markdown"; import { badgeKind, basename, openLocation, relDir } from "../common/open"; import { parseLocationQuery } from "../common/query"; +import { CodePreview, type PreviewHighlight } from "../find/parts"; import { SeController, type SeSnapshot } from "./controller"; import { ipcDeps } from "./deps"; import { @@ -33,13 +35,21 @@ import { TABS, buildRows, isSectionRow, + parseCommand, stableIndex, topResultSettled, type SeRow, type SeTab, type SectionId, } from "./model"; -import { hideSearchEverywhere, pushRecentAction, useSearchEverywhere } from "./store"; +import { + hideSearchEverywhere, + pushRecentAction, + pushSearchHistory, + searchHistory, + setSearchEverywherePreview, + useSearchEverywhere, +} from "./store"; import styles from "./SearchEverywhere.module.css"; type SeScope = "allPlaces" | ProjectFileSet; @@ -52,7 +62,10 @@ const SE_SCOPES: Array<{ value: SeScope; label: string }> = [ ]; const ROW = 24; -const MAX_LIST = 528; +const MAX_LIST = 672; +const MIN_LIST = 10 * ROW; +/** List cap while the preview pane takes the lower part of the popup. */ +const MAX_LIST_PREVIEW = 312; const DEBOUNCE_MS = 60; export function SearchEverywhereOverlay() { @@ -70,6 +83,7 @@ function SearchEverywhereDialog() { const fileSet = useSearchEverywhere((s) => s.fileSet); const session = useSearchEverywhere((s) => s.session); const ctx = useSearchEverywhere((s) => s.ctx); + const preview = useSearchEverywhere((s) => s.preview); const inputRef = useRef(null); const listRef = useRef(null); @@ -78,7 +92,13 @@ function SearchEverywhereDialog() { const [snap, setSnap] = useState(null); const [controller] = useState(() => new SeController(ipcDeps(ctx), setSnap)); const [sel, setSel] = useState({ key: null, index: -1, moved: false }); - const [menu, setMenu] = useState<{ anchor: Element; items: MenuItem[] } | null>(null); + const [menu, setMenu] = useState<{ + anchor: Element; + items: MenuItem[]; + placement: "right-start" | "bottom-start"; + } | null>(null); + /** Position in the tab's history while ⌥↑/⌥↓ browse it (-1 = the typed draft). */ + const history = useRef({ pos: -1, draft: "" }); const [toggleTick, setToggleTick] = useState(0); const bumpToggles = () => setToggleTick((t) => t + 1); @@ -98,28 +118,67 @@ function SearchEverywhereDialog() { setSel({ key: null, index: -1, moved: false }); } + // "/fi" lists scope commands instead of searching. + const listing = useMemo(() => { + const c = parseCommand(query); + return c?.kind === "list" ? c.rows : null; + }, [query]); + const searchQuery = parseCommand(debounced)?.kind === "list" ? "" : debounced; + useEffect(() => { - controller.run({ query: debounced, tab, includeNonProject, fileSet: tab === "all" ? null : fileSet, extra }); - }, [controller, debounced, tab, includeNonProject, fileSet, extra]); + controller.run({ query: searchQuery, tab, includeNonProject, fileSet: tab === "all" ? null : fileSet, extra }); + }, [controller, searchQuery, tab, includeNonProject, fileSet, extra]); useEffect(() => { inputRef.current?.select(); }, [session]); - const parsed = useMemo(() => parseLocationQuery(debounced), [debounced]); + const parsed = useMemo(() => parseLocationQuery(searchQuery), [searchQuery]); + // The All tab interleaves its sources by relevance once there is a query. + const mix = tab === "all" ? parsed.text : undefined; const rows = useMemo( - () => (snap ? buildRows(snap.sections, { headers: !parsed.text.trim() }) : []), - [snap, parsed.text], + () => listing ?? (snap ? buildRows(snap.sections, { headers: !parsed.text.trim(), mix }) : []), + [listing, snap, parsed.text, mix], ); const active = stableIndex(rows, sel.key, sel.index, sel.moved); const activeRow = active >= 0 ? rows[active] : undefined; // Rows still answer an older query (debounce / in-flight search): Enter must wait. const settled = - !!snap && query === debounced && snap.query === debounced && snap.tab === tab && topResultSettled(snap.sections); + !!snap && + query === debounced && + snap.query === debounced && + snap.tab === tab && + topResultSettled(snap.sections, mix); const pendingEnter = useRef<{ side: boolean } | null>(null); const select = (index: number) => setSel({ key: rows[index]?.key ?? null, index, moved: true }); + /** Typed or picked query; "/files foo" switches to Files with "foo". */ + const setQuery = (value: string) => { + history.current.pos = -1; + const c = parseCommand(value); + useSearchEverywhere.setState(c?.kind === "run" ? { tab: c.tab, query: c.rest } : { query: value }); + }; + const browseHistory = (dir: 1 | -1) => { + const h = searchHistory(tab); + const cur = history.current; + const pos = Math.max(-1, Math.min(h.length - 1, cur.pos + dir)); + if (pos === cur.pos) return; + if (cur.pos < 0) cur.draft = query; + cur.pos = pos; + useSearchEverywhere.setState({ query: pos < 0 ? cur.draft : h[pos]! }); + }; + const openHistory = (anchor: Element) => { + const h = searchHistory(tab).slice(0, 15); + const items: MenuItem[] = h.length + ? [ + { kind: "header", label: "Recent Searches" }, + ...h.map((q) => ({ label: q, verbatim: true, run: () => setQuery(q) })), + ] + : [{ label: "No recent searches", disabled: true, run: () => {} }]; + setMenu({ anchor, items, placement: "bottom-start" }); + }; const setTab = (t: SeTab) => { + history.current.pos = -1; useSearchEverywhere.setState({ tab: t }); inputRef.current?.focus(); }; @@ -137,9 +196,15 @@ function SearchEverywhereDialog() { const activate = (row: SeRow | undefined, side = false) => { if (!row) return; + const isResult = row.kind !== "section" && row.kind !== "more" && row.kind !== "command"; + if (isResult && !(row.kind === "action" && !row.enabled)) pushSearchHistory(tab, query); switch (row.kind) { case "section": return; + case "command": + useSearchEverywhere.setState({ tab: row.tab, query: "" }); + inputRef.current?.focus(); + return; case "more": { const step = tab === "all" ? MORE_STEP_ALL : MORE_STEP_TAB; setExtra((e) => ({ ...e, [row.section]: (e[row.section] ?? 0) + step })); @@ -181,7 +246,7 @@ function SearchEverywhereDialog() { const openIntentions = () => { const row = activeRow; - if (!row || row.kind === "section" || row.kind === "more") return; + if (!row || row.kind === "section" || row.kind === "more" || row.kind === "command") return; const anchor = listRef.current?.querySelector('[aria-selected="true"]') ?? inputRef.current; if (!anchor) return; const copy = (text: string) => void navigator.clipboard?.writeText(text).catch(() => {}); @@ -208,7 +273,7 @@ function SearchEverywhereDialog() { ] : []), ]; - setMenu({ anchor, items }); + setMenu({ anchor, items, placement: "right-start" }); }; const onKeyDown = (e: KeyboardEvent) => { @@ -224,10 +289,16 @@ function SearchEverywhereDialog() { if (e.altKey) openIntentions(); else if (hasPrimaryModifier(e) && tab === "text") closeThen(() => void executeAction("find.inPath", { query: parsed.text })); + else if (listing) activate(activeRow); else if (!settled && !sel.moved) pendingEnter.current = { side: e.shiftKey }; else activate(activeRow, e.shiftKey); return; } + if (e.altKey && (e.key === "ArrowUp" || e.key === "ArrowDown")) { + e.preventDefault(); + browseHistory(e.key === "ArrowUp" ? 1 : -1); + return; + } if (!["ArrowDown", "ArrowUp", "PageDown", "PageUp"].includes(e.key)) return; const next = listNavigate(e.key, { items: rows, active, isSection: isSectionRow, wrap: true, pageSize: 10 }); if (next === null) return; @@ -235,11 +306,14 @@ function SearchEverywhereDialog() { select(next); }; - const listHeight = Math.min(MAX_LIST, rows.length * ROW); + const maxList = preview ? MAX_LIST_PREVIEW : MAX_LIST; + // While a query is typed the list keeps a minimum height so the popup doesn't jump with every keystroke. + const minList = parsed.text.trim() && !listing ? MIN_LIST : 0; + const listHeight = Math.min(maxList, Math.max(minList, rows.length * ROW)); + const previewTarget = previewOf(activeRow); const emptyQuery = !parsed.text.trim(); const showEmpty = !rows.length && snap && !snap.loading && (!emptyQuery || tab !== "all"); - const canSplit = - !!activeRow && activeRow.kind !== "section" && activeRow.kind !== "more" && activeRow.kind !== "action"; + const canSplit = activeRow?.kind === "file" || activeRow?.kind === "symbol" || activeRow?.kind === "text"; const onTabKey = (e: KeyboardEvent) => { if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return; e.preventDefault(); @@ -250,7 +324,7 @@ function SearchEverywhereDialog() { - className={styles.include} + variant="ghost" + className={clsx(styles.include, styles.scope)} value={includeNonProject ? "allPlaces" : fileSet} options={SE_SCOPES} onChange={(v) => { @@ -299,7 +374,16 @@ function SearchEverywhereDialog() { /> )} e.preventDefault()} + onClick={() => setSearchEverywherePreview(!preview)} + /> +
- + useSearchEverywhere.setState({ query: e.target.value })} + onChange={(e) => setQuery(e.target.value)} onKeyDown={onKeyDown} /> {snap?.loading && !emptyQuery && } - - {hintFor(tab)} - + {hintFor(tab) && ( + + {hintFor(tab)} + + )}
{(rows.length > 0 || showEmpty) && ( @@ -358,7 +454,13 @@ function SearchEverywhereDialog() { className={styles.list} aria-label={tx("Results")} renderItem={(r, state) => ( - + )} /> )} @@ -372,6 +474,15 @@ function SearchEverywhereDialog() { )} {!rows.length && !showEmpty &&