Search suggestions (#85)

The motivation for building search suggestions is two-fold: (1) to make the zoekt query language more approachable by presenting all available options to the user, and (2) make it easier for power-users to craft complex queries.

The meat-n-potatoes of this change are concentrated in searchBar.tsx and searchSuggestionBox.tsx. The suggestions box works by maintaining a state-machine of "modes". By default, the box is in the refine mode, where suggestions for different prefixes (e.g., repo:, lang:, etc.) are suggested to the user. When one of these prefixes is matched, the state-machine transitions to the corresponding mode (e.g., repository, language, etc.) and surfaces suggestions for that mode (if any).

The query is split up into parts by spaces " " (e.g., 'test repo:hello' -> ['test', 'repo:hello']). See splitQuery. The part that has the cursor over it is considered the active part. We evaluate which mode the state machine is in based on the active part. When a suggestion is clicked, we only modify the active part of the query.

Three modes are currently missing suggestion data: file (file names), revision (branch / tag names), and symbol (symbol names). In future PRs, we will need to introduce endpoints into the backend to allow the frontend to fetch this data and surface it as suggestions..
This commit is contained in:
Brendan Kellam 2024-11-22 18:50:13 -08:00 committed by GitHub
parent 3fe2d3295a
commit 7f952ce163
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 2365 additions and 211 deletions

28
.github/workflows/test-web.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: Test Web
on:
pull_request:
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: "true"
- name: Use Node.Js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Install
run: yarn install --frozen-lockfile
- name: Test
run: yarn workspace @sourcebot/web test

View file

@ -1,5 +1,6 @@
{ {
"recommendations": [ "recommendations": [
"dbaeumer.vscode-eslint" "dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss"
] ]
} }

11
.vscode/settings.json vendored
View file

@ -7,5 +7,16 @@
{ {
"pattern": "./packages/*/" "pattern": "./packages/*/"
} }
],
// @see : https://cva.style/docs/getting-started/installation#intellisense
"tailwindCSS.experimental.classRegex": [
[
"cva\\(([^)]*)\\)",
"[\"'`]([^\"'`]*).*?[\"'`]"
],
[
"cx\\(([^)]*)\\)",
"(?:'|\"|`)([^']*)(?:'|\"|`)"
]
] ]
} }

View file

@ -5,7 +5,7 @@
], ],
"scripts": { "scripts": {
"build": "yarn workspaces run build", "build": "yarn workspaces run build",
"test": "yarn workspace @sourcebot/backend test", "test": "yarn workspaces run test",
"dev": "npm-run-all --print-label --parallel dev:zoekt dev:backend dev:web", "dev": "npm-run-all --print-label --parallel dev:zoekt dev:backend dev:web",
"dev:zoekt": "export PATH=\"$PWD/bin:$PATH\" && zoekt-webserver -index .sourcebot/index -rpc", "dev:zoekt": "export PATH=\"$PWD/bin:$PATH\" && zoekt-webserver -index .sourcebot/index -rpc",
"dev:backend": "yarn workspace @sourcebot/backend dev:watch", "dev:backend": "yarn workspace @sourcebot/backend dev:watch",

View file

@ -6,7 +6,8 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint",
"test": "vitest"
}, },
"dependencies": { "dependencies": {
"@codemirror/commands": "^6.6.0", "@codemirror/commands": "^6.6.0",
@ -77,9 +78,12 @@
"eslint-config-next": "14.2.6", "eslint-config-next": "14.2.6",
"eslint-plugin-react": "^7.35.0", "eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^4.6.2",
"jsdom": "^25.0.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5" "typescript": "^5",
"vite-tsconfig-paths": "^5.1.3",
"vitest": "^2.1.5"
} }
} }

View file

@ -7,8 +7,8 @@ import { GitHubLogoIcon, DiscordLogoIcon } from "@radix-ui/react-icons";
import { SettingsDropdown } from "./settingsDropdown"; import { SettingsDropdown } from "./settingsDropdown";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import Image from "next/image"; import Image from "next/image";
import logoDark from "../../public/sb_logo_dark_small.png"; import logoDark from "../../../public/sb_logo_dark_small.png";
import logoLight from "../../public/sb_logo_light_small.png"; import logoLight from "../../../public/sb_logo_light_small.png";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
const SOURCEBOT_DISCORD_URL = "https://discord.gg/6Fhp27x7Pb"; const SOURCEBOT_DISCORD_URL = "https://discord.gg/6Fhp27x7Pb";

View file

@ -0,0 +1,224 @@
import { Suggestion, SuggestionMode } from "./searchSuggestionsBox";
/**
* List of search prefixes that can be used while the
* `refine` suggestion mode is active.
*/
enum SearchPrefix {
repo = "repo:",
r = "r:",
lang = "lang:",
file = "file:",
rev = "rev:",
revision = "revision:",
b = "b:",
branch = "branch:",
sym = "sym:",
content = "content:",
archived = "archived:",
case = "case:",
fork = "fork:",
public = "public:"
}
const negate = (prefix: SearchPrefix) => {
return `-${prefix}`;
}
type SuggestionModeMapping = {
suggestionMode: SuggestionMode,
prefixes: string[],
}
/**
* Maps search prefixes to a suggestion mode. When a query starts
* with a prefix, the corresponding suggestion mode is enabled.
* @see [searchSuggestionsBox.tsx](./searchSuggestionsBox.tsx)
*/
export const suggestionModeMappings: SuggestionModeMapping[] = [
{
suggestionMode: "repo",
prefixes: [
SearchPrefix.repo, negate(SearchPrefix.repo),
SearchPrefix.r, negate(SearchPrefix.r),
]
},
{
suggestionMode: "language",
prefixes: [
SearchPrefix.lang, negate(SearchPrefix.lang),
]
},
{
suggestionMode: "file",
prefixes: [
SearchPrefix.file, negate(SearchPrefix.file),
]
},
{
suggestionMode: "content",
prefixes: [
SearchPrefix.content, negate(SearchPrefix.content),
]
},
{
suggestionMode: "revision",
prefixes: [
SearchPrefix.rev, negate(SearchPrefix.rev),
SearchPrefix.revision, negate(SearchPrefix.revision),
SearchPrefix.branch, negate(SearchPrefix.branch),
SearchPrefix.b, negate(SearchPrefix.b),
]
},
{
suggestionMode: "symbol",
prefixes: [
SearchPrefix.sym, negate(SearchPrefix.sym),
]
},
{
suggestionMode: "archived",
prefixes: [
SearchPrefix.archived
]
},
{
suggestionMode: "case",
prefixes: [
SearchPrefix.case
]
},
{
suggestionMode: "fork",
prefixes: [
SearchPrefix.fork
]
},
{
suggestionMode: "public",
prefixes: [
SearchPrefix.public
]
}
];
export const refineModeSuggestions: Suggestion[] = [
{
value: SearchPrefix.repo,
description: "Include only results from the given repository.",
spotlight: true,
},
{
value: negate(SearchPrefix.repo),
description: "Exclude results from the given repository."
},
{
value: SearchPrefix.lang,
description: "Include only results from the given language.",
spotlight: true,
},
{
value: negate(SearchPrefix.lang),
description: "Exclude results from the given language."
},
{
value: SearchPrefix.file,
description: "Include only results from filepaths matching the given search pattern.",
spotlight: true,
},
{
value: negate(SearchPrefix.file),
description: "Exclude results from file paths matching the given search pattern."
},
{
value: SearchPrefix.rev,
description: "Search a given branch or tag instead of the default branch.",
spotlight: true,
},
{
value: negate(SearchPrefix.rev),
description: "Exclude results from the given branch or tag."
},
{
value: SearchPrefix.sym,
description: "Include only symbols matching the given search pattern.",
spotlight: true,
},
{
value: negate(SearchPrefix.sym),
description: "Exclude results from symbols matching the given search pattern."
},
{
value: SearchPrefix.content,
description: "Include only results from files if their content matches the given search pattern."
},
{
value: negate(SearchPrefix.content),
description: "Exclude results from files if their content matches the given search pattern."
},
{
value: SearchPrefix.archived,
description: "Include results from archived repositories.",
},
{
value: SearchPrefix.case,
description: "Control case-sensitivity of search patterns."
},
{
value: SearchPrefix.fork,
description: "Include only results from forked repositories."
},
{
value: SearchPrefix.public,
description: "Filter on repository visibility."
},
];
export const publicModeSuggestions: Suggestion[] = [
{
value: "yes",
description: "Only include results from public repositories."
},
{
value: "no",
description: "Only include results from private repositories."
},
];
export const forkModeSuggestions: Suggestion[] = [
{
value: "yes",
description: "Only include results from forked repositories."
},
{
value: "no",
description: "Only include results from non-forked repositories."
},
];
export const caseModeSuggestions: Suggestion[] = [
{
value: "auto",
description: "Search patterns are case-insensitive if all characters are lowercase, and case sensitive otherwise (default)."
},
{
value: "yes",
description: "Case sensitive search."
},
{
value: "no",
description: "Case insensitive search."
},
];
export const archivedModeSuggestions: Suggestion[] = [
{
value: "yes",
description: "Only include results in archived repositories."
},
{
value: "no",
description: "Only include results in non-archived repositories."
},
];

View file

@ -0,0 +1,2 @@
export { SearchBar } from "./searchBar";

View file

