Skip to content

Commit

Permalink
feat: add preview support for file selector
Browse files Browse the repository at this point in the history
  • Loading branch information
2214962083 committed Sep 9, 2024
1 parent e07a790 commit 56b0b8c
Show file tree
Hide file tree
Showing 20 changed files with 408 additions and 187 deletions.
27 changes: 27 additions & 0 deletions src/extension/webview-api/controllers/system.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as os from 'os'

import { Controller } from '../types'

export interface SystemInfo {
os: string
cpu: string
memory: string
platform: string
}

export class SystemController extends Controller {
readonly name = 'system'

async getSystemInfo(): Promise<SystemInfo> {
return {
os: `${os.type()} ${os.release()}`,
cpu: os.cpus()[0]?.model || 'Unknown',
memory: `${Math.round(os.totalmem() / (1024 * 1024 * 1024))} GB`,
platform: os.platform()
}
}

async isWindows(): Promise<boolean> {
return os.platform() === 'win32'
}
}
7 changes: 6 additions & 1 deletion src/extension/webview-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as vscode from 'vscode'

import { ChatController } from './controllers/chat.controller'
import { FileController } from './controllers/file.controller'
import { SystemController } from './controllers/system.controller'
import type {
Controller,
ControllerClass,
Expand Down Expand Up @@ -82,7 +83,11 @@ class APIManager {
}
}

export const controllers = [ChatController, FileController] as const
export const controllers = [
ChatController,
FileController,
SystemController
] as const
export type Controllers = typeof controllers

