diff --git a/src/App.tsx b/src/App.tsx index 7a01c3f..d552596 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import { FileTree, FileNode } from './components/FileTree'; import { FileTabs } from './components/FileTabs'; import { ConsolePanel } from './components/ConsolePanel'; import { FileSearchModal } from './components/FileSearchModal'; +import { SearchInFilesPanel } from './components/SearchInFilesPanel'; import { CommandPalette, createDefaultCommands } from './components/CommandPalette'; import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './components/ui/resizable'; import { useIsMobile } from './hooks/use-mobile'; @@ -34,6 +35,7 @@ function App() { const [showNewProject, setShowNewProject] = useState(false); const [showFileSearch, setShowFileSearch] = useState(false); const [showCommandPalette, setShowCommandPalette] = useState(false); + const [showSearchInFiles, setShowSearchInFiles] = useState(false); const [code, setCode] = useState(''); const isMobile = useIsMobile(); @@ -102,6 +104,17 @@ function App() { }, description: 'Find in editor', }, + { + key: 'f', + meta: true, + ctrl: true, + shift: true, + handler: () => { + setShowSearchInFiles(true); + captureEvent('keyboard_shortcut', { action: 'search_in_files' }); + }, + description: 'Search in all files', + }, ]); const handleLoginSuccess = (user: { login: string; avatarUrl: string; email: string }) => { @@ -451,6 +464,12 @@ end)`, collapsed={consoleCollapsed} onToggle={() => setConsoleCollapsed(!consoleCollapsed)} /> + setShowSearchInFiles(false)} + /> )} {/* Unified feature tabs for all major panels */} diff --git a/src/components/SearchInFilesPanel.tsx b/src/components/SearchInFilesPanel.tsx new file mode 100644 index 0000000..d0f4b3f --- /dev/null +++ b/src/components/SearchInFilesPanel.tsx @@ -0,0 +1,203 @@ +import { useState } from 'react'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { MagnifyingGlass, X, FileText } from '@phosphor-icons/react'; +import { FileNode } from './FileTree'; + +interface SearchResult { + fileId: string; + fileName: string; + line: number; + content: string; + matchStart: number; + matchEnd: number; +} + +interface SearchInFilesPanelProps { + files: FileNode[]; + onFileSelect: (file: FileNode, line?: number) => void; + isOpen: boolean; + onClose: () => void; +} + +export function SearchInFilesPanel({ + files, + onFileSelect, + isOpen, + onClose, +}: SearchInFilesPanelProps) { + const [searchQuery, setSearchQuery] = useState(''); + const [results, setResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [caseSensitive, setCaseSensitive] = useState(false); + + const searchInFiles = (query: string) => { + if (!query.trim()) { + setResults([]); + return; + } + + setIsSearching(true); + const foundResults: SearchResult[] = []; + + const searchRegex = new RegExp( + caseSensitive ? query : query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), + caseSensitive ? 'g' : 'gi' + ); + + const searchNode = (node: FileNode) => { + if (node.type === 'file' && node.content) { + const lines = node.content.split('\n'); + + lines.forEach((line, index) => { + const matches = Array.from(line.matchAll(searchRegex)); + + matches.forEach((match) => { + if (match.index !== undefined) { + foundResults.push({ + fileId: node.id, + fileName: node.name, + line: index + 1, + content: line.trim(), + matchStart: match.index, + matchEnd: match.index + match[0].length, + }); + } + }); + }); + } + + if (node.children) { + node.children.forEach(searchNode); + } + }; + + files.forEach(searchNode); + setResults(foundResults); + setIsSearching(false); + }; + + const handleSearch = () => { + searchInFiles(searchQuery); + }; + + const handleResultClick = (result: SearchResult) => { + const findFile = (nodes: FileNode[]): FileNode | null => { + for (const node of nodes) { + if (node.id === result.fileId) return node; + if (node.children) { + const found = findFile(node.children); + if (found) return found; + } + } + return null; + }; + + const file = findFile(files); + if (file) { + onFileSelect(file, result.line); + } + }; + + const highlightMatch = (content: string, start: number, end: number) => { + return ( + <> + {content.substring(0, start)} + + {content.substring(start, end)} + + {content.substring(end)} + + ); + }; + + if (!isOpen) return null; + + return ( +
+
+
+ + Search in Files + {results.length > 0 && ( + + {results.length} results + + )} +
+ +
+ +
+
+ setSearchQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSearch(); + }} + className="flex-1" + /> + +
+ +
+ +
+
+ + +
+ {results.length === 0 && searchQuery && !isSearching && ( +
+ No results found for "{searchQuery}" +
+ )} + + {results.length === 0 && !searchQuery && ( +
+ Enter a search query to find text across all files +
+ )} + + {results.map((result, index) => ( +
handleResultClick(result)} + className="p-2 rounded hover:bg-muted/60 cursor-pointer group transition-colors border border-transparent hover:border-accent/30" + > +
+ + + {result.fileName} + + + Line {result.line} + +
+
+ {highlightMatch(result.content, result.matchStart, result.matchEnd)} +
+
+ ))} +
+
+
+ ); +}