@ -0,0 +1,712 @@
// From https://github.com/github-linguist/linguist/blob/main/lib/linguist/languages.yml
const languages = [
"1C Enterprise",
"2-Dimensional Array",
"4D",
"ABAP",
"ABAP CDS",
"ABNF",
"AGS Script",
"AIDL",
"AL",
"AMPL",
"ANTLR",
"API Blueprint",
"APL",
"ASL",
"ASN.1",
"ASP.NET",
"ATS",
"ActionScript",
"Ada",
"Adblock Filter List",
"Adobe Font Metrics",
"Agda",
"Alloy",
"Alpine Abuild",
"Altium Designer",
"AngelScript",
"Ant Build System",
"Antlers",
"ApacheConf",
"Apex",
"Apollo Guidance Computer",
"AppleScript",
"Arc",
"AsciiDoc",
"AspectJ",
"Assembly",
"Astro",
"Asymptote",
"Augeas",
"AutoHotkey",
"AutoIt",
"Avro IDL",
"Awk",
"BASIC",
"Ballerina",
"Batchfile",
"Beef",
"Befunge",
"Berry",
"BibTeX",
"Bicep",
"Bikeshed",
"Bison",
"BitBake",
"Blade",
"BlitzBasic",
"BlitzMax",
"Bluespec",
"Bluespec BH",
"Boo",
"Boogie",
"Brainfuck",
"BrighterScript",
"Brightscript",
"Browserslist",
"C",
"C#",
"C++",
"C-ObjDump",
"C2hs Haskell",
"CAP CDS",
"CIL",
"CLIPS",
"CMake",
"COBOL",
"CODEOWNERS",
"COLLADA",
"CSON",
"CSS",
"CSV",
"CUE",
"CWeb",
"Cabal Config",
"Cadence",
"Cairo",
"CameLIGO",
"Cap'n Proto",
"CartoCSS",
"Ceylon",
"Chapel",
"Charity",
"Checksums",
"ChucK",
"Circom",
"Cirru",
"Clarion",
"Clarity",
"Classic ASP",
"Clean",
"Click",
"Clojure",
"Closure Templates",
"Cloud Firestore Security Rules",
"CoNLL-U",
"CodeQL",
"CoffeeScript",
"ColdFusion",
"ColdFusion CFC",
"Common Lisp",
"Common Workflow Language",
"Component Pascal",
"Cool",
"Coq",
"Cpp-ObjDump",
"Creole",
"Crystal",
"Csound",
"Csound Document",
"Csound Score",
"Cuda",
"Cue Sheet",
"Curry",
"Cycript",
"Cypher",
"Cython",
"D",
"D-ObjDump",
"D2",
"DIGITAL Command Language",
"DM",
"DNS Zone",
"DTrace",
"Dafny",
"Darcs Patch",
"Dart",
"DataWeave",
"Debian Package Control File",
"DenizenScript",
"Dhall",
"Diff",
"DirectX 3D File",
"Dockerfile",
"Dogescript",
"Dotenv",
"Dylan",
"E",
"E-mail",
"EBNF",
"ECL",
"ECLiPSe",
"EJS",
"EQ",
"Eagle",
"Earthly",
"Easybuild",
"Ecere Projects",
"Ecmarkup",
"EditorConfig",
"Edje Data Collection",
"Eiffel",
"Elixir",
"Elm",
"Elvish",
"Elvish Transcript",
"Emacs Lisp",
"EmberScript",
"Erlang",
"Euphoria",
"F#",
"F*",
"FIGlet Font",
"FLUX",
"Factor",
"Fancy",
"Fantom",
"Faust",
"Fennel",
"Filebench WML",
"Filterscript",
"Fluent",
"Formatted",
"Forth",
"Fortran",
"Fortran Free Form",
"FreeBasic",
"FreeMarker",
"Frege",
"Futhark",
"G-code",
"GAML",
"GAMS",
"GAP",
"GCC Machine Description",
"GDB",
"GDScript",
"GEDCOM",
"GLSL",
"GN",
"GSC",
"Game Maker Language",
"Gemfile.lock",
"Gemini",
"Genero",
"Genero Forms",
"Genie",
"Genshi",
"Gentoo Ebuild",
"Gentoo Eclass",
"Gerber Image",
"Gettext Catalog",
"Gherkin",
"Git Attributes",
"Git Config",
"Git Revision List",
"Gleam",
"Glyph",
"Glyph Bitmap Distribution Format",
"Gnuplot",
"Go",
"Go Checksums",
"Go Module",
"Go Workspace",
"Godot Resource",
"Golo",
"Gosu",
"Grace",
"Gradle",
"Gradle Kotlin DSL",
"Grammatical Framework",
"Graph Modeling Language",
"GraphQL",
"Graphviz (DOT)",
"Groovy",
"Groovy Server Pages",
"HAProxy",
"HCL",
"HLSL",
"HOCON",
"HTML",
"HTML+ECR",
"HTML+EEX",
"HTML+ERB",
"HTML+PHP",
"HTML+Razor",
"HTTP",
"HXML",
"Hack",
"Haml",
"Handlebars",
"Harbour",
"Haskell",
"Haxe",
"HiveQL",
"HolyC",
"Hosts File",
"Hy",
"HyPhy",
"IDL",
"IGOR Pro",
"INI",
"IRC log",
"Idris",
"Ignore List",
"ImageJ Macro",
"Imba",
"Inform 7",
"Ink",
"Inno Setup",
"Io",
"Ioke",
"Isabelle",
"Isabelle ROOT",
"J",
"JAR Manifest",
"JCL",
"JFlex",
"JSON",
"JSON with Comments",
"JSON5",
"JSONLD",
"JSONiq",
"Janet",
"Jasmin",
"Java",
"Java Properties",
"Java Server Pages",
"JavaScript",
"JavaScript+ERB",
"Jest Snapshot",
"JetBrains MPS",
"Jinja",
"Jison",
"Jison Lex",
"Jolie",
"Jsonnet",
"Julia",
"Jupyter Notebook",
"Just",
"KRL",
"Kaitai Struct",
"KakouneScript",
"KerboScript",
"KiCad Layout",
"KiCad Legacy Layout",
"KiCad Schematic",
"Kickstart",
"Kit",
"Kotlin",
"Kusto",
"LFE",
"LLVM",
"LOLCODE",
"LSL",
"LTspice Symbol",
"LabVIEW",
"Lark",
"Lasso",
"Latte",
"Lean",
"Less",
"Lex",
"LigoLANG",
"LilyPond",
"Limbo",
"Linker Script",
"Linux Kernel Module",
"Liquid",
"Literate Agda",
"Literate CoffeeScript",
"Literate Haskell",
"LiveScript",
"Logos",
"Logtalk",
"LookML",
"LoomScript",
"Lua",
"M",
"M4",
"M4Sugar",
"MATLAB",
"MAXScript",
"MDX",
"MLIR",
"MQL4",
"MQL5",
"MTML",
"MUF",
"Macaulay2",
"Makefile",
"Mako",
"Markdown",
"Marko",
"Mask",
"Mathematica",
"Maven POM",
"Max",
"Mercury",
"Mermaid",
"Meson",
"Metal",
"Microsoft Developer Studio Project",
"Microsoft Visual Studio Solution",
"MiniD",
"MiniYAML",
"Mint",
"Mirah",
"Modelica",
"Modula-2",
"Modula-3",
"Module Management System",
"Monkey",
"Monkey C",
"Moocode",
"MoonScript",
"Motoko",
"Motorola 68K Assembly",
"Move",
"Muse",
"Mustache",
"Myghty",
"NASL",
"NCL",
"NEON",
"NL",
"NPM Config",
"NSIS",
"NWScript",
"Nasal",
"Nearley",
"Nemerle",
"NetLinx",
"NetLinx+ERB",
"NetLogo",
"NewLisp",
"Nextflow",
"Nginx",
"Nim",
"Ninja",
"Nit",
"Nix",
"Nu",
"NumPy",
"Nunjucks",
"Nushell",
"OASv2-json",
"OASv2-yaml",
"OASv3-json",
"OASv3-yaml",
"OCaml",
"ObjDump",
"Object Data Instance Notation",
"ObjectScript",
"Objective-C",
"Objective-C++",
"Objective-J",
"Odin",
"Omgrofl",
"Opa",
"Opal",
"Open Policy Agent",
"OpenAPI Specification v2",
"OpenAPI Specification v3",
"OpenCL",
"OpenEdge ABL",
"OpenQASM",
"OpenRC runscript",
"OpenSCAD",
"OpenStep Property List",
"OpenType Feature File",
"Option List",
"Org",
"Ox",
"Oxygene",
"Oz",
"P4",
"PDDL",
"PEG.js",
"PHP",
"PLSQL",
"PLpgSQL",
"POV-Ray SDL",
"Pact",
"Pan",
"Papyrus",
"Parrot",
"Parrot Assembly",
"Parrot Internal Representation",
"Pascal",
"Pawn",
"Pep8",
"Perl",
"Pic",
"Pickle",
"PicoLisp",
"PigLatin",
"Pike",
"PlantUML",
"Pod",
"Pod 6",
"PogoScript",
"Polar",
"Pony",
"Portugol",
"PostCSS",
"PostScript",
"PowerBuilder",
"PowerShell",
"Prisma",
"Processing",
"Procfile",
"Proguard",
"Prolog",
"Promela",
"Propeller Spin",
"Protocol Buffer",
"Protocol Buffer Text Format",
"Public Key",
"Pug",
"Puppet",
"Pure Data",
"PureBasic",
"PureScript",
"Pyret",
"Python",
"Python console",
"Python traceback",
"Q#",
"QML",
"QMake",
"Qt Script",
"Quake",
"R",
"RAML",
"RBS",
"RDoc",
"REALbasic",
"REXX",
"RMarkdown",
"RPC",
"RPGLE",
"RPM Spec",
"RUNOFF",
"Racket",
"Ragel",
"Raku",
"Rascal",
"Raw token data",
"ReScript",
"Readline Config",
"Reason",
"ReasonLIGO",
"Rebol",
"Record Jar",
"Red",
"Redcode",
"Redirect Rules",
"Regular Expression",
"Ren'Py",
"RenderScript",
"Rez",
"Rich Text Format",
"Ring",
"Riot",
"RobotFramework",
"Roff",
"Roff Manpage",
"Rouge",
"RouterOS Script",
"Ruby",
"Rust",
"SAS",
"SCSS",
"SELinux Policy",
"SMT",
"SPARQL",
"SQF",
"SQL",
"SQLPL",
"SRecode Template",
"SSH Config",
"STAR",
"STL",
"STON",
"SVG",
"SWIG",
"Sage",
"SaltStack",
"Sass",
"Scala",
"Scaml",
"Scenic",
"Scheme",
"Scilab",
"Self",
"ShaderLab",
"Shell",
"ShellCheck Config",
"ShellSession",
"Shen",
"Sieve",
"Simple File Verification",
"Singularity",
"Slash",
"Slice",
"Slim",
"SmPL",
"Smali",
"Smalltalk",
"Smarty",
"Smithy",
"Snakemake",
"Solidity",
"Soong",
"SourcePawn",
"Spline Font Database",
"Squirrel",
"Stan",
"Standard ML",
"Starlark",
"Stata",
"StringTemplate",
"Stylus",
"SubRip Text",
"SugarSS",
"SuperCollider",
"Svelte",
"Sway",
"Sweave",
"Swift",
"SystemVerilog",
"TI Program",
"TL-Verilog",
"TLA",
"TOML",
"TSQL",
"TSV",
"TSX",
"TXL",
"Talon",
"Tcl",
"Tcsh",
"TeX",
"Tea",
"Terra",
"Texinfo",
"Text",
"TextMate Properties",
"Textile",
"Thrift",
"Turing",
"Turtle",
"Twig",
"Type Language",
"TypeScript",
"Typst",
"Unified Parallel C",
"Unity3D Asset",
"Unix Assembly",
"Uno",
"UnrealScript",
"UrWeb",
"V",
"VBA",
"VBScript",
"VCL",
"VHDL",
"Vala",
"Valve Data Format",
"Velocity Template Language",
"Verilog",
"Vim Help File",
"Vim Script",
"Vim Snippet",
"Visual Basic .NET",
"Visual Basic 6.0",
"Volt",
"Vue",
"Vyper",
"WDL",
"WGSL",
"Wavefront Material",
"Wavefront Object",
"Web Ontology Language",
"WebAssembly",
"WebAssembly Interface Type",
"WebIDL",
"WebVTT",
"Wget Config",
"Whiley",
"Wikitext",
"Win32 Message File",
"Windows Registry Entries",
"Witcher Script",
"Wollok",
"World of Warcraft Addon Data",
"Wren",
"X BitMap",
"X Font Directory Index",
"X PixMap",
"X10",
"XC",
"XCompose",
"XML",
"XML Property List",
"XPages",
"XProc",
"XQuery",
"XS",
"XSLT",
"Xojo",
"Xonsh",
"Xtend",
"YAML",
"YANG",
"YARA",
"YASnippet",
"Yacc",
"Yul",
"ZAP",
"ZIL",
"Zeek",
"ZenScript",
"Zephir",
"Zig",
"Zimpl",
"cURL Config",
"desktop",
"dircolors",
"eC",
"edn",
"fish",
"hoon",
"jq",
"kvlang",
"mIRC Script",
"mcfunction",
"mupad",
"nanorc",
"nesC",
"ooc",
"q",
"reStructuredText",
"robots.txt",
"sed",
"wisp",
"xBase",
]
export default languages;