export const setupWebviewAPIManager = (
Expand Down
2 changes: 1 addition & 1 deletion src/webview/components/chat/editor/file-attachments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import { Cross1Icon, PlusIcon } from '@radix-ui/react-icons'
import { FileIcon } from '@webview/components/file-icon'
import type { FileInfo } from '@webview/types/chat'
import { getFileNameFromPath } from '@webview/utils/common'
import { getFileNameFromPath } from '@webview/utils/path'

import { Button } from '../../ui/button'
import { FileSelector } from '../selectors/file-selector'
Expand Down
2 changes: 1 addition & 1 deletion src/webview/components/chat/editor/file-info-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@webview/components/ui/popover'
import { useControllableState } from '@webview/hooks/use-controllable-state'
import { FileInfo } from '@webview/types/chat'
import { getFileNameFromPath } from '@webview/utils/common'
import { getFileNameFromPath } from '@webview/utils/path'

interface FFileInfoPopoverProps {
file: FileInfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
} from '@webview/components/ui/command'
import { useKeyboardNavigation } from '@webview/hooks/use-keyboard-navigation'
import { FileInfo } from '@webview/types/chat'
import { cn, getFileNameFromPath } from '@webview/utils/common'
import { cn } from '@webview/utils/common'
import { getFileNameFromPath } from '@webview/utils/path'
import { useEvent } from 'react-use'

const keyboardShortcuts: ShortcutInfo[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type ShortcutInfo
} from '@webview/components/keyboard-shortcuts-info'
import { Tree, TreeItem, TreeNodeRenderProps } from '@webview/components/tree'
import { useFilesTreeItems } from '@webview/hooks/chat/use-files-tree-items'
import { useKeyboardNavigation } from '@webview/hooks/use-keyboard-navigation'
import { FileInfo } from '@webview/types/chat'
import { cn } from '@webview/utils/common'
Expand Down Expand Up @@ -34,8 +35,7 @@ export const FileTreeView: React.FC<FileTreeViewProps> = ({
const [autoExpandedIds, setAutoExpandedIds] = useState<Set<string>>(new Set())
const initializedRef = useRef(false)
const visibleItemRefs = useRef<(HTMLInputElement | null)[]>([])
const treeItems = useMemo(() => convertFilesToTreeItems(files), [files])
const flattenedItems = useMemo(() => flattenTreeItems(treeItems), [treeItems])
const { treeItems, flattenedItems } = useFilesTreeItems({ files })

const selectedIds = useMemo(
() => selectedFiles.map(file => file.fullPath),
Expand Down Expand Up @@ -92,29 +92,6 @@ export const FileTreeView: React.FC<FileTreeViewProps> = ({
initializedRef.current = true
}, [treeItems, selectedIds, getAllParentIds])

// useEffect(() => {
// if (!initializedRef.current) return

// const newAutoExpandedIds = new Set<string>()
// const expandSearchNodes = (items: TreeItem[]) => {
// items.forEach(item => {
// if (
// searchQuery &&
// item.name.toLowerCase().includes(searchQuery.toLowerCase())
// ) {
// newAutoExpandedIds.add(item.id)
// getAllParentIds(treeItems, item.id).forEach(id =>
// newAutoExpandedIds.add(id)
// )
// }
// if (item.children) expandSearchNodes(item.children)
// })
// }

// expandSearchNodes(treeItems)
// setAutoExpandedIds(newAutoExpandedIds)
// }, [treeItems, searchQuery, getAllParentIds])

useEffect(() => {
if (!initializedRef.current) return

Expand Down Expand Up @@ -254,72 +231,3 @@ export const FileTreeView: React.FC<FileTreeViewProps> = ({
</div>
)
}

// Helper functions (flattenTreeItems and convertFilesToTreeItems) remain unchanged
function flattenTreeItems(items: TreeItem[]): TreeItem[] {
return items.reduce((acc: TreeItem[], item) => {
acc.push(item)
if (item.children) {
acc.push(...flattenTreeItems(item.children))
}
return acc
}, [])
}

function convertFilesToTreeItems(files: FileInfo[]): TreeItem[] {
const root: Record<string, any> = {}

files.forEach(file => {
const parts = file.relativePath.split('/')
let current = root

parts.forEach((part, index) => {
if (!current[part]) {
current[part] =
index === parts.length - 1 ? { ...file, isLeaf: true } : {}
}
if (index < parts.length - 1) current = current[part]
})
})

const sortItems = (items: TreeItem[]): TreeItem[] =>
items.sort((a, b) => {
// Folders come before files
if (a.children && !b.children) return -1
if (!a.children && b.children) return 1

// Alphabetical sorting within each group
return a.name.localeCompare(b.name)
})

const buildTreeItems = (node: any, path: string[] = []): TreeItem => {
const name = path[path.length - 1] || 'root'
const fullPath = path.join('/')

if (node.isLeaf) {
return {
id: node.fullPath,
name,
isLeaf: true,
fullPath: node.fullPath,
relativePath: node.relativePath
}
}

const children = Object.entries(node).map(([key, value]) =>
buildTreeItems(value, [...path, key])
)

return {
id: fullPath || 'root',
name,
children: sortItems(children),
fullPath,
relativePath: fullPath
}
}

return sortItems(
Object.entries(root).map(([key, value]) => buildTreeItems(value, [key]))
)
}
4 changes: 2 additions & 2 deletions src/webview/components/chat/selectors/file-selector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
TabsList,
TabsTrigger
} from '@webview/components/ui/tabs'
import { useFileSearch } from '@webview/hooks/chat/use-file-search'
import { useFilesSearch } from '@webview/hooks/chat/use-files-search'
import { useControllableState } from '@webview/hooks/use-controllable-state'
import { useKeyboardNavigation } from '@webview/hooks/use-keyboard-navigation'
import type { FileInfo } from '@webview/types/chat'
Expand Down Expand Up @@ -57,7 +57,7 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
onChange: onOpenChange
})

const { searchQuery, setSearchQuery, filteredFiles } = useFileSearch()
const { searchQuery, setSearchQuery, filteredFiles } = useFilesSearch()
const [activeTab, setActiveTab] = useState<TabOption>('list')
const [topSearchQuery, setTopSearchQuery] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { FileIcon } from '@webview/components/file-icon'
import { TruncateStart } from '@webview/components/truncate-start'
import type { MentionOption } from '@webview/types/chat'

export const MentionFileItem = (mentionOption: MentionOption) => (
export const MentionFileItem: React.FC<MentionOption> = mentionOption => (
<div className="flex items-center w-full">
<div className="flex-shrink-0 flex items-center mr-2">
<FileIcon
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { memo, useCallback, useMemo } from 'react'
import { ChevronDownIcon } from '@radix-ui/react-icons'
import { FileIcon } from '@webview/components/file-icon'
import { Tree, type TreeNodeRenderProps } from '@webview/components/tree'
import { useFilesTreeItems } from '@webview/hooks/chat/use-files-tree-items'
import type { FileInfo, MentionOption } from '@webview/types/chat'

export const MentionFilePreview: React.FC<MentionOption> = memo(
mentionOption => {
const fileInfo = mentionOption.data as FileInfo
const { treeItems, getAllChildrenIds } = useFilesTreeItems({
files: [fileInfo]
})

const allExpandedIds = useMemo(
() => getAllChildrenIds(treeItems[0]!),
[treeItems, getAllChildrenIds]
)

const renderItem = useCallback(
({
item,
isExpanded,
hasChildren,
onToggleExpand,
level
}: TreeNodeRenderProps) => (
<div
className="flex items-center py-1 text-sm cursor-pointer"
style={{ marginLeft: `${level * 20}px` }}
onClick={onToggleExpand}
>
{hasChildren && <ChevronDownIcon className="size-4 mr-1" />}

<FileIcon
className="size-4 mr-1"
isFolder={hasChildren}
isOpen={isExpanded}
filePath={item.name}
/>

<span>{item.name}</span>
</div>
),
[]
)

return (
<div className="flex flex-col h-full">
<div className="flex flex-col flex-1 overflow-auto">
<Tree
items={treeItems}
expandedItemIds={allExpandedIds}
renderItem={renderItem}
/>
</div>
</div>
)
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const MentionSelector: React.FC<MentionSelectorProps> = ({
})

const itemRefs = useRef<(HTMLDivElement | null)[]>([])
const { focusedIndex, setFocusedIndex, handleKeyDown } =
const { focusedIndex, setFocusedIndex, handleKeyDown, listEventHandlers } =
useKeyboardNavigation({
itemCount: filteredOptions.length,
itemRefs,
Expand All @@ -73,6 +73,8 @@ export const MentionSelector: React.FC<MentionSelectorProps> = ({

useEvent('keydown', handleKeyDown)

const focusedOption = filteredOptions[focusedIndex]

useEffect(() => {
setFocusedIndex(0)
}, [filteredOptions])
Expand Down Expand Up @@ -127,38 +129,64 @@ export const MentionSelector: React.FC<MentionSelectorProps> = ({
onCloseAutoFocus={e => e.preventDefault()}
onKeyDown={e => e.stopPropagation()}
>
<Command ref={commandRef} shouldFilter={false}>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup
className={cn(filteredOptions.length === 0 ? 'p-0' : 'p-1')}
<div>
<Popover open={isOpen && Boolean(focusedOption?.customRenderPreview)}>
<PopoverTrigger asChild>
<div />
</PopoverTrigger>
<PopoverContent
side="top"
align="start"
sideOffset={16}
className="min-w-[200px] max-w-[400px] w-screen p-0 z-99"
onOpenAutoFocus={e => e.preventDefault()}
onCloseAutoFocus={e => e.preventDefault()}
onKeyDown={e => e.stopPropagation()}
>
{filteredOptions.map((option, index) => (
<CommandItem
key={option.id}
defaultValue=""
value=""
onSelect={() => handleSelect(option)}
className={cn(
'px-1.5 py-1',
focusedIndex === index && 'bg-secondary'
)}
ref={el => {
if (itemRefs.current) {
itemRefs.current[index] = el
}
}}
>
{option.customRender ? (
<option.customRender {...option} />
) : (
option.label
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
{focusedOption?.customRenderPreview && (
<focusedOption.customRenderPreview {...focusedOption} />
)}
</PopoverContent>
</Popover>

<Command ref={commandRef} shouldFilter={false}>
<CommandList {...listEventHandlers}>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup
className={cn(filteredOptions.length === 0 ? 'p-0' : 'p-1')}
>
{filteredOptions.map((option, index) => (
<CommandItem
key={option.id}
defaultValue=""
value=""
onSelect={() => handleSelect(option)}
className={cn(
'px-1.5 py-1',
focusedIndex === index && 'bg-secondary'
)}
ref={el => {
if (itemRefs.current) {
itemRefs.current[index] = el
}
}}
>
{option.customRenderItem ? (
<option.customRenderItem {...option} />
) : (
<div className="flex items-center w-full">
{option.itemIcon && (
<option.itemIcon className="size-4 mr-1 shrink-0" />
)}
{option.label}
</div>
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</div>
</PopoverContent>
</Popover>
)
Expand Down
Loading

0 comments on commit 56b0b8c

Please sign in to comment.