diff --git a/apps/web/features/search/components/search-command.tsx b/apps/web/features/search/components/search-command.tsx index c55647e7a..6c3218332 100644 --- a/apps/web/features/search/components/search-command.tsx +++ b/apps/web/features/search/components/search-command.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { Loader2, MessageSquare, SearchIcon } from "lucide-react"; import { Command as CommandPrimitive } from "cmdk"; @@ -17,6 +17,42 @@ import { } from "@multica/ui/components/ui/dialog"; import { useSearchStore } from "../stores/search-store"; +function HighlightText({ text, query }: { text: string; query: string }) { + const parts = useMemo(() => { + if (!query.trim()) return [{ text, highlight: false }]; + const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const regex = new RegExp(`(${escaped})`, "gi"); + const result: { text: string; highlight: boolean }[] = []; + let lastIndex = 0; + let match: RegExpExecArray | null; + while ((match = regex.exec(text)) !== null) { + if (match.index > lastIndex) { + result.push({ text: text.slice(lastIndex, match.index), highlight: false }); + } + result.push({ text: match[0], highlight: true }); + lastIndex = regex.lastIndex; + } + if (lastIndex < text.length) { + result.push({ text: text.slice(lastIndex), highlight: false }); + } + return result.length > 0 ? result : [{ text, highlight: false }]; + }, [text, query]); + + return ( + <> + {parts.map((part, i) => + part.highlight ? ( + + {part.text} + + ) : ( + part.text + ), + )} + > + ); +} + export function SearchCommand() { const router = useRouter(); const open = useSearchStore((s) => s.open); @@ -166,7 +202,9 @@ export function SearchCommand() { {issue.identifier} - {issue.title} + + + @@ -178,7 +216,10 @@ export function SearchCommand() { - {issue.matched_snippet} + )}