View file

@ -0,0 +1,292 @@
'use client';
import { useTailwind } from "@/hooks/useTailwind";
import { Repository, SearchQueryParams } from "@/lib/types";
import { cn, createPathWithQueryParams } from "@/lib/utils";
import {
cursorCharLeft,
cursorCharRight,
cursorDocEnd,
cursorDocStart,
cursorLineBoundaryBackward,
cursorLineBoundaryForward,
deleteCharBackward,
deleteCharForward,
deleteGroupBackward,
deleteGroupForward,
deleteLineBoundaryBackward,
deleteLineBoundaryForward,
history,
historyKeymap,
selectAll,
selectCharLeft,
selectCharRight,
selectDocEnd,
selectDocStart,
selectLineBoundaryBackward,
selectLineBoundaryForward
} from "@codemirror/commands";
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
import CodeMirror, { Annotation, EditorView, KeyBinding, keymap, ReactCodeMirrorRef } from "@uiw/react-codemirror";
import { cva } from "class-variance-authority";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useHotkeys } from 'react-hotkeys-hook';
import { SearchSuggestionsBox, Suggestion } from "./searchSuggestionsBox";
import { useClickListener } from "@/hooks/useClickListener";
import { getRepos } from "../../api/(client)/client";
import languages from "./languages";
import { zoekt } from "./zoektLanguageExtension";
interface SearchBarProps {
className?: string;
size?: "default" | "sm";
defaultQuery?: string;
autoFocus?: boolean;
}
const searchBarKeymap: readonly KeyBinding[] = ([
{ key: "ArrowLeft", run: cursorCharLeft, shift: selectCharLeft, preventDefault: true },
{ key: "ArrowRight", run: cursorCharRight, shift: selectCharRight, preventDefault: true },
{ key: "Home", run: cursorLineBoundaryBackward, shift: selectLineBoundaryBackward, preventDefault: true },
{ key: "Mod-Home", run: cursorDocStart, shift: selectDocStart },
{ key: "End", run: cursorLineBoundaryForward, shift: selectLineBoundaryForward, preventDefault: true },
{ key: "Mod-End", run: cursorDocEnd, shift: selectDocEnd },
{ key: "Mod-a", run: selectAll },
{ key: "Backspace", run: deleteCharBackward, shift: deleteCharBackward },
{ key: "Delete", run: deleteCharForward },
{ key: "Mod-Backspace", mac: "Alt-Backspace", run: deleteGroupBackward },
{ key: "Mod-Delete", mac: "Alt-Delete", run: deleteGroupForward },
{ mac: "Mod-Backspace", run: deleteLineBoundaryBackward },
{ mac: "Mod-Delete", run: deleteLineBoundaryForward }
] as KeyBinding[]).concat(historyKeymap);
const searchBarContainerVariants = cva(
"search-bar-container flex items-center p-0.5 border rounded-md relative",
{
variants: {
size: {
default: "h-10",
sm: "h-8"
}
},
defaultVariants: {
size: "default",
}
}
);
export const SearchBar = ({
className,
size,
defaultQuery,
autoFocus,
}: SearchBarProps) => {
const router = useRouter();
const tailwind = useTailwind();
const suggestionBoxRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<ReactCodeMirrorRef>(null);
const [cursorPosition, setCursorPosition] = useState(0);
const [isSuggestionsBoxEnabled, setIsSuggestionsBoxEnabled ] = useState(false);
const [isSuggestionsBoxFocused, setIsSuggestionsBoxFocused] = useState(false);
const focusEditor = useCallback(() => editorRef.current?.view?.focus(), []);
const focusSuggestionsBox = useCallback(() => suggestionBoxRef.current?.focus(), []);
const [_query, setQuery] = useState(defaultQuery ?? "");
const query = useMemo(() => {
// Replace any newlines with spaces to handle
// copy & pasting text with newlines.
return _query.replaceAll(/\n/g, " ");
}, [_query]);
const [repos, setRepos] = useState<Repository[]>([]);
useEffect(() => {
getRepos().then((response) => {
setRepos(response.List.Repos.map(r => r.Repository));
});
}, []);
const suggestionData = useMemo(() => {
const repoSuggestions: Suggestion[] = repos.map((repo) => {
return {
value: repo.Name,
}
});
const languageSuggestions: Suggestion[] = languages.map((lang) => {
const spotlight = [
"Python",
"Java",
"TypeScript",
"Go",
"C++",
"C#"
].includes(lang);
return {
value: lang,
spotlight,
};
})
return {
repos: repoSuggestions,
languages: languageSuggestions,
}
}, [repos]);
const theme = useMemo(() => {
return createTheme({
theme: 'light',
settings: {
background: tailwind.theme.colors.background,
foreground: tailwind.theme.colors.foreground,
caret: '#AEAFAD',
},
styles: [
{
tag: t.keyword,
color: tailwind.theme.colors.highlight,
},
{
tag: t.paren,
color: tailwind.theme.colors.highlight,
}
],
});
}, [tailwind]);
const extensions = useMemo(() => {
return [
keymap.of(searchBarKeymap),
history(),
zoekt(),
EditorView.updateListener.of(update => {
if (update.selectionSet) {
const selection = update.state.selection.main;
if (selection.empty) {
setCursorPosition(selection.anchor);
}
}
})
];
}, []);
// Hotkey to focus the search bar.
useHotkeys('/', (event) => {
event.preventDefault();
focusEditor();
setIsSuggestionsBoxEnabled(true);
if (editorRef.current?.view) {
cursorDocEnd({
state: editorRef.current.view.state,
dispatch: editorRef.current.view.dispatch,
});
}
});
// Collapse the suggestions box if the user clicks outside of the search bar container.
useClickListener('.search-bar-container', (isElementClicked) => {
if (!isElementClicked) {
setIsSuggestionsBoxEnabled(false);
} else {
setIsSuggestionsBoxEnabled(true);
}
});
const onSubmit = () => {
const url = createPathWithQueryParams('/search',
[SearchQueryParams.query, query],
)
router.push(url);
}
return (
<div
className={cn(searchBarContainerVariants({ size, className }))}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
setIsSuggestionsBoxEnabled(false);
onSubmit();
}
if (e.key === 'Escape') {
e.preventDefault();
setIsSuggestionsBoxEnabled(false);
}
if (e.key === 'ArrowDown') {
e.preventDefault();
setIsSuggestionsBoxEnabled(true);
focusSuggestionsBox();
}
if (e.key === 'ArrowUp') {
e.preventDefault();
}
}}
>
<CodeMirror
ref={editorRef}
className="overflow-x-auto scrollbar-hide w-full"
placeholder={"Search..."}
value={query}
onChange={(value) => {
setQuery(value);
// Whenever the user types, we want to re-enable
// the suggestions box.
setIsSuggestionsBoxEnabled(true);
}}
theme={theme}
basicSetup={false}
extensions={extensions}
indentWithTab={false}
autoFocus={autoFocus ?? false}
/>
<SearchSuggestionsBox
ref={suggestionBoxRef}
query={query}
onCompletion={(newQuery: string, newCursorPosition: number) => {
setQuery(newQuery);
// Move the cursor to it's new position.
// @note : normally, react-codemirror handles syncing `query`
// and the document state, but this happens on re-render. Since
// we want to move the cursor before the component re-renders,
// we manually update the document state inline.
editorRef.current?.view?.dispatch({
changes: { from: 0, to: query.length, insert: newQuery },
annotations: [Annotation.define<boolean>().of(true)],
});
editorRef.current?.view?.dispatch({
selection: { anchor: newCursorPosition, head: newCursorPosition },
});
// Re-focus the editor since suggestions cause focus to be lost (both click & keyboard)
editorRef.current?.view?.focus();
}}
isEnabled={isSuggestionsBoxEnabled}
onReturnFocus={() => {
focusEditor();
}}
isFocused={isSuggestionsBoxFocused}
onFocus={() => {
setIsSuggestionsBoxFocused(document.activeElement === suggestionBoxRef.current);
}}
onBlur={() => {
setIsSuggestionsBoxFocused(document.activeElement === suggestionBoxRef.current);
}}
cursorPosition={cursorPosition}
data={suggestionData}
/>
</div>
)
}

