mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-14 16:10:59 -07:00
Add initial /extensions explore implementation
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
import {
|
||||
type CommandContext,
|
||||
type SlashCommand,
|
||||
type SlashCommandActionReturn,
|
||||
CommandKind,
|
||||
} from './types.js';
|
||||
import open from 'open';
|
||||
@@ -35,6 +36,7 @@ import { stat } from 'node:fs/promises';
|
||||
import { ExtensionSettingScope } from '../../config/extensions/extensionSettings.js';
|
||||
import { type ConfigLogger } from '../../commands/extensions/utils.js';
|
||||
import { ConfigExtensionDialog } from '../components/ConfigExtensionDialog.js';
|
||||
import { ExtensionRegistryView } from '../components/views/ExtensionRegistryView.js';
|
||||
import React from 'react';
|
||||
|
||||
function showMessageIfNoExtensions(
|
||||
@@ -265,7 +267,28 @@ async function restartAction(
|
||||
}
|
||||
}
|
||||
|
||||
async function exploreAction(context: CommandContext) {
|
||||
async function exploreAction(
|
||||
context: CommandContext,
|
||||
): Promise<SlashCommandActionReturn | void> {
|
||||
const settings = context.services.settings.merged;
|
||||
const useRegistryUI = settings.experimental?.extensionRegistry;
|
||||
|
||||
if (useRegistryUI) {
|
||||
const extensionManager = context.services.config?.getExtensionLoader();
|
||||
if (extensionManager instanceof ExtensionManager) {
|
||||
return {
|
||||
type: 'custom_dialog' as const,
|
||||
component: React.createElement(ExtensionRegistryView, {
|
||||
onSelect: (extension) => {
|
||||
debugLogger.debug(`Selected extension: ${extension.extensionName}`);
|
||||
},
|
||||
onClose: () => context.ui.removeComponent(),
|
||||
extensionManager,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const extensionsUrl = 'https://geminicli.com/extensions/';
|
||||
|
||||
// Only check for NODE_ENV for explicit test mode, not for unit test framework
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { TextInput } from './TextInput.js';
|
||||
@@ -31,6 +30,25 @@ export interface SearchableListProps<T extends GenericListItem> {
|
||||
searchPlaceholder?: string;
|
||||
/** Max items to show at once */
|
||||
maxItemsToShow?: number;
|
||||
/** Custom item renderer */
|
||||
renderItem?: (
|
||||
item: T,
|
||||
isActive: boolean,
|
||||
labelWidth: number,
|
||||
) => React.JSX.Element;
|
||||
/** Optional custom header element */
|
||||
header?: React.ReactNode;
|
||||
/** Optional custom footer element, can be a function to receive pagination info */
|
||||
footer?:
|
||||
| React.ReactNode
|
||||
| ((pagination: SearchableListPaginationInfo) => React.ReactNode);
|
||||
}
|
||||
|
||||
export interface SearchableListPaginationInfo {
|
||||
startIndex: number; // 0-indexed
|
||||
endIndex: number; // 0-indexed, exclusive
|
||||
totalVisible: number;
|
||||
totalItems: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,6 +62,9 @@ export function SearchableList<T extends GenericListItem>({
|
||||
initialSearchQuery = '',
|
||||
searchPlaceholder = 'Search...',
|
||||
maxItemsToShow = 10,
|
||||
renderItem,
|
||||
header,
|
||||
footer,
|
||||
}: SearchableListProps<T>): React.JSX.Element {
|
||||
const { filteredItems, searchBuffer, maxLabelWidth } = useFuzzyList({
|
||||
items,
|
||||
@@ -113,16 +134,25 @@ export function SearchableList<T extends GenericListItem>({
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
flexDirection="column"
|
||||
padding={1}
|
||||
width="100%"
|
||||
height="100%"
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
>
|
||||
{/* Header */}
|
||||
{/* Title */}
|
||||
{title && (
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>{title}</Text>
|
||||
<Box marginX={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{'>'} {title}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{header && (
|
||||
<Box marginX={1} marginTop={1}>
|
||||
{header}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -132,7 +162,9 @@ export function SearchableList<T extends GenericListItem>({
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.focused}
|
||||
paddingX={1}
|
||||
marginBottom={1}
|
||||
height={3}
|
||||
marginTop={1}
|
||||
width="100%"
|
||||
>
|
||||
<TextInput
|
||||
buffer={searchBuffer}
|
||||
@@ -143,45 +175,85 @@ export function SearchableList<T extends GenericListItem>({
|
||||
)}
|
||||
|
||||
{/* List */}
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
{showScrollUp && (
|
||||
<Box marginLeft={1}>
|
||||
<Text color={theme.text.secondary}>▲</Text>
|
||||
</Box>
|
||||
)}
|
||||
{visibleItems.length === 0 ? (
|
||||
<Text color={theme.text.secondary}>No items found.</Text>
|
||||
<Box marginLeft={2}>
|
||||
<Text color={theme.text.secondary}>No items found.</Text>
|
||||
</Box>
|
||||
) : (
|
||||
visibleItems.map((item, idx) => {
|
||||
const index = scrollOffset + idx;
|
||||
const isActive = index === activeIndex;
|
||||
|
||||
if (renderItem) {
|
||||
return (
|
||||
<React.Fragment key={item.key}>
|
||||
<Box>{renderItem(item, isActive, maxLabelWidth)}</Box>
|
||||
<Box height={1} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={item.key} flexDirection="row">
|
||||
<Text
|
||||
color={isActive ? theme.status.success : theme.text.secondary}
|
||||
>
|
||||
{isActive ? '> ' : ' '}
|
||||
</Text>
|
||||
<Box width={maxLabelWidth + 2}>
|
||||
<Text
|
||||
color={isActive ? theme.status.success : theme.text.primary}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
<React.Fragment key={item.key}>
|
||||
<Box flexDirection="row" alignItems="flex-start">
|
||||
<Box minWidth={2} flexShrink={0}>
|
||||
<Text
|
||||
color={
|
||||
isActive ? theme.status.success : theme.text.secondary
|
||||
}
|
||||
>
|
||||
{isActive ? '> ' : ' '}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={maxLabelWidth + 2}>
|
||||
<Text
|
||||
bold={isActive}
|
||||
color={
|
||||
isActive ? theme.status.success : theme.text.primary
|
||||
}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</Box>
|
||||
{item.description && (
|
||||
<Text color={theme.text.secondary} wrap="truncate-end">
|
||||
{' '}
|
||||
| {item.description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{item.description && (
|
||||
<Text color={theme.text.secondary}>{item.description}</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box height={1} />
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
)}
|
||||
{showScrollDown && (
|
||||
<Box marginLeft={1}>
|
||||
<Text color={theme.text.secondary}>▼</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Footer/Scroll Indicators */}
|
||||
{(showScrollUp || showScrollDown) && (
|
||||
<Box marginTop={1} justifyContent="center">
|
||||
<Text color={theme.text.secondary}>
|
||||
{showScrollUp ? '▲ ' : ' '}
|
||||
{filteredItems.length} items
|
||||
{showScrollDown ? ' ▼' : ' '}
|
||||
</Text>
|
||||
{/* Footer */}
|
||||
{footer && (
|
||||
<Box marginX={1} marginTop={1}>
|
||||
{typeof footer === 'function'
|
||||
? footer({
|
||||
startIndex: scrollOffset,
|
||||
endIndex: Math.min(
|
||||
scrollOffset + maxItemsToShow,
|
||||
filteredItems.length,
|
||||
),
|
||||
totalVisible: filteredItems.length,
|
||||
totalItems: items.length,
|
||||
})
|
||||
: footer}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderWithProviders as render } from '../../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { ExtensionRegistryView } from './ExtensionRegistryView.js';
|
||||
import {
|
||||
ExtensionRegistryClient,
|
||||
type RegistryExtension,
|
||||
} from '../../../config/extensionRegistryClient.js';
|
||||
import { type ExtensionManager } from '../../../config/extension-manager.js';
|
||||
|
||||
vi.mock('../../config/extensionRegistryClient.js');
|
||||
|
||||
const mockExtensions = [
|
||||
{
|
||||
id: 'ext-1',
|
||||
extensionName: 'Extension 1',
|
||||
extensionDescription: 'Description 1',
|
||||
repoDescription: 'Repo Description 1',
|
||||
},
|
||||
{
|
||||
id: 'ext-2',
|
||||
extensionName: 'Extension 2',
|
||||
extensionDescription: 'Description 2',
|
||||
repoDescription: 'Repo Description 2',
|
||||
},
|
||||
];
|
||||
|
||||
describe('ExtensionRegistryView', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render loading state initially', async () => {
|
||||
const mockExtensionManager = {
|
||||
getExtensions: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
|
||||
const { lastFrame } = render(
|
||||
<ExtensionRegistryView
|
||||
extensionManager={mockExtensionManager as unknown as ExtensionManager}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toContain('Loading extensions...');
|
||||
});
|
||||
|
||||
it('should render extensions after fetching', async () => {
|
||||
vi.spyOn(
|
||||
ExtensionRegistryClient.prototype,
|
||||
'getExtensions',
|
||||
).mockResolvedValue({
|
||||
extensions: mockExtensions as unknown as RegistryExtension[],
|
||||
total: 2,
|
||||
});
|
||||
|
||||
const mockExtensionManager = {
|
||||
getExtensions: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
|
||||
const { lastFrame } = render(
|
||||
<ExtensionRegistryView
|
||||
extensionManager={mockExtensionManager as unknown as ExtensionManager}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const frame = lastFrame();
|
||||
expect(frame).toContain('Extension 1');
|
||||
expect(frame).toContain('Description 1');
|
||||
expect(frame).toContain('Extension 2');
|
||||
expect(frame).toContain('Description 2');
|
||||
});
|
||||
|
||||
it('should render error message on fetch failure', async () => {
|
||||
vi.spyOn(
|
||||
ExtensionRegistryClient.prototype,
|
||||
'getExtensions',
|
||||
).mockRejectedValue(new Error('Fetch failed'));
|
||||
|
||||
const mockExtensionManager = {
|
||||
getExtensions: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
|
||||
const { lastFrame } = render(
|
||||
<ExtensionRegistryView
|
||||
extensionManager={mockExtensionManager as unknown as ExtensionManager}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const frame = lastFrame();
|
||||
expect(frame).toContain('Error loading extensions:');
|
||||
expect(frame).toContain('Fetch failed');
|
||||
});
|
||||
|
||||
it('should call onSelect when an item is selected', async () => {
|
||||
vi.spyOn(
|
||||
ExtensionRegistryClient.prototype,
|
||||
'getExtensions',
|
||||
).mockResolvedValue({
|
||||
extensions: mockExtensions as unknown as RegistryExtension[],
|
||||
total: 2,
|
||||
});
|
||||
const onSelect = vi.fn();
|
||||
|
||||
const mockExtensionManager = {
|
||||
getExtensions: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
|
||||
const { stdin } = render(
|
||||
<ExtensionRegistryView
|
||||
onSelect={onSelect}
|
||||
extensionManager={mockExtensionManager as unknown as ExtensionManager}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
// Press Enter to select the first item
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(mockExtensions[0]);
|
||||
});
|
||||
});
|
||||
209
packages/cli/src/ui/components/views/ExtensionRegistryView.tsx
Normal file
209
packages/cli/src/ui/components/views/ExtensionRegistryView.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import {
|
||||
ExtensionRegistryClient,
|
||||
type RegistryExtension,
|
||||
} from '../../../config/extensionRegistryClient.js';
|
||||
import { SearchableList } from '../shared/SearchableList.js';
|
||||
import type { GenericListItem } from '../../hooks/useFuzzyList.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
|
||||
import { ExtensionUpdateState } from '../../state/extensions.js';
|
||||
import { useExtensionUpdates } from '../../hooks/useExtensionUpdates.js';
|
||||
import { useConfig } from '../../contexts/ConfigContext.js';
|
||||
import type { ExtensionManager } from '../../../config/extension-manager.js';
|
||||
|
||||
interface ExtensionRegistryViewProps {
|
||||
onSelect?: (extension: RegistryExtension) => void;
|
||||
onClose?: () => void;
|
||||
extensionManager: ExtensionManager;
|
||||
}
|
||||
|
||||
interface ExtensionItem extends GenericListItem {
|
||||
extension: RegistryExtension;
|
||||
}
|
||||
|
||||
export function ExtensionRegistryView({
|
||||
onSelect,
|
||||
onClose,
|
||||
extensionManager,
|
||||
}: ExtensionRegistryViewProps): React.JSX.Element {
|
||||
const [extensions, setExtensions] = useState<RegistryExtension[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const config = useConfig();
|
||||
|
||||
const { extensionsUpdateState } = useExtensionUpdates(
|
||||
extensionManager,
|
||||
() => 0,
|
||||
config.getEnableExtensionReloading(),
|
||||
);
|
||||
|
||||
const installedExtensions = extensionManager.getExtensions();
|
||||
|
||||
const client = useMemo(() => new ExtensionRegistryClient(), []);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
const fetchExtensions = async () => {
|
||||
try {
|
||||
const result = await client.getExtensions(1, 1000); // Fetch a large enough batch
|
||||
if (active) {
|
||||
setExtensions(result.extensions);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
if (active) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void fetchExtensions();
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [client]);
|
||||
|
||||
const items: ExtensionItem[] = useMemo(
|
||||
() =>
|
||||
extensions.map((ext) => ({
|
||||
key: ext.id,
|
||||
label: ext.extensionName,
|
||||
description: ext.extensionDescription || ext.repoDescription,
|
||||
extension: ext,
|
||||
})),
|
||||
[extensions],
|
||||
);
|
||||
|
||||
const handleSelect = (item: ExtensionItem) => {
|
||||
onSelect?.(item.extension);
|
||||
};
|
||||
|
||||
const renderItem = (
|
||||
item: ExtensionItem,
|
||||
isActive: boolean,
|
||||
_labelWidth: number,
|
||||
) => {
|
||||
const isInstalled = installedExtensions.some(
|
||||
(e) => e.name === item.extension.extensionName,
|
||||
);
|
||||
const updateState = extensionsUpdateState.get(item.extension.extensionName);
|
||||
const hasUpdate = updateState === ExtensionUpdateState.UPDATE_AVAILABLE;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" width="100%" justifyContent="space-between">
|
||||
<Box flexDirection="row" flexShrink={1} minWidth={0}>
|
||||
<Box width={2} flexShrink={0}>
|
||||
<Text
|
||||
color={isActive ? theme.status.success : theme.text.secondary}
|
||||
>
|
||||
{isActive ? '> ' : ' '}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexShrink={0}>
|
||||
<Text
|
||||
bold={isActive}
|
||||
color={isActive ? theme.status.success : theme.text.primary}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexShrink={0} marginX={1}>
|
||||
<Text color={theme.text.secondary}>|</Text>
|
||||
</Box>
|
||||
{isInstalled && (
|
||||
<Box marginRight={1} flexShrink={0}>
|
||||
<Text color={theme.status.success}>[Installed]</Text>
|
||||
</Box>
|
||||
)}
|
||||
{hasUpdate && (
|
||||
<Box marginRight={1} flexShrink={0}>
|
||||
<Text color={theme.status.warning}>[Update available]</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box flexShrink={1} minWidth={0}>
|
||||
<Text color={theme.text.secondary} wrap="truncate-end">
|
||||
{item.description}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flexShrink={0} marginLeft={2} width={8} flexDirection="row">
|
||||
<Text color={theme.status.warning}>⭐</Text>
|
||||
<Text color={isActive ? theme.status.success : theme.text.secondary}>
|
||||
{' '}
|
||||
{item.extension.stars || 0}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const header = (
|
||||
<Box flexDirection="row" justifyContent="space-between" width="100%">
|
||||
<Box flexShrink={1}>
|
||||
<Text color={theme.text.secondary} wrap="truncate">
|
||||
Browse and search extensions from the registry.
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexShrink={0} marginLeft={2}>
|
||||
<Text color={theme.text.secondary}>
|
||||
Reg: {extensions.length} | Inst: {installedExtensions.length}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const footer = ({
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalVisible,
|
||||
}: {
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
totalVisible: number;
|
||||
}) => (
|
||||
<Text color={theme.text.secondary}>
|
||||
({startIndex + 1}-{endIndex}) / {totalVisible}
|
||||
</Text>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box padding={1}>
|
||||
<Text color={theme.text.secondary}>Loading extensions...</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Box padding={1} flexDirection="column">
|
||||
<Text color={theme.status.error}>Error loading extensions:</Text>
|
||||
<Text color={theme.text.secondary}>{error}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchableList<ExtensionItem>
|
||||
title="Extensions"
|
||||
items={items}
|
||||
onSelect={handleSelect}
|
||||
onClose={onClose || (() => {})}
|
||||
searchPlaceholder="Search extension gallery"
|
||||
renderItem={renderItem}
|
||||
header={header}
|
||||
footer={footer}
|
||||
maxItemsToShow={8}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -13,14 +13,6 @@ import {
|
||||
} from '../components/shared/text-buffer.js';
|
||||
import { getCachedStringWidth } from '../utils/textUtils.js';
|
||||
|
||||
interface FzfResult {
|
||||
item: string;
|
||||
start: number;
|
||||
end: number;
|
||||
score: number;
|
||||
positions?: number[];
|
||||
}
|
||||
|
||||
export interface GenericListItem {
|
||||
key: string;
|
||||
label: string;
|
||||
@@ -54,21 +46,11 @@ export function useFuzzyList<T extends GenericListItem>({
|
||||
);
|
||||
|
||||
// FZF instance for fuzzy searching
|
||||
const { fzfInstance, searchMap } = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
const searchItems: string[] = [];
|
||||
|
||||
items.forEach((item) => {
|
||||
searchItems.push(item.label);
|
||||
map.set(item.label.toLowerCase(), item.key);
|
||||
});
|
||||
|
||||
const fzf = new AsyncFzf(searchItems, {
|
||||
const fzfInstance = useMemo(() => new AsyncFzf(items, {
|
||||
fuzzy: 'v2',
|
||||
casing: 'case-insensitive',
|
||||
});
|
||||
return { fzfInstance: fzf, searchMap: map };
|
||||
}, [items]);
|
||||
selector: (item: T) => item.label,
|
||||
}), [items]);
|
||||
|
||||
// Perform search
|
||||
useEffect(() => {
|
||||
@@ -83,12 +65,8 @@ export function useFuzzyList<T extends GenericListItem>({
|
||||
|
||||
if (!active) return;
|
||||
|
||||
const matchedKeys = new Set<string>();
|
||||
results.forEach((res: FzfResult) => {
|
||||
const key = searchMap.get(res.item.toLowerCase());
|
||||
if (key) matchedKeys.add(key);
|
||||
});
|
||||
setFilteredKeys(Array.from(matchedKeys));
|
||||
const matchedKeys = results.map((res: { item: T }) => res.item.key);
|
||||
setFilteredKeys(matchedKeys);
|
||||
onSearch?.(searchQuery);
|
||||
};
|
||||
|
||||
@@ -98,7 +76,7 @@ export function useFuzzyList<T extends GenericListItem>({
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [searchQuery, fzfInstance, searchMap, items, onSearch]);
|
||||
}, [searchQuery, fzfInstance, items, onSearch]);
|
||||
|
||||
// Get mainAreaWidth for search buffer viewport from UIState
|
||||
const { mainAreaWidth } = useUIState();
|
||||
@@ -130,10 +108,7 @@ export function useFuzzyList<T extends GenericListItem>({
|
||||
const labelFull =
|
||||
item.label + (item.scopeMessage ? ` ${item.scopeMessage}` : '');
|
||||
const lWidth = getCachedStringWidth(labelFull);
|
||||
const dWidth = item.description
|
||||
? getCachedStringWidth(item.description)
|
||||
: 0;
|
||||
max = Math.max(max, lWidth, dWidth);
|
||||
max = Math.max(max, lWidth);
|
||||
});
|
||||
return max;
|
||||
}, [items]);
|
||||
|
||||
Reference in New Issue
Block a user