Skip to content

Commit b82f292

Browse files
committed
feat: highlight matched items for sidebar search
1 parent b3dc1ee commit b82f292

1 file changed

Lines changed: 51 additions & 11 deletions

File tree

apps/yaak-client/components/Sidebar.tsx

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,17 @@ import {
2121
websocketConnectionsAtom,
2222
workspacesAtom,
2323
} from "@yaakapp-internal/models";
24+
import type { TreeHandle, TreeItemProps, TreeNode, TreeProps } from "@yaakapp-internal/ui";
25+
import {
26+
Icon,
27+
InlineCode,
28+
isSelectedFamily,
29+
LoadingIcon,
30+
selectedIdsFamily,
31+
Tree,
32+
} from "@yaakapp-internal/ui";
2433
import classNames from "classnames";
34+
import { fuzzyMatch } from "fuzzbunny";
2535
import { atom, useAtomValue } from "jotai";
2636
import { atomFamily, selectAtom } from "jotai/utils";
2737
import { memo, useCallback, useEffect, useMemo, useRef } from "react";
@@ -42,6 +52,7 @@ import { sendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest";
4252
import { useSidebarHidden } from "../hooks/useSidebarHidden";
4353
import { getWebsocketRequestActions } from "../hooks/useWebsocketRequestActions";
4454
import { deepEqualAtom } from "../lib/atoms";
55+
import { atomWithKVStorage } from "../lib/atoms/atomWithKVStorage";
4556
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
4657
import { jotaiStore } from "../lib/jotai";
4758
import { resolvedModelName } from "../lib/resolvedModelName";
@@ -54,19 +65,9 @@ import { filter } from "./core/Editor/filter/extension";
5465
import { evaluate, parseQuery } from "./core/Editor/filter/query";
5566
import { HttpMethodTag } from "./core/HttpMethodTag";
5667
import { HttpStatusTag } from "./core/HttpStatusTag";
57-
import {
58-
Icon,
59-
LoadingIcon,
60-
Tree,
61-
isSelectedFamily,
62-
selectedIdsFamily,
63-
InlineCode,
64-
} from "@yaakapp-internal/ui";
65-
import type { TreeNode, TreeHandle, TreeProps, TreeItemProps } from "@yaakapp-internal/ui";
6668
import { IconButton } from "./core/IconButton";
6769
import type { InputHandle } from "./core/Input";
6870
import { Input } from "./core/Input";
69-
import { atomWithKVStorage } from "../lib/atoms/atomWithKVStorage";
7071
import { GitDropdown } from "./git/GitDropdown";
7172

7273
const collapsedFamily = atomFamily((treeId: string) => {
@@ -75,6 +76,7 @@ const collapsedFamily = atomFamily((treeId: string) => {
7576
});
7677

7778
type SidebarModel = Workspace | Folder | HttpRequest | GrpcRequest | WebsocketRequest;
79+
7880
function isSidebarLeafModel(m: AnyModel): boolean {
7981
const modelMap: Record<Exclude<SidebarModel["model"], "workspace">, null> = {
8082
http_request: null,
@@ -854,7 +856,9 @@ const SidebarInnerItem = memo(function SidebarInnerItem({
854856

855857
return (
856858
<div className="flex items-center gap-2 min-w-0 h-full w-full text-left">
857-
<div className="truncate">{resolvedModelName(item)}</div>
859+
<div className="truncate">
860+
<HighlightedName name={resolvedModelName(item)} />
861+
</div>
858862
{response != null && (
859863
<div className="ml-auto">
860864
{response.state !== "closed" ? (
@@ -868,6 +872,42 @@ const SidebarInnerItem = memo(function SidebarInnerItem({
868872
);
869873
});
870874

875+
const bareTermNeedleAtom = selectAtom(sidebarFilterAtom, (filter) => {
876+
const text = filter.text.trim();
877+
if (!text) return "";
878+
const bareTerms = text.match(/(?:^|\s)(?!-)(?!["])([A-Za-z0-9_\-./]+)(?!["]?:)/g);
879+
if (!bareTerms) return "";
880+
return bareTerms
881+
.map((t) => t.trim())
882+
.filter((t) => {
883+
if (!t || t.startsWith("-") || t.startsWith('"')) return false;
884+
if (/^(AND|OR|NOT)$/i.test(t)) return false;
885+
if (t.includes(":")) return false;
886+
return true;
887+
})
888+
.join(" ");
889+
});
890+
891+
function HighlightedName({ name }: { name: string }) {
892+
const needle = useAtomValue(bareTermNeedleAtom);
893+
if (!needle) return <>{name}</>;
894+
const match = fuzzyMatch(name, needle);
895+
if (!match) return <>{name}</>;
896+
return (
897+
<>
898+
{match.highlights.map((segment, i) =>
899+
i % 2 === 1 ? (
900+
<mark key={i} className="bg-transparent text-notice font-semibold">
901+
{segment}
902+
</mark>
903+
) : (
904+
<span key={i}>{segment}</span>
905+
),
906+
)}
907+
</>
908+
);
909+
}
910+
871911
function getItemFields(node: TreeNode<SidebarModel>): Record<string, string> {
872912
const item = node.item;
873913

0 commit comments

Comments
 (0)