View file

@ -0,0 +1,221 @@
import { expect, test } from 'vitest'
import { completeSuggestion, splitQuery } from './searchSuggestionsBox'
test('splitQuery returns a single element when the query is empty', () => {
const { queryParts, cursorIndex } = splitQuery('', 0);
expect(cursorIndex).toEqual(0);
expect(queryParts).toEqual(['']);
});
test('splitQuery splits on spaces', () => {
const query = String.raw`repo:^github\.com/example/example$ world`;
const { queryParts, cursorIndex } = splitQuery(query, 0);
expect(queryParts).toEqual(query.split(" "));
expect(cursorIndex).toEqual(0);
});
test('splitQuery groups parts that are in the same quote capture group into a single part', () => {
const part1 = 'lang:"1C Enterprise"';
const part2 = "hello";
const { queryParts, cursorIndex } = splitQuery(`${part1} ${part2}`, 12);
expect(queryParts).toEqual([part1, part2]);
expect(cursorIndex).toEqual(0);
});
test('splitQuery does not support nested quote capture groups', () => {
const { queryParts } = splitQuery('lang:"My language "with quotes"" hello', 0);
expect(queryParts).toEqual(['lang:"My language "with', 'quotes""', 'hello']);
});
test('splitQuery groups all parts together when a quote capture group is not closed', () => {
const query = '"hello asdf ok'
const { queryParts, cursorIndex } = splitQuery(query, 0);
expect(queryParts).toEqual([query]);
expect(cursorIndex).toBe(0);
});
test('splitQuery correclty locates the cursor index given the cursor position (1)', () => {
const query = 'foo bar "fizz buzz"';
const { queryParts: parts1, cursorIndex: index1 } = splitQuery(query, 0);
expect(parts1).toEqual(['foo', 'bar', '"fizz buzz"']);
expect(parts1[index1]).toBe('foo');
const { queryParts: parts2, cursorIndex: index2 } = splitQuery(query, 6);
expect(parts2).toEqual(['foo', 'bar', '"fizz buzz"']);
expect(parts2[index2]).toBe('bar');
const { queryParts: parts3, cursorIndex: index3 } = splitQuery(query, 15);
expect(parts3).toEqual(['foo', 'bar', '"fizz buzz"']);
expect(parts3[index3]).toBe('"fizz buzz"');
});
test('splitQuery correclty locates the cursor index given the cursor position (2)', () => {
const query = 'a b';
expect(splitQuery(query, 0).cursorIndex).toBe(0);
expect(splitQuery(query, 1).cursorIndex).toBe(0);
expect(splitQuery(query, 2).cursorIndex).toBe(1);
expect(splitQuery(query, 3).cursorIndex).toBe(1);
});
test('splitQuery can handle multiple spaces adjacent', () => {
expect(splitQuery("a b ", 0).queryParts).toEqual(['a', '', '', 'b', '', '']);
});
test('splitQuery locates the cursor index to the last query part when the cursor position is at the end of the query', () => {
const query = "as df";
const cursorPos = query.length;
const { queryParts, cursorIndex } = splitQuery(query, cursorPos);
expect(cursorIndex).toBe(queryParts.length - 1);
expect(queryParts[cursorIndex]).toBe("df");
expect(queryParts).toEqual(['as', 'df']);
});
test('splitQuery sets the cursor index to 0 when the cursor position is out of bounds', () => {
const query = "hello world";
const cursorPos = query.length + 1;
const { queryParts, cursorIndex } = splitQuery(query, cursorPos);
expect(cursorIndex).toBe(0);
expect(queryParts[cursorIndex]).toBe("hello");
expect(queryParts).toEqual(['hello', 'world']);
});
test('completeSuggestion can complete a empty query', () => {
const suggestionQuery = ``;
const query = ``;
const suggestion = "hello";
const { newQuery, newCursorPosition } = completeSuggestion({
query,
suggestionQuery,
suggestion,
trailingSpace: false,
regexEscaped: false,
cursorPosition: 0,
});
const expectedNewQuery = String.raw`hello`;
expect(newQuery).toEqual(expectedNewQuery);
expect(newCursorPosition).toBe(newQuery.length);
});
test('completeSuggestion can complete with a empty suggestion query', () => {
const suggestionQuery = ``;
const query = `case:`;
const suggestion = "auto";
const { newQuery, newCursorPosition } = completeSuggestion({
query,
suggestionQuery,
suggestion,
trailingSpace: false,
regexEscaped: false,
cursorPosition: query.length,
});
const expectedNewQuery = `case:auto`;
expect(newQuery).toEqual(expectedNewQuery);
expect(newCursorPosition).toBe(newQuery.length);
});
test('completeSuggestion inserts a trailing space when trailingSpace is true and the completion is at the end of the query', () => {
const suggestionQuery = 'a';
const part1 = String.raw`lang:Go`;
const part2 = String.raw`case:${suggestionQuery}`;
const query = `${part1} ${part2}`
const suggestion = 'auto';
const cursorPosition = query.length;
const { newQuery, newCursorPosition } = completeSuggestion({
query,
suggestionQuery,
suggestion,
trailingSpace: true,
regexEscaped: false,
cursorPosition,
});
const expectedPart2 = `case:auto`
const expectedNewQuery = `${part1} ${expectedPart2} `;
expect(newQuery).toEqual(expectedNewQuery);
expect(newCursorPosition).toBe(newQuery.length);
});
test('completeSuggestion does not insert a trailing space when trailingSpace is true and the completion is not at the end of the query', () => {
const suggestionQuery = 'G';
const part1 = String.raw`lang:${suggestionQuery}`;
const part2 = String.raw`case:auto`;
const query = `${part1} ${part2}`
const suggestion = 'Go';
const cursorPosition = part1.length;
const { newQuery, newCursorPosition } = completeSuggestion({
query,
suggestionQuery,
suggestion,
trailingSpace: true,
regexEscaped: false,
cursorPosition,
});
const expectedPart1 = `lang:Go`
const expectedNewQuery = `${expectedPart1} ${part2}`; // Notice no trailing space
expect(newQuery).toEqual(expectedNewQuery);
expect(newCursorPosition).toBe(expectedPart1.length);
});
test('completeSuggestion wraps suggestions in quotes when the suggestion contains a space and regexEscaped is false', () => {
const suggestionQuery = `m`;
const query = `lang:${suggestionQuery}`;
const suggestion = `my language`;
const { newQuery, newCursorPosition } = completeSuggestion({
query,
suggestionQuery,
suggestion,
trailingSpace: false,
regexEscaped: false,
cursorPosition: query.length,
});
const expectedNewQuery = `lang:"my language"`;
expect(newQuery).toEqual(expectedNewQuery);
expect(newCursorPosition).toBe(newQuery.length);
});
test('completeSuggestion completes on query parts that are inbetween other parts', () => {
const part1 = String.raw`repo:^github\.com/sourcebot\x2ddev/sourcebot$`;
const suggestionQuery = 'Type';
const part2 = String.raw`lang:${suggestionQuery}`;
const part3 = String.raw`case:auto`;
const query = `${part1} ${part2} ${part3}`;
const suggestion = 'TypeScript';
const cursorPosition = ([part1, part2].join(" ").length);
const { newQuery, newCursorPosition } = completeSuggestion({
query,
suggestionQuery,
suggestion,
trailingSpace: false,
regexEscaped: false,
cursorPosition,
});
const expectedPart2 = "lang:TypeScript";
const expectedNewQuery = String.raw`${part1} ${expectedPart2} ${part3}`;
expect(newQuery).toEqual(expectedNewQuery);
expect(newCursorPosition).toBe([part1, expectedPart2].join(" ").length);
});
test('completeSuggestions regex escapes suggestions when regexEscaped is true', () => {
const query = "repo:github";
const { newQuery, newCursorPosition } = completeSuggestion({
query,
suggestionQuery: "github",
suggestion: "github.com/sourcebot-dev/sourcebot",
trailingSpace: true,
regexEscaped: true,
cursorPosition: query.length,
});
const expectedNewQuery = String.raw`repo:^github\.com/sourcebot\x2ddev/sourcebot$ `;
expect(newQuery).toEqual(expectedNewQuery);
expect(newCursorPosition).toBe(newQuery.length);
});

View file

