Symbol suggestions (#98)

This commit is contained in:
Brendan Kellam 2024-11-28 13:26:27 -08:00 committed by GitHub
parent b115218be9
commit 120d84a046
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 201 additions and 75 deletions

View file

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- Added symbol suggestions as suggestion type. ([#98](https://github.com/sourcebot-dev/sourcebot/pull/98))
## [2.5.2] - 2024-11-27 ## [2.5.2] - 2024-11-27
### Fixed ### Fixed

View file

@ -392,3 +392,9 @@ Or if you are [building locally](#build-from-source), create a `.env.local` file
SOURCEBOT_TELEMETRY_DISABLED=1 SOURCEBOT_TELEMETRY_DISABLED=1
NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED=1 NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED=1
``` ```
## Attributions
Sourcebot makes use of the following libraries:
- [@vscode/codicons](https://github.com/microsoft/vscode-codicons) under the [CC BY 4.0 License](https://github.com/microsoft/vscode-codicons/blob/main/LICENSE).

View file

@ -59,6 +59,7 @@
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.53.0", "react-hook-form": "^7.53.0",
"react-hotkeys-hook": "^4.5.1", "react-hotkeys-hook": "^4.5.1",
"react-icons": "^5.3.0",
"react-resizable-panels": "^2.1.1", "react-resizable-panels": "^2.1.1",
"server-only": "^0.0.1", "server-only": "^0.0.1",
"sharp": "^0.33.5", "sharp": "^0.33.5",

View file

@ -255,13 +255,16 @@ export const SearchBar = ({
setIsSuggestionsBoxFocused(document.activeElement === suggestionBoxRef.current); setIsSuggestionsBoxFocused(document.activeElement === suggestionBoxRef.current);
}} }}
cursorPosition={cursorPosition} cursorPosition={cursorPosition}
data={suggestionData} onSuggestionModeChanged={(newSuggestionMode) => {
onSuggestionModeChanged={(suggestionMode) => { if (suggestionMode !== newSuggestionMode) {
setSuggestionMode(suggestionMode); console.debug(`Suggestion mode changed: ${suggestionMode} -> ${newSuggestionMode}`);
}
setSuggestionMode(newSuggestionMode);
}} }}
onSuggestionQueryChanged={(suggestionQuery) => { onSuggestionQueryChanged={(suggestionQuery) => {
setSuggestionQuery(suggestionQuery); setSuggestionQuery(suggestionQuery);
}} }}
{...suggestionData}
/> />
</div> </div>
) )

View file

@ -1,8 +1,6 @@
'use client'; 'use client';
import { isDefined } from "@/lib/utils"; 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 assert from "assert";
import clsx from "clsx"; import clsx from "clsx";
import escapeStringRegexp from "escape-string-regexp"; import escapeStringRegexp from "escape-string-regexp";
@ -16,13 +14,14 @@ import {
refineModeSuggestions, refineModeSuggestions,
suggestionModeMappings suggestionModeMappings
} from "./constants"; } from "./constants";
import { IconType } from "react-icons/lib";
type Icon = React.ForwardRefExoticComponent<IconProps & React.RefAttributes<SVGSVGElement>>; import { VscFile, VscFilter, VscRepo, VscSymbolMisc } from "react-icons/vsc";
export type Suggestion = { export type Suggestion = {
value: string; value: string;
description?: string; description?: string;
spotlight?: boolean; spotlight?: boolean;
Icon?: IconType;
} }
export type SuggestionMode = export type SuggestionMode =
@ -50,18 +49,17 @@ interface SearchSuggestionsBoxProps {
onSuggestionModeChanged: (suggestionMode: SuggestionMode) => void; onSuggestionModeChanged: (suggestionMode: SuggestionMode) => void;
onSuggestionQueryChanged: (suggestionQuery: string) => void; onSuggestionQueryChanged: (suggestionQuery: string) => void;
data: { isLoadingSuggestions: boolean;
repos: Suggestion[]; repoSuggestions: Suggestion[];
languages: Suggestion[]; fileSuggestions: Suggestion[];
files: Suggestion[]; symbolSuggestions: Suggestion[];
} languageSuggestions: Suggestion[];
} }
const SearchSuggestionsBox = forwardRef(({ const SearchSuggestionsBox = forwardRef(({
query, query,
onCompletion, onCompletion,
isEnabled, isEnabled,
data,
cursorPosition, cursorPosition,
isFocused, isFocused,
onFocus, onFocus,
@ -69,11 +67,24 @@ const SearchSuggestionsBox = forwardRef(({
onReturnFocus, onReturnFocus,
onSuggestionModeChanged, onSuggestionModeChanged,
onSuggestionQueryChanged, onSuggestionQueryChanged,
isLoadingSuggestions,
repoSuggestions,
fileSuggestions,
symbolSuggestions,
languageSuggestions,
}: SearchSuggestionsBoxProps, ref: Ref<HTMLDivElement>) => { }: SearchSuggestionsBoxProps, ref: Ref<HTMLDivElement>) => {
const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState(0); const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState(0);
const { suggestionQuery, suggestionMode } = useMemo<{ suggestionQuery?: string, suggestionMode?: SuggestionMode }>(() => { const { suggestionQuery, suggestionMode } = useMemo<{ suggestionQuery?: string, suggestionMode?: SuggestionMode }>(() => {
// Only re-calculate the suggestion mode and query if the box is enabled.
// This is to avoid transitioning the suggestion mode and causing a fetch
// when it is not needed.
// @see: useSuggestionsData.ts
if (!isEnabled) {
return {};
}
const { queryParts, cursorIndex } = splitQuery(query, cursorPosition); const { queryParts, cursorIndex } = splitQuery(query, cursorPosition);
if (queryParts.length === 0) { if (queryParts.length === 0) {
return {}; return {};
@ -107,10 +118,10 @@ const SearchSuggestionsBox = forwardRef(({
suggestionQuery: part, suggestionQuery: part,
suggestionMode: "refine", suggestionMode: "refine",
} }
}, [cursorPosition, query]); }, [cursorPosition, isEnabled, query]);
const { suggestions, isHighlightEnabled, Icon, onSuggestionClicked } = useMemo(() => { const { suggestions, isHighlightEnabled, DefaultIcon, onSuggestionClicked } = useMemo(() => {
if (!isDefined(suggestionQuery) || !isDefined(suggestionMode)) { if (!isEnabled || !isDefined(suggestionQuery) || !isDefined(suggestionMode)) {
return {}; return {};
} }
@ -144,7 +155,7 @@ const SearchSuggestionsBox = forwardRef(({
isSpotlightEnabled = false, isSpotlightEnabled = false,
isClientSideSearchEnabled = true, isClientSideSearchEnabled = true,
onSuggestionClicked, onSuggestionClicked,
Icon, DefaultIcon,
} = ((): { } = ((): {
threshold?: number, threshold?: number,
limit?: number, limit?: number,
@ -153,7 +164,7 @@ const SearchSuggestionsBox = forwardRef(({
isSpotlightEnabled?: boolean, isSpotlightEnabled?: boolean,
isClientSideSearchEnabled?: boolean, isClientSideSearchEnabled?: boolean,
onSuggestionClicked: (value: string) => void, onSuggestionClicked: (value: string) => void,
Icon?: Icon DefaultIcon?: IconType
} => { } => {
switch (suggestionMode) { switch (suggestionMode) {
case "public": case "public":
@ -178,13 +189,13 @@ const SearchSuggestionsBox = forwardRef(({
} }
case "repo": case "repo":
return { return {
list: data.repos, list: repoSuggestions,
Icon: CommitIcon, DefaultIcon: VscRepo,
onSuggestionClicked: createOnSuggestionClickedHandler({ regexEscaped: true }), onSuggestionClicked: createOnSuggestionClickedHandler({ regexEscaped: true }),
} }
case "language": { case "language": {
return { return {
list: data.languages, list: languageSuggestions,
onSuggestionClicked: createOnSuggestionClickedHandler(), onSuggestionClicked: createOnSuggestionClickedHandler(),
isSpotlightEnabled: true, isSpotlightEnabled: true,
} }
@ -195,18 +206,25 @@ const SearchSuggestionsBox = forwardRef(({
list: refineModeSuggestions, list: refineModeSuggestions,
isHighlightEnabled: true, isHighlightEnabled: true,
isSpotlightEnabled: true, isSpotlightEnabled: true,
Icon: MixerVerticalIcon, DefaultIcon: VscFilter,
onSuggestionClicked: createOnSuggestionClickedHandler({ trailingSpace: false }), onSuggestionClicked: createOnSuggestionClickedHandler({ trailingSpace: false }),
} }
case "file": case "file":
return { return {
list: data.files, list: fileSuggestions,
onSuggestionClicked: createOnSuggestionClickedHandler(), onSuggestionClicked: createOnSuggestionClickedHandler(),
isClientSideSearchEnabled: false, isClientSideSearchEnabled: false,
DefaultIcon: VscFile,
}
case "symbol":
return {
list: symbolSuggestions,
onSuggestionClicked: createOnSuggestionClickedHandler(),
isClientSideSearchEnabled: false,
DefaultIcon: VscSymbolMisc,
} }
case "revision": case "revision":
case "content": case "content":
case "symbol":
return { return {
list: [], list: [],
onSuggestionClicked: createOnSuggestionClickedHandler(), onSuggestionClicked: createOnSuggestionClickedHandler(),
@ -252,11 +270,11 @@ const SearchSuggestionsBox = forwardRef(({
return { return {
suggestions, suggestions,
isHighlightEnabled, isHighlightEnabled,
Icon, DefaultIcon,
onSuggestionClicked, onSuggestionClicked,
} }
}, [suggestionQuery, suggestionMode, query, cursorPosition, onCompletion, data.repos, data.files, data.languages]); }, [isEnabled, suggestionQuery, suggestionMode, query, cursorPosition, onCompletion, repoSuggestions, fileSuggestions, symbolSuggestions, languageSuggestions]);
// When the list of suggestions change, reset the highlight index // When the list of suggestions change, reset the highlight index
useEffect(() => { useEffect(() => {
@ -283,7 +301,13 @@ const SearchSuggestionsBox = forwardRef(({
case "repo": case "repo":
return "Repositories"; return "Repositories";
case "refine": case "refine":
return "Refine search" return "Refine search";
case "file":
return "Files";
case "symbol":
return "Symbols";
case "language":
return "Languages";
default: default:
return ""; return "";
} }
@ -291,12 +315,15 @@ const SearchSuggestionsBox = forwardRef(({
if ( if (
!isEnabled || !isEnabled ||
!suggestions || !suggestions
suggestions.length === 0
) { ) {
return null; return null;
} }
if (suggestions.length === 0 && !isLoadingSuggestions) {
return null;
}
return ( return (
<div <div
ref={ref} ref={ref}
@ -305,6 +332,9 @@ const SearchSuggestionsBox = forwardRef(({
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.stopPropagation(); e.stopPropagation();
if (highlightedSuggestionIndex < 0 || highlightedSuggestionIndex >= suggestions.length) {
return;
}
const value = suggestions[highlightedSuggestionIndex].value; const value = suggestions[highlightedSuggestionIndex].value;
onSuggestionClicked(value); onSuggestionClicked(value);
} }
@ -334,7 +364,17 @@ const SearchSuggestionsBox = forwardRef(({
<p className="text-muted-foreground text-sm mb-1"> <p className="text-muted-foreground text-sm mb-1">
{suggestionModeText} {suggestionModeText}
</p> </p>
{suggestions.map((result, index) => ( {isLoadingSuggestions ? (
// Skeleton placeholder
<div className="animate-pulse flex flex-col gap-2 px-1 py-0.5">
{
Array.from({ length: 10 }).map((_, index) => (
<div key={index} className="h-4 bg-muted rounded-md w-full"></div>
))
}
</div>
) : suggestions.map((result, index) => (
// Suggestion list
<div <div
key={index} 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", { className={clsx("flex flex-row items-center font-mono text-sm hover:bg-muted rounded-md px-1 py-0.5 cursor-pointer", {
@ -345,23 +385,24 @@ const SearchSuggestionsBox = forwardRef(({
onSuggestionClicked(result.value) onSuggestionClicked(result.value)
}} }}
> >
{Icon && ( {result.Icon ? (
<Icon className="w-3 h-3 mr-2" /> <result.Icon className="w-3 h-3 mr-2 flex-none" />
)} ) : DefaultIcon ? (
<div className="flex flex-row items-center"> <DefaultIcon className="w-3 h-3 mr-2 flex-none" />
<span ) : null}
className={clsx('mr-2 flex-none', { <span
"text-highlight": isHighlightEnabled className={clsx('mr-2', {
})} "text-highlight": isHighlightEnabled,
> "truncate": !result.description,
{result.value} })}
>
{result.value}
</span>
{result.description && (
<span className="text-muted-foreground font-light">
{result.description}
</span> </span>
{result.description && ( )}
<span className="text-muted-foreground font-light">
{result.description}
</span>
)}
</div>
</div> </div>
))} ))}
{isFocused && ( {isFocused && (

View file

@ -4,7 +4,20 @@ import { useQuery } from "@tanstack/react-query";
import { Suggestion, SuggestionMode } from "./searchSuggestionsBox"; import { Suggestion, SuggestionMode } from "./searchSuggestionsBox";
import { getRepos, search } from "@/app/api/(client)/client"; import { getRepos, search } from "@/app/api/(client)/client";
import { useMemo } from "react"; import { useMemo } from "react";
import { Symbol } from "@/lib/types";
import languages from "./languages"; import languages from "./languages";
import {
VscSymbolClass,
VscSymbolConstant,
VscSymbolEnum,
VscSymbolField,
VscSymbolInterface,
VscSymbolMethod,
VscSymbolProperty,
VscSymbolStructure,
VscSymbolVariable
} from "react-icons/vsc";
interface Props { interface Props {
suggestionMode: SuggestionMode; suggestionMode: SuggestionMode;
@ -18,7 +31,7 @@ export const useSuggestionsData = ({
suggestionMode, suggestionMode,
suggestionQuery, suggestionQuery,
}: Props) => { }: Props) => {
const { data: repoSuggestions } = useQuery({ const { data: repoSuggestions, isLoading: _isLoadingRepos } = useQuery({
queryKey: ["repoSuggestions"], queryKey: ["repoSuggestions"],
queryFn: getRepos, queryFn: getRepos,
select: (data): Suggestion[] => { select: (data): Suggestion[] => {
@ -30,8 +43,9 @@ export const useSuggestionsData = ({
}, },
enabled: suggestionMode === "repo", enabled: suggestionMode === "repo",
}); });
const isLoadingRepos = useMemo(() => suggestionMode === "repo" && _isLoadingRepos, [_isLoadingRepos, suggestionMode]);
const { data: fileSuggestions } = useQuery({ const { data: fileSuggestions, isLoading: _isLoadingFiles } = useQuery({
queryKey: ["fileSuggestions", suggestionQuery], queryKey: ["fileSuggestions", suggestionQuery],
queryFn: () => search({ queryFn: () => search({
query: `file:${suggestionQuery}`, query: `file:${suggestionQuery}`,
@ -44,6 +58,32 @@ export const useSuggestionsData = ({
}, },
enabled: suggestionMode === "file" enabled: suggestionMode === "file"
}); });
const isLoadingFiles = useMemo(() => suggestionMode === "file" && _isLoadingFiles, [_isLoadingFiles, suggestionMode]);
const { data: symbolSuggestions, isLoading: _isLoadingSymbols } = useQuery({
queryKey: ["symbolSuggestions", suggestionQuery],
queryFn: () => search({
query: `sym:${suggestionQuery.length > 0 ? suggestionQuery : ".*"}`,
maxMatchDisplayCount: 15,
}),
select: (data): Suggestion[] => {
const symbols = data.Result.Files?.flatMap((file) => file.ChunkMatches).flatMap((chunk) => chunk.SymbolInfo ?? []);
if (!symbols) {
return [];
}
// De-duplicate on symbol name & kind.
const symbolMap = new Map<string, Symbol>(symbols.map((symbol: Symbol) => [`${symbol.Kind}.${symbol.Sym}`, symbol]));
const suggestions = Array.from(symbolMap.values()).map((symbol) => ({
value: symbol.Sym,
Icon: getSymbolIcon(symbol),
} satisfies Suggestion));
return suggestions;
},
enabled: suggestionMode === "symbol",
});
const isLoadingSymbols = useMemo(() => suggestionMode === "symbol" && _isLoadingSymbols, [suggestionMode, _isLoadingSymbols]);
const languageSuggestions = useMemo((): Suggestion[] => { const languageSuggestions = useMemo((): Suggestion[] => {
return languages.map((lang) => { return languages.map((lang) => {
@ -63,13 +103,45 @@ export const useSuggestionsData = ({
}); });
}, []); }, []);
const data = useMemo(() => { const isLoadingSuggestions = useMemo(() => {
return { return isLoadingSymbols || isLoadingFiles || isLoadingRepos;
repos: repoSuggestions ?? [], }, [isLoadingFiles, isLoadingRepos, isLoadingSymbols]);
languages: languageSuggestions,
files: fileSuggestions ?? [],
}
}, [repoSuggestions, fileSuggestions, languageSuggestions]);
return data; return {
repoSuggestions: repoSuggestions ?? [],
fileSuggestions: fileSuggestions ?? [],
symbolSuggestions: symbolSuggestions ?? [],
languageSuggestions,
isLoadingSuggestions,
}
}
const getSymbolIcon = (symbol: Symbol) => {
switch (symbol.Kind) {
case "methodSpec":
case "method":
case "function":
case "func":
return VscSymbolMethod;
case "variable":
return VscSymbolVariable;
case "class":
return VscSymbolClass;
case "const":
case "macro":
case "constant":
return VscSymbolConstant;
case "property":
return VscSymbolProperty;
case "struct":
return VscSymbolStructure;
case "field":
case "member":
return VscSymbolField;
case "interface":
return VscSymbolInterface;
case "enum":
case "enumerator":
return VscSymbolEnum;
}
} }

View file

@ -46,6 +46,13 @@ export const searchResponseStats = {
FlushReason: z.number(), FlushReason: z.number(),
} }
export const symbolSchema = z.object({
Sym: z.string(),
Kind: z.string(),
Parent: z.string(),
ParentKind: z.string(),
});
// @see : https://github.com/sourcebot-dev/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L497 // @see : https://github.com/sourcebot-dev/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L497
export const zoektSearchResponseSchema = z.object({ export const zoektSearchResponseSchema = z.object({
Result: z.object({ Result: z.object({
@ -62,6 +69,7 @@ export const zoektSearchResponseSchema = z.object({
FileName: z.boolean(), FileName: z.boolean(),
ContentStart: locationSchema, ContentStart: locationSchema,
Score: z.number(), Score: z.number(),
SymbolInfo: z.array(symbolSchema).nullable(),
})), })),
Checksum: z.string(), Checksum: z.string(),
Score: z.number(), Score: z.number(),

View file

@ -1,5 +1,5 @@
import { z } from "zod"; import { z } from "zod";
import { fileSourceRequestSchema, fileSourceResponseSchema, listRepositoriesResponseSchema, locationSchema, rangeSchema, repositorySchema, searchRequestSchema, searchResponseSchema } from "./schemas"; import { fileSourceRequestSchema, fileSourceResponseSchema, listRepositoriesResponseSchema, locationSchema, rangeSchema, repositorySchema, searchRequestSchema, searchResponseSchema, symbolSchema } from "./schemas";
export type KeymapType = "default" | "vim"; export type KeymapType = "default" | "vim";
@ -18,6 +18,8 @@ export type FileSourceResponse = z.infer<typeof fileSourceResponseSchema>;
export type ListRepositoriesResponse = z.infer<typeof listRepositoriesResponseSchema>; export type ListRepositoriesResponse = z.infer<typeof listRepositoriesResponseSchema>;
export type Repository = z.infer<typeof repositorySchema>; export type Repository = z.infer<typeof repositorySchema>;
export type Symbol = z.infer<typeof symbolSchema>;
export enum SearchQueryParams { export enum SearchQueryParams {
query = "query", query = "query",
maxMatchDisplayCount = "maxMatchDisplayCount", maxMatchDisplayCount = "maxMatchDisplayCount",

View file

@ -4604,6 +4604,11 @@ react-hotkeys-hook@^4.5.1:
resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-4.5.1.tgz#990260ecc7e5a431414148a93b02a2f1a9707897" resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-4.5.1.tgz#990260ecc7e5a431414148a93b02a2f1a9707897"
integrity sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg== integrity sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg==
react-icons@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.3.0.tgz#ccad07a30aebd40a89f8cfa7d82e466019203f1c"
integrity sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==
react-is@^16.13.1: react-is@^16.13.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -5075,16 +5080,7 @@ string-argv@^0.3.1:
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6"
integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==
"string-width-cjs@npm:string-width@^4.2.0": "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -5181,14 +5177,7 @@ string_decoder@^1.1.1, string_decoder@^1.3.0:
dependencies: dependencies:
safe-buffer "~5.2.0" safe-buffer "~5.2.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1": "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==