mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
Symbol suggestions (#98)
This commit is contained in:
parent
b115218be9
commit
120d84a046
9 changed files with 201 additions and 75 deletions
|
|
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added symbol suggestions as suggestion type. ([#98](https://github.com/sourcebot-dev/sourcebot/pull/98))
|
||||
|
||||
## [2.5.2] - 2024-11-27
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -392,3 +392,9 @@ Or if you are [building locally](#build-from-source), create a `.env.local` file
|
|||
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).
|
||||
|
|
@ -59,6 +59,7 @@
|
|||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-hotkeys-hook": "^4.5.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-resizable-panels": "^2.1.1",
|
||||
"server-only": "^0.0.1",
|
||||
"sharp": "^0.33.5",
|
||||
|
|
|
|||
|
|
@ -255,13 +255,16 @@ export const SearchBar = ({
|
|||
setIsSuggestionsBoxFocused(document.activeElement === suggestionBoxRef.current);
|
||||
}}
|
||||
cursorPosition={cursorPosition}
|
||||
data={suggestionData}
|
||||
onSuggestionModeChanged={(suggestionMode) => {
|
||||
setSuggestionMode(suggestionMode);
|
||||
onSuggestionModeChanged={(newSuggestionMode) => {
|
||||
if (suggestionMode !== newSuggestionMode) {
|
||||
console.debug(`Suggestion mode changed: ${suggestionMode} -> ${newSuggestionMode}`);
|
||||
}
|
||||
setSuggestionMode(newSuggestionMode);
|
||||
}}
|
||||
onSuggestionQueryChanged={(suggestionQuery) => {
|
||||
setSuggestionQuery(suggestionQuery);
|
||||
}}
|
||||
{...suggestionData}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
'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";
|
||||
|
|
@ -16,13 +14,14 @@ import {
|
|||
refineModeSuggestions,
|
||||
suggestionModeMappings
|
||||
} from "./constants";
|
||||
|
||||
type Icon = React.ForwardRefExoticComponent<IconProps & React.RefAttributes<SVGSVGElement>>;
|
||||
import { IconType } from "react-icons/lib";
|
||||
import { VscFile, VscFilter, VscRepo, VscSymbolMisc } from "react-icons/vsc";
|
||||
|
||||
export type Suggestion = {
|
||||
value: string;
|
||||
description?: string;
|
||||
spotlight?: boolean;
|
||||
Icon?: IconType;
|
||||
}
|
||||
|
||||
export type SuggestionMode =
|
||||
|
|
@ -50,18 +49,17 @@ interface SearchSuggestionsBoxProps {
|
|||
onSuggestionModeChanged: (suggestionMode: SuggestionMode) => void;
|
||||
onSuggestionQueryChanged: (suggestionQuery: string) => void;
|
||||
|
||||
data: {
|
||||
repos: Suggestion[];
|
||||
languages: Suggestion[];
|
||||
files: Suggestion[];
|
||||
}
|
||||
isLoadingSuggestions: boolean;
|
||||
repoSuggestions: Suggestion[];
|
||||
fileSuggestions: Suggestion[];
|
||||
symbolSuggestions: Suggestion[];
|
||||
languageSuggestions: Suggestion[];
|
||||
}
|
||||
|
||||
const SearchSuggestionsBox = forwardRef(({
|
||||
query,
|
||||
onCompletion,
|
||||
isEnabled,
|
||||
data,
|
||||
cursorPosition,
|
||||
isFocused,
|
||||
onFocus,
|
||||
|
|
@ -69,11 +67,24 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
onReturnFocus,
|
||||
onSuggestionModeChanged,
|
||||
onSuggestionQueryChanged,
|
||||
isLoadingSuggestions,
|
||||
repoSuggestions,
|
||||
fileSuggestions,
|
||||
symbolSuggestions,
|
||||
languageSuggestions,
|
||||
}: SearchSuggestionsBoxProps, ref: Ref<HTMLDivElement>) => {
|
||||
|
||||
const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState(0);
|
||||
|
||||
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);
|
||||
if (queryParts.length === 0) {
|
||||
return {};
|
||||
|
|
@ -107,10 +118,10 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
suggestionQuery: part,
|
||||
suggestionMode: "refine",
|
||||
}
|
||||
}, [cursorPosition, query]);
|
||||
}, [cursorPosition, isEnabled, query]);
|
||||
|
||||
const { suggestions, isHighlightEnabled, Icon, onSuggestionClicked } = useMemo(() => {
|
||||
if (!isDefined(suggestionQuery) || !isDefined(suggestionMode)) {
|
||||
const { suggestions, isHighlightEnabled, DefaultIcon, onSuggestionClicked } = useMemo(() => {
|
||||
if (!isEnabled || !isDefined(suggestionQuery) || !isDefined(suggestionMode)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +155,7 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
isSpotlightEnabled = false,
|
||||
isClientSideSearchEnabled = true,
|
||||
onSuggestionClicked,
|
||||
Icon,
|
||||
DefaultIcon,
|
||||
} = ((): {
|
||||
threshold?: number,
|
||||
limit?: number,
|
||||
|
|
@ -153,7 +164,7 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
isSpotlightEnabled?: boolean,
|
||||
isClientSideSearchEnabled?: boolean,
|
||||
onSuggestionClicked: (value: string) => void,
|
||||
Icon?: Icon
|
||||
DefaultIcon?: IconType
|
||||
} => {
|
||||
switch (suggestionMode) {
|
||||
case "public":
|
||||
|
|
@ -178,13 +189,13 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
}
|
||||
case "repo":
|
||||
return {
|
||||
list: data.repos,
|
||||
Icon: CommitIcon,
|
||||
list: repoSuggestions,
|
||||
DefaultIcon: VscRepo,
|
||||
onSuggestionClicked: createOnSuggestionClickedHandler({ regexEscaped: true }),
|
||||
}
|
||||
case "language": {
|
||||
return {
|
||||
list: data.languages,
|
||||
list: languageSuggestions,
|
||||
onSuggestionClicked: createOnSuggestionClickedHandler(),
|
||||
isSpotlightEnabled: true,
|
||||
}
|
||||
|
|
@ -195,18 +206,25 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
list: refineModeSuggestions,
|
||||
isHighlightEnabled: true,
|
||||
isSpotlightEnabled: true,
|
||||
Icon: MixerVerticalIcon,
|
||||
DefaultIcon: VscFilter,
|
||||
onSuggestionClicked: createOnSuggestionClickedHandler({ trailingSpace: false }),
|
||||
}
|
||||
case "file":
|
||||
return {
|
||||
list: data.files,
|
||||
list: fileSuggestions,
|
||||
onSuggestionClicked: createOnSuggestionClickedHandler(),
|
||||
isClientSideSearchEnabled: false,
|
||||
DefaultIcon: VscFile,
|
||||
}
|
||||
case "symbol":
|
||||
return {
|
||||
list: symbolSuggestions,
|
||||
onSuggestionClicked: createOnSuggestionClickedHandler(),
|
||||
isClientSideSearchEnabled: false,
|
||||
DefaultIcon: VscSymbolMisc,
|
||||
}
|
||||
case "revision":
|
||||
case "content":
|
||||
case "symbol":
|
||||
return {
|
||||
list: [],
|
||||
onSuggestionClicked: createOnSuggestionClickedHandler(),
|
||||
|
|
@ -252,11 +270,11 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
return {
|
||||
suggestions,
|
||||
isHighlightEnabled,
|
||||
Icon,
|
||||
DefaultIcon,
|
||||
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
|
||||
useEffect(() => {
|
||||
|
|
@ -283,7 +301,13 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
case "repo":
|
||||
return "Repositories";
|
||||
case "refine":
|
||||
return "Refine search"
|
||||
return "Refine search";
|
||||
case "file":
|
||||
return "Files";
|
||||
case "symbol":
|
||||
return "Symbols";
|
||||
case "language":
|
||||
return "Languages";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
|
@ -291,12 +315,15 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
|
||||
if (
|
||||
!isEnabled ||
|
||||
!suggestions ||
|
||||
suggestions.length === 0
|
||||
!suggestions
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (suggestions.length === 0 && !isLoadingSuggestions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
|
|
@ -305,6 +332,9 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.stopPropagation();
|
||||
if (highlightedSuggestionIndex < 0 || highlightedSuggestionIndex >= suggestions.length) {
|
||||
return;
|
||||
}
|
||||
const value = suggestions[highlightedSuggestionIndex].value;
|
||||
onSuggestionClicked(value);
|
||||
}
|
||||
|
|
@ -334,7 +364,17 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
<p className="text-muted-foreground text-sm mb-1">
|
||||
{suggestionModeText}
|
||||
</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
|
||||
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", {
|
||||
|
|
@ -345,13 +385,15 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
onSuggestionClicked(result.value)
|
||||
}}
|
||||
>
|
||||
{Icon && (
|
||||
<Icon className="w-3 h-3 mr-2" />
|
||||
)}
|
||||
<div className="flex flex-row items-center">
|
||||
{result.Icon ? (
|
||||
<result.Icon className="w-3 h-3 mr-2 flex-none" />
|
||||
) : DefaultIcon ? (
|
||||
<DefaultIcon className="w-3 h-3 mr-2 flex-none" />
|
||||
) : null}
|
||||
<span
|
||||
className={clsx('mr-2 flex-none', {
|
||||
"text-highlight": isHighlightEnabled
|
||||
className={clsx('mr-2', {
|
||||
"text-highlight": isHighlightEnabled,
|
||||
"truncate": !result.description,
|
||||
})}
|
||||
>
|
||||
{result.value}
|
||||
|
|
@ -362,7 +404,6 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isFocused && (
|
||||
<div className="flex flex-row items-center justify-end mt-1">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,20 @@ import { useQuery } from "@tanstack/react-query";
|
|||
import { Suggestion, SuggestionMode } from "./searchSuggestionsBox";
|
||||
import { getRepos, search } from "@/app/api/(client)/client";
|
||||
import { useMemo } from "react";
|
||||
import { Symbol } from "@/lib/types";
|
||||
import languages from "./languages";
|
||||
import {
|
||||
VscSymbolClass,
|
||||
VscSymbolConstant,
|
||||
VscSymbolEnum,
|
||||
VscSymbolField,
|
||||
VscSymbolInterface,
|
||||
VscSymbolMethod,
|
||||
VscSymbolProperty,
|
||||
VscSymbolStructure,
|
||||
VscSymbolVariable
|
||||
} from "react-icons/vsc";
|
||||
|
||||
|
||||
interface Props {
|
||||
suggestionMode: SuggestionMode;
|
||||
|
|
@ -18,7 +31,7 @@ export const useSuggestionsData = ({
|
|||
suggestionMode,
|
||||
suggestionQuery,
|
||||
}: Props) => {
|
||||
const { data: repoSuggestions } = useQuery({
|
||||
const { data: repoSuggestions, isLoading: _isLoadingRepos } = useQuery({
|
||||
queryKey: ["repoSuggestions"],
|
||||
queryFn: getRepos,
|
||||
select: (data): Suggestion[] => {
|
||||
|
|
@ -30,8 +43,9 @@ export const useSuggestionsData = ({
|
|||
},
|
||||
enabled: suggestionMode === "repo",
|
||||
});
|
||||
const isLoadingRepos = useMemo(() => suggestionMode === "repo" && _isLoadingRepos, [_isLoadingRepos, suggestionMode]);
|
||||
|
||||
const { data: fileSuggestions } = useQuery({
|
||||
const { data: fileSuggestions, isLoading: _isLoadingFiles } = useQuery({
|
||||
queryKey: ["fileSuggestions", suggestionQuery],
|
||||
queryFn: () => search({
|
||||
query: `file:${suggestionQuery}`,
|
||||
|
|
@ -44,6 +58,32 @@ export const useSuggestionsData = ({
|
|||
},
|
||||
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[] => {
|
||||
return languages.map((lang) => {
|
||||
|
|
@ -63,13 +103,45 @@ export const useSuggestionsData = ({
|
|||
});
|
||||
}, []);
|
||||
|
||||
const data = useMemo(() => {
|
||||
return {
|
||||
repos: repoSuggestions ?? [],
|
||||
languages: languageSuggestions,
|
||||
files: fileSuggestions ?? [],
|
||||
}
|
||||
}, [repoSuggestions, fileSuggestions, languageSuggestions]);
|
||||
const isLoadingSuggestions = useMemo(() => {
|
||||
return isLoadingSymbols || isLoadingFiles || isLoadingRepos;
|
||||
}, [isLoadingFiles, isLoadingRepos, isLoadingSymbols]);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,13 @@ export const searchResponseStats = {
|
|||
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
|
||||
export const zoektSearchResponseSchema = z.object({
|
||||
Result: z.object({
|
||||
|
|
@ -62,6 +69,7 @@ export const zoektSearchResponseSchema = z.object({
|
|||
FileName: z.boolean(),
|
||||
ContentStart: locationSchema,
|
||||
Score: z.number(),
|
||||
SymbolInfo: z.array(symbolSchema).nullable(),
|
||||
})),
|
||||
Checksum: z.string(),
|
||||
Score: z.number(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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";
|
||||
|
||||
|
|
@ -18,6 +18,8 @@ export type FileSourceResponse = z.infer<typeof fileSourceResponseSchema>;
|
|||
export type ListRepositoriesResponse = z.infer<typeof listRepositoriesResponseSchema>;
|
||||
export type Repository = z.infer<typeof repositorySchema>;
|
||||
|
||||
export type Symbol = z.infer<typeof symbolSchema>;
|
||||
|
||||
export enum SearchQueryParams {
|
||||
query = "query",
|
||||
maxMatchDisplayCount = "maxMatchDisplayCount",
|
||||
|
|
|
|||
25
yarn.lock
25
yarn.lock
|
|
@ -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"
|
||||
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:
|
||||
version "16.13.1"
|
||||
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"
|
||||
integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.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:
|
||||
"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==
|
||||
|
|
@ -5181,14 +5177,7 @@ string_decoder@^1.1.1, string_decoder@^1.3.0:
|
|||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
"strip-ansi-cjs@npm: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:
|
||||
"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==
|
||||
|
|
|
|||
Loading…
Reference in a new issue