@ -0,0 +1,453 @@
'use client';
import { isDefined } from "@/lib/utils";
import { CommitIcon, MixerVerticalIcon } from "@radix-ui/react-icons";
import { IconProps } from "@radix-ui/react-icons/dist/types";
import assert from "assert";
import clsx from "clsx";
import escapeStringRegexp from "escape-string-regexp";
import Fuse from "fuse.js";
import { forwardRef, Ref, useEffect, useMemo, useState } from "react";
import {
archivedModeSuggestions,
caseModeSuggestions,
forkModeSuggestions,
publicModeSuggestions,
refineModeSuggestions,
suggestionModeMappings
} from "./constants";
type Icon = React.ForwardRefExoticComponent<IconProps & React.RefAttributes<SVGSVGElement>>;
export type Suggestion = {
value: string;
description?: string;
spotlight?: boolean;
}
export type SuggestionMode =
"refine" |
"archived" |
"file" |
"language" |
"case" |
"fork" |
"public" |
"revision" |
"symbol" |
"content" |
"repo";
interface SearchSuggestionsBoxProps {
query: string;
onCompletion: (newQuery: string, newCursorPosition: number) => void,
isEnabled: boolean;
cursorPosition: number;
isFocused: boolean;
onFocus: () => void;
onBlur: () => void;
onReturnFocus: () => void;
data: {
repos: Suggestion[];
languages: Suggestion[];
}
}
const SearchSuggestionsBox = forwardRef(({
query,
onCompletion,
isEnabled,
data,
cursorPosition,
isFocused,
onFocus,
onBlur,
onReturnFocus,
}: SearchSuggestionsBoxProps, ref: Ref<HTMLDivElement>) => {
const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState(0);
const { suggestionQuery, suggestionMode } = useMemo<{ suggestionQuery?: string, suggestionMode?: SuggestionMode }>(() => {
const { queryParts, cursorIndex } = splitQuery(query, cursorPosition);
if (queryParts.length === 0) {
return {};
}
const part = queryParts[cursorIndex];
// Check if the query part starts with one of the
// prefixes. If it does, then we are in the corresponding
// suggestion mode for that prefix.
const suggestionMode = (() => {
for (const mapping of suggestionModeMappings) {
for (const prefix of mapping.prefixes) {
if (part.startsWith(prefix)) {
return mapping.suggestionMode;
}
}
}
})();
if (suggestionMode) {
const index = part.indexOf(":");
return {
suggestionQuery: part.substring(index + 1),
suggestionMode,
}
}
// Default to the refine suggestion mode
// if there was no match.
return {
suggestionQuery: part,
suggestionMode: "refine",
}
}, [cursorPosition, query]);
const { suggestions, isHighlightEnabled, Icon, onSuggestionClicked } = useMemo(() => {
if (!isDefined(suggestionQuery) || !isDefined(suggestionMode)) {
return {};
}
const createOnSuggestionClickedHandler = (params: { regexEscaped?: boolean, trailingSpace?: boolean } = {}) => {
const {
regexEscaped = false,
trailingSpace = true
} = params;
const onSuggestionClicked = (suggestion: string) => {
const { newQuery, newCursorPosition } = completeSuggestion({
query,
cursorPosition,
regexEscaped,
trailingSpace,
suggestion,
suggestionQuery,
});
onCompletion(newQuery, newCursorPosition);
}
return onSuggestionClicked;
}
const {
threshold = 0.5,
limit = 10,
list,
isHighlightEnabled = false,
isSpotlightEnabled = false,
onSuggestionClicked,
Icon,
} = ((): {
threshold?: number,
limit?: number,
list: Suggestion[],
isHighlightEnabled?: boolean,
isSpotlightEnabled?: boolean,
onSuggestionClicked: (value: string) => void,
Icon?: Icon
} => {
switch (suggestionMode) {
case "public":
return {
list: publicModeSuggestions,
onSuggestionClicked: createOnSuggestionClickedHandler(),
}
case "fork":
return {
list: forkModeSuggestions,
onSuggestionClicked: createOnSuggestionClickedHandler(),
}
case "case":
return {
list: caseModeSuggestions,
onSuggestionClicked: createOnSuggestionClickedHandler(),
}
case "archived":
return {
list: archivedModeSuggestions,
onSuggestionClicked: createOnSuggestionClickedHandler(),
}
case "repo":
return {
list: data.repos,
Icon: CommitIcon,
onSuggestionClicked: createOnSuggestionClickedHandler({ regexEscaped: true }),
}
case "language": {
return {
list: data.languages,
onSuggestionClicked: createOnSuggestionClickedHandler(),
isSpotlightEnabled: true,
}
}
case "refine":
return {
threshold: 0.1,
list: refineModeSuggestions,
isHighlightEnabled: true,
isSpotlightEnabled: true,
Icon: MixerVerticalIcon,
onSuggestionClicked: createOnSuggestionClickedHandler({ trailingSpace: false }),
}
case "file":
case "revision":
case "content":
case "symbol":
return {
list: [],
onSuggestionClicked: createOnSuggestionClickedHandler(),
}
}
})();
const fuse = new Fuse(list, {
threshold,
keys: ['value'],
isCaseSensitive: true,
});
const suggestions = (() => {
if (suggestionQuery.length === 0) {
// If spotlight is enabled, get the suggestions that are
// flagged to be surfaced.
if (isSpotlightEnabled) {
const spotlightSuggestions = list.filter((suggestion) => suggestion.spotlight);
return spotlightSuggestions;
// Otherwise, just show the Nth first suggestions.
} else {
return list.slice(0, limit);
}
}
// Special case: don't show any suggestions if the query
// is the keyword "or".
if (suggestionQuery === "or") {
return [];
}
return fuse.search(suggestionQuery, {
limit,
}).map(result => result.item);
})();
return {
suggestions,
isHighlightEnabled,
Icon,
onSuggestionClicked,
}
}, [suggestionQuery, suggestionMode, onCompletion, cursorPosition, data.repos, data.languages, query]);
// When the list of suggestions change, reset the highlight index
useEffect(() => {
setHighlightedSuggestionIndex(0);
}, [suggestions]);
const suggestionModeText = useMemo(() => {
if (!suggestionMode) {
return "";
}
switch (suggestionMode) {
case "repo":
return "Repositories";
case "refine":
return "Refine search"
default:
return "";
}
}, [suggestionMode]);
if (
!isEnabled ||
!suggestions ||
suggestions.length === 0
) {
return null;
}
return (
<div
ref={ref}
className="w-full absolute z-10 top-12 border rounded-md bg-background drop-shadow-2xl p-2"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.stopPropagation();
const value = suggestions[highlightedSuggestionIndex].value;
onSuggestionClicked(value);
}
if (e.key === 'ArrowUp') {
e.stopPropagation();
setHighlightedSuggestionIndex((curIndex) => {
return curIndex <= 0 ? suggestions.length - 1 : curIndex - 1;
});
}
if (e.key === 'ArrowDown') {
e.stopPropagation();
setHighlightedSuggestionIndex((curIndex) => {
return curIndex >= suggestions.length - 1 ? 0 : curIndex + 1;
});
}
if (e.key === 'Escape') {
e.stopPropagation();
onReturnFocus();
}
}}
onFocus={onFocus}
onBlur={onBlur}
>
<p className="text-muted-foreground text-sm mb-1">
{suggestionModeText}
</p>
{suggestions.map((result, index) => (
<div
key={index}
className={clsx("flex flex-row items-center font-mono text-sm hover:bg-muted rounded-md px-1 py-0.5 cursor-pointer", {
"bg-muted": isFocused && index === highlightedSuggestionIndex,
})}
tabIndex={-1}
onClick={() => {
onSuggestionClicked(result.value)
}}
>
{Icon && (
<Icon className="w-3 h-3 mr-2" />
)}
<div className="flex flex-row items-center">
<span
className={clsx('mr-2 flex-none', {
"text-highlight": isHighlightEnabled
})}
>
{result.value}
</span>
{result.description && (
<span className="text-muted-foreground font-light">
{result.description}
</span>
)}
</div>
</div>
))}
{isFocused && (
<div className="flex flex-row items-center justify-end mt-1">
<span className="text-muted-foreground text-xs">
Press <kbd className="font-mono text-xs font-bold">Enter</kbd> to select
</span>
</div>
)}
</div>
)
});
SearchSuggestionsBox.displayName = "SearchSuggestionsBox";
export { SearchSuggestionsBox };
export const splitQuery = (query: string, cursorPos: number) => {
const queryParts = [];
const seperator = " ";
let cursorIndex = 0;
let accumulator = "";
let isInQuoteCapture = false;
for (let i = 0; i < query.length; i++) {
if (i === cursorPos) {
cursorIndex = queryParts.length;
}
if (query[i] === "\"") {
isInQuoteCapture = !isInQuoteCapture;
}
if (!isInQuoteCapture && query[i] === seperator) {
queryParts.push(accumulator);
accumulator = "";
continue;
}
accumulator += query[i];
}
queryParts.push(accumulator);
// Edge case: if the cursor is at the end of the query, set the cursor index to the last query part
if (cursorPos === query.length) {
cursorIndex = queryParts.length - 1;
}
// @note: since we're guaranteed to have at least one query part, we can safely assume that the cursor position
// will be within bounds.
assert(cursorIndex >= 0 && cursorIndex < queryParts.length, "Cursor position is out of bounds");
return {
queryParts,
cursorIndex
}
}
export const completeSuggestion = (params: {
query: string,
suggestionQuery: string,
cursorPosition: number,
suggestion: string,
trailingSpace: boolean,
regexEscaped: boolean,
}) => {
const {
query,
suggestionQuery,
cursorPosition,
suggestion,
trailingSpace,
regexEscaped,
} = params;
const { queryParts, cursorIndex } = splitQuery(query, cursorPosition);
const start = queryParts.slice(0, cursorIndex).join(" ");
const end = queryParts.slice(cursorIndex + 1).join(" ");
let part = queryParts[cursorIndex];
// Remove whatever query we have in the suggestion so far (if any).
// For example, if our part is "repo:gith", then we want to remove "gith"
// from the part before we complete the suggestion.
if (suggestionQuery.length > 0) {
part = part.slice(0, -suggestionQuery.length);
}
if (regexEscaped) {
part = part + `^${escapeStringRegexp(suggestion)}$`;
} else if (suggestion.includes(" ")) {
part = part + `"${suggestion}"`;
} else {
part = part + suggestion;
}
// Add a trailing space if we are at the end of the query
if (trailingSpace && cursorIndex === queryParts.length - 1) {
part += " ";
}
let newQuery = [
...(start.length > 0 ? [start] : []),
part,
].join(" ");
const newCursorPosition = newQuery.length;
newQuery = [
newQuery,
...(end.length > 0 ? [end] : []),
].join(" ");
return {
newQuery,
newCursorPosition,
}
}

View file

@ -0,0 +1,25 @@
import { LanguageSupport, StreamLanguage } from "@codemirror/language";
import { tags as t } from '@lezer/highlight';
const zoektLanguage = StreamLanguage.define({
token: (stream) => {
if (stream.match(/-?(file|branch|revision|rev|case|repo|lang|content|sym|archived|fork|public):/)) {
return t.keyword.toString();
}
if (stream.match(/\bor\b/)) {
return t.keyword.toString();
}
if (stream.match(/(\(|\))/)) {
return t.paren.toString();
}
stream.next();
return null;
},
});
export const zoekt = () => {
return new LanguageSupport(zoektLanguage);
}

View file

@ -4,12 +4,12 @@ import Image from "next/image";
import { Suspense } from "react"; import { Suspense } from "react";
import logoDark from "../../public/sb_logo_dark_large.png"; import logoDark from "../../public/sb_logo_dark_large.png";
import logoLight from "../../public/sb_logo_light_large.png"; import logoLight from "../../public/sb_logo_light_large.png";
import { NavigationMenu } from "./navigationMenu"; import { NavigationMenu } from "./components/navigationMenu";
import { RepositoryCarousel } from "./repositoryCarousel"; import { RepositoryCarousel } from "./components/repositoryCarousel";
import { SearchBar } from "./searchBar"; import { SearchBar } from "./components/searchBar";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { SymbolIcon } from "@radix-ui/react-icons"; import { SymbolIcon } from "@radix-ui/react-icons";
import { UpgradeToast } from "./upgradeToast"; import { UpgradeToast } from "./components/upgradeToast";
export default async function Home() { export default async function Home() {
@ -18,7 +18,7 @@ export default async function Home() {
<NavigationMenu /> <NavigationMenu />
<UpgradeToast /> <UpgradeToast />
<div className="flex flex-col justify-center items-center mt-8 mb-8 md:mt-18 max-w-[90%]"> <div className="flex flex-col justify-center items-center mt-8 mb-8 md:mt-18 w-full px-5">
<div className="max-h-44 w-auto"> <div className="max-h-44 w-auto">
<Image <Image
src={logoDark} src={logoDark}
@ -33,18 +33,17 @@ export default async function Home() {
priority={true} priority={true}
/> />
</div> </div>
<div className="w-full flex flex-row mt-4"> <SearchBar
<SearchBar autoFocus={true}
autoFocus={true} className="mt-4 w-full max-w-[800px]"
/> />
</div>
<div className="mt-8"> <div className="mt-8">
<Suspense fallback={<div>...</div>}> <Suspense fallback={<div>...</div>}>
<RepositoryList /> <RepositoryList />
</Suspense> </Suspense>
</div> </div>
<Separator className="mt-5 mb-8" />
<div className="flex flex-col items-center w-fit gap-6"> <div className="flex flex-col items-center w-fit gap-6">
<Separator className="mt-5" />
<span className="font-semibold">How to search</span> <span className="font-semibold">How to search</span>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5"> <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<HowToSection <HowToSection
@ -76,7 +75,7 @@ export default async function Home() {
<Query query="lang:typescript"><Highlight>lang:</Highlight>typescript</Query> <QueryExplanation>(by language)</QueryExplanation> <Query query="lang:typescript"><Highlight>lang:</Highlight>typescript</Query> <QueryExplanation>(by language)</QueryExplanation>
</QueryExample> </QueryExample>
<QueryExample> <QueryExample>
<Query query="revision:HEAD"><Highlight>revision:</Highlight>HEAD</Query> <QueryExplanation>(by branch or tag)</QueryExplanation> <Query query="rev:HEAD"><Highlight>rev:</Highlight>HEAD</Query> <QueryExplanation>(by branch or tag)</QueryExplanation>
</QueryExample> </QueryExample>
</HowToSection> </HowToSection>
<HowToSection <HowToSection

View file

@ -1,5 +1,5 @@
import { Suspense } from "react"; import { Suspense } from "react";
import { NavigationMenu } from "../navigationMenu"; import { NavigationMenu } from "../components/navigationMenu";
import { RepositoryTable } from "./repositoryTable"; import { RepositoryTable } from "./repositoryTable";
export default function ReposPage() { export default function ReposPage() {

View file

@ -45,7 +45,6 @@ export const FilterPanel = ({
"Language", "Language",
matches, matches,
(key) => { (key) => {
// @todo: Get language icons
return { return {
key, key,
displayName: key, displayName: key,

View file

@ -18,8 +18,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import logoDark from "../../../public/sb_logo_dark.png"; import logoDark from "../../../public/sb_logo_dark.png";
import logoLight from "../../../public/sb_logo_light.png"; import logoLight from "../../../public/sb_logo_light.png";
import { search } from "../api/(client)/client"; import { search } from "../api/(client)/client";
import { SearchBar } from "../searchBar"; import { SearchBar } from "../components/searchBar";
import { SettingsDropdown } from "../settingsDropdown"; import { SettingsDropdown } from "../components/settingsDropdown";
import { CodePreviewPanel } from "./components/codePreviewPanel"; import { CodePreviewPanel } from "./components/codePreviewPanel";
import { FilterPanel } from "./components/filterPanel"; import { FilterPanel } from "./components/filterPanel";
import { SearchResultsPanel } from "./components/searchResultsPanel"; import { SearchResultsPanel } from "./components/searchResultsPanel";
@ -109,7 +109,7 @@ export default function SearchPage() {
totalMatchCount: searchResponse.Result.MatchCount, totalMatchCount: searchResponse.Result.MatchCount,
isBranchFilteringEnabled, isBranchFilteringEnabled,
} }
}, [searchResponse, searchQuery]); }, [searchResponse]);
const isMoreResultsButtonVisible = useMemo(() => { const isMoreResultsButtonVisible = useMemo(() => {
return totalMatchCount > maxMatchDisplayCount; return totalMatchCount > maxMatchDisplayCount;
@ -161,6 +161,7 @@ export default function SearchPage() {
<SearchBar <SearchBar
size="sm" size="sm"
defaultQuery={searchQuery} defaultQuery={searchQuery}
className="w-full"
/> />
</div> </div>
<SettingsDropdown <SettingsDropdown

View file

@ -1,178 +0,0 @@
'use client';
import { useTailwind } from "@/hooks/useTailwind";
import { SearchQueryParams } from "@/lib/types";
import { cn, createPathWithQueryParams } from "@/lib/utils";
import {
cursorCharLeft,
cursorCharRight,
cursorDocEnd,
cursorDocStart,
cursorLineBoundaryBackward,
cursorLineBoundaryForward,
deleteCharBackward,
deleteCharForward,
deleteGroupBackward,
deleteGroupForward,
deleteLineBoundaryBackward,
deleteLineBoundaryForward,
history,
historyKeymap,
selectAll,
selectCharLeft,
selectCharRight,
selectDocEnd,
selectDocStart,
selectLineBoundaryBackward,
selectLineBoundaryForward
} from "@codemirror/commands";
import { LanguageSupport, StreamLanguage } from "@codemirror/language";
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
import CodeMirror, { KeyBinding, keymap, ReactCodeMirrorRef } from "@uiw/react-codemirror";
import { cva } from "class-variance-authority";
import { useRouter } from "next/navigation";
import { useMemo, useRef, useState } from "react";
import { useHotkeys } from 'react-hotkeys-hook';
interface SearchBarProps {
className?: string;
size?: "default" | "sm";
defaultQuery?: string;
autoFocus?: boolean;
}
const searchBarKeymap: readonly KeyBinding[] = ([
{ key: "ArrowLeft", run: cursorCharLeft, shift: selectCharLeft, preventDefault: true },
{ key: "ArrowRight", run: cursorCharRight, shift: selectCharRight, preventDefault: true },
{ key: "Home", run: cursorLineBoundaryBackward, shift: selectLineBoundaryBackward, preventDefault: true },
{ key: "Mod-Home", run: cursorDocStart, shift: selectDocStart },
{ key: "End", run: cursorLineBoundaryForward, shift: selectLineBoundaryForward, preventDefault: true },
{ key: "Mod-End", run: cursorDocEnd, shift: selectDocEnd },
{ key: "Mod-a", run: selectAll },
{ key: "Backspace", run: deleteCharBackward, shift: deleteCharBackward },
{ key: "Delete", run: deleteCharForward },
{ key: "Mod-Backspace", mac: "Alt-Backspace", run: deleteGroupBackward },
{ key: "Mod-Delete", mac: "Alt-Delete", run: deleteGroupForward },
{ mac: "Mod-Backspace", run: deleteLineBoundaryBackward },
{ mac: "Mod-Delete", run: deleteLineBoundaryForward }
] as KeyBinding[]).concat(historyKeymap);
const zoektLanguage = StreamLanguage.define({
token: (stream) => {
if (stream.match(/-?(file|branch|revision|rev|case|repo|lang|content|sym):/)) {
return t.keyword.toString();
}
if (stream.match(/\bor\b/)) {
return t.keyword.toString();
}
stream.next();
return null;
},
});
const zoekt = () =>{
return new LanguageSupport(zoektLanguage);
}
const extensions = [
keymap.of(searchBarKeymap),
history(),
zoekt()
];
const searchBarVariants = cva(
"flex items-center w-full p-0.5 border rounded-md",
{
variants: {
size: {
default: "h-10",
sm: "h-8"
}
},
defaultVariants: {
size: "default",
}
}
);
export const SearchBar = ({
className,
size,
defaultQuery,
autoFocus,
}: SearchBarProps) => {
const router = useRouter();
const tailwind = useTailwind();
const theme = useMemo(() => {
return createTheme({
theme: 'light',
settings: {
background: tailwind.theme.colors.background,
foreground: tailwind.theme.colors.foreground,
caret: '#AEAFAD',
},
styles: [
{
tag: t.keyword,
color: tailwind.theme.colors.highlight,
},
],
});
}, [tailwind]);
const [query, setQuery] = useState(defaultQuery ?? "");
const editorRef = useRef<ReactCodeMirrorRef>(null);
useHotkeys('/', (event) => {
event.preventDefault();
editorRef.current?.view?.focus();
if (editorRef.current?.view) {
cursorDocEnd({
state: editorRef.current.view.state,
dispatch: editorRef.current.view.dispatch,
});
}
});
const onSubmit = () => {
const url = createPathWithQueryParams('/search',
[SearchQueryParams.query, query],
)
router.push(url);
}
return (
<div
className={cn(searchBarVariants({ size, className }))}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
onSubmit();
}
}}
>
<CodeMirror
ref={editorRef}
className="grow"
placeholder={"Search..."}
value={query}
onChange={(value) => {
setQuery(value);
}}
theme={theme}
basicSetup={false}
extensions={extensions}
indentWithTab={false}
autoFocus={autoFocus ?? false}
/>
</div>
)
}

View file

@ -0,0 +1,24 @@
'use client';
import { useEffect } from "react";
export const useClickListener = (elementSelector: string, onClick: (elementClicked: boolean) => void) => {
useEffect(() => {
const handleClick = (event: MouseEvent) => {
const element = document.querySelector(elementSelector);
if (element) {
const isElementClicked = element.contains(event.target as Node);
onClick(isElementClicked);
}
};
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
};
}, [onClick, elementSelector]);
return null;
}

View file

@ -7,7 +7,7 @@ import tailwindConfig from '../../tailwind.config';
export const useTailwind = () => { export const useTailwind = () => {
const tailwind = useMemo(() => { const tailwind = useMemo(() => {
return resolveConfig(tailwindConfig); return resolveConfig(tailwindConfig);
}, [tailwindConfig]); }, []);
return tailwind; return tailwind;
} }

View file

@ -1,6 +1,6 @@
import escapeStringRegexp from "escape-string-regexp"; import escapeStringRegexp from "escape-string-regexp";
import { SHARD_MAX_MATCH_COUNT, TOTAL_MAX_MATCH_COUNT } from "../environment"; import { SHARD_MAX_MATCH_COUNT, TOTAL_MAX_MATCH_COUNT } from "../environment";
import { listRepositoriesResponseSchema, searchResponseSchema, zoektSearchResponseSchema } from "../schemas"; import { listRepositoriesResponseSchema, zoektSearchResponseSchema } from "../schemas";
import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "../types"; import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "../types";
import { fileNotFound, invalidZoektResponse, ServiceError, unexpectedError } from "../serviceError"; import { fileNotFound, invalidZoektResponse, ServiceError, unexpectedError } from "../serviceError";
import { isServiceError } from "../utils"; import { isServiceError } from "../utils";

View file

@ -119,3 +119,8 @@ export const base64Decode = (base64: string): string => {
const binString = atob(base64); const binString = atob(base64);
return Buffer.from(Uint8Array.from(binString, (m) => m.codePointAt(0)!).buffer).toString(); return Buffer.from(Uint8Array.from(binString, (m) => m.codePointAt(0)!).buffer).toString();
} }
// @see: https://stackoverflow.com/a/65959350/23221295
export const isDefined = <T>(arg: T | null | undefined): arg is T extends null | undefined ? never : T => {
return arg !== null && arg !== undefined;
}

View file

@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config'
import tsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
plugins: [tsconfigPaths()],
test: {
environment: 'jsdom',
watch: false,
},
})

337
yarn.lock
View file

@ -1779,6 +1779,16 @@
chai "^5.1.2" chai "^5.1.2"
tinyrainbow "^1.2.0" tinyrainbow "^1.2.0"
"@vitest/expect@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.5.tgz#5a6afa6314cae7a61847927bb5bc038212ca7381"
integrity sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q==
dependencies:
"@vitest/spy" "2.1.5"
"@vitest/utils" "2.1.5"
chai "^5.1.2"
tinyrainbow "^1.2.0"
"@vitest/mocker@2.1.4": "@vitest/mocker@2.1.4":
version "2.1.4" version "2.1.4"
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.4.tgz#0dc07edb9114f7f080a0181fbcdb16cd4a2d855d" resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.4.tgz#0dc07edb9114f7f080a0181fbcdb16cd4a2d855d"
@ -1788,6 +1798,15 @@
estree-walker "^3.0.3" estree-walker "^3.0.3"
magic-string "^0.30.12" magic-string "^0.30.12"
"@vitest/mocker@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.5.tgz#54ee50648bc0bb606dfc58e13edfacb8b9208324"
integrity sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ==
dependencies:
"@vitest/spy" "2.1.5"
estree-walker "^3.0.3"
magic-string "^0.30.12"
"@vitest/pretty-format@2.1.4", "@vitest/pretty-format@^2.1.4": "@vitest/pretty-format@2.1.4", "@vitest/pretty-format@^2.1.4":
version "2.1.4" version "2.1.4"
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.4.tgz#fc31993bdc1ef5a6c1a4aa6844e7ba55658a4f9f" resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.4.tgz#fc31993bdc1ef5a6c1a4aa6844e7ba55658a4f9f"
@ -1795,6 +1814,13 @@
dependencies: dependencies:
tinyrainbow "^1.2.0" tinyrainbow "^1.2.0"
"@vitest/pretty-format@2.1.5", "@vitest/pretty-format@^2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.5.tgz#bc79b8826d4a63dc04f2a75d2944694039fa50aa"
integrity sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==
dependencies:
tinyrainbow "^1.2.0"
"@vitest/runner@2.1.4": "@vitest/runner@2.1.4":
version "2.1.4" version "2.1.4"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.4.tgz#f9346500bdd0be1c926daaac5d683bae87ceda2c" resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.4.tgz#f9346500bdd0be1c926daaac5d683bae87ceda2c"
@ -1803,6 +1829,14 @@
"@vitest/utils" "2.1.4" "@vitest/utils" "2.1.4"
pathe "^1.1.2" pathe "^1.1.2"
"@vitest/runner@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.5.tgz#4d5e2ba2dfc0af74e4b0f9f3f8be020559b26ea9"
integrity sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g==
dependencies:
"@vitest/utils" "2.1.5"
pathe "^1.1.2"
"@vitest/snapshot@2.1.4": "@vitest/snapshot@2.1.4":
version "2.1.4" version "2.1.4"
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.4.tgz#ef8c3f605fbc23a32773256d37d3fdfd9b23d353" resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.4.tgz#ef8c3f605fbc23a32773256d37d3fdfd9b23d353"
@ -1812,6 +1846,15 @@
magic-string "^0.30.12" magic-string "^0.30.12"
pathe "^1.1.2" pathe "^1.1.2"
"@vitest/snapshot@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.5.tgz#a09a8712547452a84e08b3ec97b270d9cc156b4f"
integrity sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg==
dependencies:
"@vitest/pretty-format" "2.1.5"
magic-string "^0.30.12"
pathe "^1.1.2"
"@vitest/spy@2.1.4": "@vitest/spy@2.1.4":
version "2.1.4" version "2.1.4"
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.4.tgz#4e90f9783437c5841a27c80f8fd84d7289a6100a" resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.4.tgz#4e90f9783437c5841a27c80f8fd84d7289a6100a"
@ -1819,6 +1862,13 @@
dependencies: dependencies:
tinyspy "^3.0.2" tinyspy "^3.0.2"
"@vitest/spy@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.5.tgz#f790d1394a5030644217ce73562e92465e83147e"
integrity sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==
dependencies:
tinyspy "^3.0.2"
"@vitest/utils@2.1.4": "@vitest/utils@2.1.4":
version "2.1.4" version "2.1.4"
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.4.tgz#6d67ac966647a21ce8bc497472ce230de3b64537" resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.4.tgz#6d67ac966647a21ce8bc497472ce230de3b64537"
@ -1828,6 +1878,15 @@
loupe "^3.1.2" loupe "^3.1.2"
tinyrainbow "^1.2.0" tinyrainbow "^1.2.0"
"@vitest/utils@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.5.tgz#0e19ce677c870830a1573d33ee86b0d6109e9546"
integrity sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==
dependencies:
"@vitest/pretty-format" "2.1.5"
loupe "^3.1.2"
tinyrainbow "^1.2.0"
abort-controller@^3.0.0: abort-controller@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
@ -1845,6 +1904,13 @@ acorn@^8.9.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
agent-base@^7.0.2, agent-base@^7.1.0:
version "7.1.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317"
integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==
dependencies:
debug "^4.3.4"
ajv@^6.12.4: ajv@^6.12.4:
version "6.12.6" version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@ -2342,6 +2408,13 @@ cssesc@^3.0.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
cssstyle@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.1.0.tgz#161faee382af1bafadb6d3867a92a19bcb4aea70"
integrity sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==
dependencies:
rrweb-cssom "^0.7.1"
csstype@^3.0.2: csstype@^3.0.2:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
@ -2352,6 +2425,14 @@ damerau-levenshtein@^1.0.8:
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
data-urls@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde"
integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==
dependencies:
whatwg-mimetype "^4.0.0"
whatwg-url "^14.0.0"
data-view-buffer@^1.0.1: data-view-buffer@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2"
@ -2379,6 +2460,13 @@ data-view-byte-offset@^1.0.0:
es-errors "^1.3.0" es-errors "^1.3.0"
is-data-view "^1.0.1" is-data-view "^1.0.1"
debug@4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.7:
version "4.3.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
dependencies:
ms "^2.1.3"
debug@^3.2.7: debug@^3.2.7:
version "3.2.7" version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
@ -2386,12 +2474,10 @@ debug@^3.2.7:
dependencies: dependencies:
ms "^2.1.1" ms "^2.1.1"
debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.7: decimal.js@^10.4.3:
version "4.3.7" version "10.4.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
dependencies:
ms "^2.1.3"
deep-eql@^5.0.1: deep-eql@^5.0.1:
version "5.0.2" version "5.0.2"
@ -2552,6 +2638,11 @@ enhanced-resolve@^5.15.0:
graceful-fs "^4.2.4" graceful-fs "^4.2.4"
tapable "^2.2.0" tapable "^2.2.0"
entities@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
error-ex@^1.3.1: error-ex@^1.3.1:
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@ -2658,6 +2749,11 @@ es-iterator-helpers@^1.0.19:
iterator.prototype "^1.1.2" iterator.prototype "^1.1.2"
safe-array-concat "^1.1.2" safe-array-concat "^1.1.2"
es-module-lexer@^1.5.4:
version "1.5.4"
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78"
integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==
es-object-atoms@^1.0.0: es-object-atoms@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941"
@ -3276,6 +3372,11 @@ globby@^11.1.0:
merge2 "^1.4.1" merge2 "^1.4.1"
slash "^3.0.0" slash "^3.0.0"
globrex@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
gopd@^1.0.1: gopd@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
@ -3344,11 +3445,41 @@ hosted-git-info@^2.1.4:
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
html-encoding-sniffer@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448"
integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==
dependencies:
whatwg-encoding "^3.1.1"
http-proxy-agent@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==
dependencies:
agent-base "^7.1.0"
debug "^4.3.4"
http-status-codes@^2.3.0: http-status-codes@^2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.3.0.tgz#987fefb28c69f92a43aecc77feec2866349a8bfc" resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.3.0.tgz#987fefb28c69f92a43aecc77feec2866349a8bfc"
integrity sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA== integrity sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==
https-proxy-agent@^7.0.5:
version "7.0.5"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2"
integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==
dependencies:
agent-base "^7.0.2"
debug "4"
iconv-lite@0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
ieee754@^1.2.1: ieee754@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@ -3547,6 +3678,11 @@ is-path-inside@^3.0.3:
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-potential-custom-element-name@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
is-regex@^1.1.4: is-regex@^1.1.4:
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
@ -3669,6 +3805,33 @@ js-yaml@^4.1.0:
dependencies: dependencies:
argparse "^2.0.1" argparse "^2.0.1"
jsdom@^25.0.1:
version "25.0.1"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-25.0.1.tgz#536ec685c288fc8a5773a65f82d8b44badcc73ef"
integrity sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==
dependencies:
cssstyle "^4.1.0"
data-urls "^5.0.0"
decimal.js "^10.4.3"
form-data "^4.0.0"
html-encoding-sniffer "^4.0.0"
http-proxy-agent "^7.0.2"
https-proxy-agent "^7.0.5"
is-potential-custom-element-name "^1.0.1"
nwsapi "^2.2.12"
parse5 "^7.1.2"
rrweb-cssom "^0.7.1"
saxes "^6.0.0"
symbol-tree "^3.2.4"
tough-cookie "^5.0.0"
w3c-xmlserializer "^5.0.0"
webidl-conversions "^7.0.0"
whatwg-encoding "^3.1.1"
whatwg-mimetype "^4.0.0"
whatwg-url "^14.0.0"
ws "^8.18.0"
xml-name-validator "^5.0.0"
json-buffer@3.0.1: json-buffer@3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
@ -4013,6 +4176,11 @@ npm-run-all@^4.1.5:
shell-quote "^1.6.1" shell-quote "^1.6.1"
string.prototype.padend "^3.0.0" string.prototype.padend "^3.0.0"
nwsapi@^2.2.12:
version "2.2.13"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.13.tgz#e56b4e98960e7a040e5474536587e599c4ff4655"
integrity sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==
object-assign@^4.0.1, object-assign@^4.1.1: object-assign@^4.0.1, object-assign@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@ -4148,6 +4316,13 @@ parse-json@^4.0.0:
error-ex "^1.3.1" error-ex "^1.3.1"
json-parse-better-errors "^1.0.1" json-parse-better-errors "^1.0.1"
parse5@^7.1.2:
version "7.2.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.2.1.tgz#8928f55915e6125f430cc44309765bf17556a33a"
integrity sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==
dependencies:
entities "^4.5.0"
path-exists@^4.0.0: path-exists@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@ -4389,7 +4564,7 @@ ps-tree@^1.2.0:
dependencies: dependencies:
event-stream "=3.3.4" event-stream "=3.3.4"
punycode@^2.1.0: punycode@^2.1.0, punycode@^2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
@ -4612,6 +4787,11 @@ rollup@^4.20.0:
"@rollup/rollup-win32-x64-msvc" "4.25.0" "@rollup/rollup-win32-x64-msvc" "4.25.0"
fsevents "~2.3.2" fsevents "~2.3.2"
rrweb-cssom@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz#c73451a484b86dd7cfb1e0b2898df4b703183e4b"
integrity sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==
run-parallel@^1.1.9: run-parallel@^1.1.9:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
@ -4653,6 +4833,18 @@ safe-stable-stringify@^2.3.1:
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd"
integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==
"safer-buffer@>= 2.1.2 < 3.0.0":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
saxes@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5"
integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==
dependencies:
xmlchars "^2.2.0"
scheduler@^0.23.2: scheduler@^0.23.2:
version "0.23.2" version "0.23.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
@ -4849,7 +5041,7 @@ stackback@0.0.2:
resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==
std-env@^3.7.0: std-env@^3.7.0, std-env@^3.8.0:
version "3.8.0" version "3.8.0"
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5"
integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w== integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==
@ -5054,6 +5246,11 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
tailwind-merge@^2.5.2: tailwind-merge@^2.5.2:
version "2.5.3" version "2.5.3"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.5.3.tgz#579546e14ddda24462e0303acd8798c50f5511bb" resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.5.3.tgz#579546e14ddda24462e0303acd8798c50f5511bb"
@ -5156,6 +5353,18 @@ tinyspy@^3.0.2:
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a"
integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==
tldts-core@^6.1.63:
version "6.1.63"
resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.63.tgz#094f2b05faf90cf1e228eda1caef658425c7c912"
integrity sha512-H1XCt54xY+QPbwhTgmxLkepX0MVHu3USfMmejiCOdkMbRcP22Pn2FVF127r/GWXVDmXTRezyF3Ckvhn4Fs6j7Q==
tldts@^6.1.32:
version "6.1.63"
resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.63.tgz#81a3898554ba1dbbdc6844ed4e68c574f09fed32"
integrity sha512-YWwhsjyn9sB/1rOkSRYxvkN/wl5LFM1QDv6F2pVR+pb/jFne4EOBxHfkKVWvDIBEAw9iGOwwubHtQTm0WRT5sQ==
dependencies:
tldts-core "^6.1.63"
to-regex-range@^5.0.1: to-regex-range@^5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@ -5163,6 +5372,20 @@ to-regex-range@^5.0.1:
dependencies: dependencies:
is-number "^7.0.0" is-number "^7.0.0"
tough-cookie@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.0.0.tgz#6b6518e2b5c070cf742d872ee0f4f92d69eac1af"
integrity sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==
dependencies:
tldts "^6.1.32"
tr46@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.0.0.tgz#3b46d583613ec7283020d79019f1335723801cec"
integrity sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==
dependencies:
punycode "^2.3.1"
tr46@~0.0.3: tr46@~0.0.3:
version "0.0.3" version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
@ -5193,6 +5416,11 @@ tsc-watch@^6.2.0:
ps-tree "^1.2.0" ps-tree "^1.2.0"
string-argv "^0.3.1" string-argv "^0.3.1"
tsconfck@^3.0.3:
version "3.1.4"
resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.4.tgz#de01a15334962e2feb526824339b51be26712229"
integrity sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==
tsconfig-paths@^3.15.0: tsconfig-paths@^3.15.0:
version "3.15.0" version "3.15.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4"
@ -5351,6 +5579,26 @@ vite-node@2.1.4:
pathe "^1.1.2" pathe "^1.1.2"
vite "^5.0.0" vite "^5.0.0"
vite-node@2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.5.tgz#cf28c637b2ebe65921f3118a165b7cf00a1cdf19"
integrity sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w==
dependencies:
cac "^6.7.14"
debug "^4.3.7"
es-module-lexer "^1.5.4"
pathe "^1.1.2"
vite "^5.0.0"
vite-tsconfig-paths@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.3.tgz#ffab28a9c2cb171e7685dd5cdcb93b132187cad5"
integrity sha512-0bz+PDlLpGfP2CigeSKL9NFTF1KtXkeHGZSSaGQSuPZH77GhoiQaA8IjYgOaynSuwlDTolSUEU0ErVvju3NURg==
dependencies:
debug "^4.1.1"
globrex "^0.1.2"
tsconfck "^3.0.3"
vite@^5.0.0: vite@^5.0.0:
version "5.4.11" version "5.4.11"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5" resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5"
@ -5388,11 +5636,44 @@ vitest@^2.1.4:
vite-node "2.1.4" vite-node "2.1.4"
why-is-node-running "^2.3.0" why-is-node-running "^2.3.0"
vitest@^2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.5.tgz#a93b7b84a84650130727baae441354e6df118148"
integrity sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A==
dependencies:
"@vitest/expect" "2.1.5"
"@vitest/mocker" "2.1.5"
"@vitest/pretty-format" "^2.1.5"
"@vitest/runner" "2.1.5"
"@vitest/snapshot" "2.1.5"
"@vitest/spy" "2.1.5"
"@vitest/utils" "2.1.5"
chai "^5.1.2"
debug "^4.3.7"
expect-type "^1.1.0"
magic-string "^0.30.12"
pathe "^1.1.2"
std-env "^3.8.0"
tinybench "^2.9.0"
tinyexec "^0.3.1"
tinypool "^1.0.1"
tinyrainbow "^1.2.0"
vite "^5.0.0"
vite-node "2.1.5"
why-is-node-running "^2.3.0"
w3c-keyname@^2.2.4: w3c-keyname@^2.2.4:
version "2.2.8" version "2.2.8"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ== integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
w3c-xmlserializer@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c"
integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==
dependencies:
xml-name-validator "^5.0.0"
web-vitals@^4.0.1: web-vitals@^4.0.1:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.3.tgz#270c4baecfbc6ec6fc15da1989e465e5f9b94fb7" resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.3.tgz#270c4baecfbc6ec6fc15da1989e465e5f9b94fb7"
@ -5403,6 +5684,31 @@ webidl-conversions@^3.0.0:
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
webidl-conversions@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
whatwg-encoding@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5"
integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==
dependencies:
iconv-lite "0.6.3"
whatwg-mimetype@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a"
integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==
whatwg-url@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.0.0.tgz#00baaa7fd198744910c4b1ef68378f2200e4ceb6"
integrity sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==
dependencies:
tr46 "^5.0.0"
webidl-conversions "^7.0.0"
whatwg-url@^5.0.0: whatwg-url@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
@ -5537,11 +5843,26 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@^8.18.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
xcase@^2.0.1: xcase@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/xcase/-/xcase-2.0.1.tgz#c7fa72caa0f440db78fd5673432038ac984450b9" resolved "https://registry.yarnpkg.com/xcase/-/xcase-2.0.1.tgz#c7fa72caa0f440db78fd5673432038ac984450b9"
integrity sha512-UmFXIPU+9Eg3E9m/728Bii0lAIuoc+6nbrNUKaRPJOFp91ih44qqGlWtxMB6kXFrRD6po+86ksHM5XHCfk6iPw== integrity sha512-UmFXIPU+9Eg3E9m/728Bii0lAIuoc+6nbrNUKaRPJOFp91ih44qqGlWtxMB6kXFrRD6po+86ksHM5XHCfk6iPw==
xml-name-validator@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673"
integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==
xmlchars@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
yaml@^2.3.4: yaml@^2.3.4:
version "2.5.1" version "2.5.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130"