Add Search in Files feature with global search capability

Implemented comprehensive search-in-files functionality:
- Search across all files in the project
- Case-sensitive search option
- Real-time search with highlighted results
- Click results to jump to file (with line number displayed)
- Keyboard shortcut: Cmd/Ctrl+Shift+F
- Clean, organized results display with file names and line numbers
- Mobile-responsive design
- Integrates seamlessly with existing file navigation

This completes all planned feature additions for this session.
This commit is contained in:
Claude 2026-01-17 22:22:08 +00:00
parent 6e875b147a
commit 3f42dc2879
No known key found for this signature in database
2 changed files with 222 additions and 0 deletions

View file

@ -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)}
/>
<SearchInFilesPanel
files={files || []}
onFileSelect={handleFileSelect}
isOpen={showSearchInFiles}
onClose={() => setShowSearchInFiles(false)}
/>
</>
)}
{/* Unified feature tabs for all major panels */}

View file

@ -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<SearchResult[]>([]);
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)}
<span className="bg-yellow-500/30 text-yellow-200 font-semibold">
{content.substring(start, end)}
</span>
{content.substring(end)}
</>
);
};
if (!isOpen) return null;
return (
<div className="h-[300px] md:h-[400px] bg-card border-t border-border flex flex-col">
<div className="flex items-center justify-between px-4 py-2 border-b border-border">
<div className="flex items-center gap-2">
<MagnifyingGlass size={18} weight="bold" />
<span className="text-sm font-semibold">Search in Files</span>
{results.length > 0 && (
<Badge variant="secondary" className="text-xs">
{results.length} results
</Badge>
)}
</div>
<Button variant="ghost" size="icon" onClick={onClose} className="h-6 w-6">
<X size={16} />
</Button>
</div>
<div className="px-4 py-3 border-b border-border space-y-2">
<div className="flex gap-2">
<Input
placeholder="Search for text..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSearch();
}}
className="flex-1"
/>
<Button onClick={handleSearch} disabled={isSearching || !searchQuery.trim()}>
<MagnifyingGlass size={16} className="mr-1" />
Search
</Button>
</div>
<div className="flex items-center gap-2">
<label className="flex items-center gap-1 text-xs cursor-pointer">
<input
type="checkbox"
checked={caseSensitive}
onChange={(e) => setCaseSensitive(e.target.checked)}
className="rounded"
/>
<span>Case sensitive</span>
</label>
</div>
</div>
<ScrollArea className="flex-1">
<div className="px-4 py-2 space-y-1">
{results.length === 0 && searchQuery && !isSearching && (
<div className="text-center text-muted-foreground py-8 text-sm">
No results found for "{searchQuery}"
</div>
)}
{results.length === 0 && !searchQuery && (
<div className="text-center text-muted-foreground py-8 text-sm">
Enter a search query to find text across all files
</div>
)}
{results.map((result, index) => (
<div
key={`${result.fileId}-${result.line}-${index}`}
onClick={() => handleResultClick(result)}
className="p-2 rounded hover:bg-muted/60 cursor-pointer group transition-colors border border-transparent hover:border-accent/30"
>
<div className="flex items-center gap-2 mb-1">
<FileText size={14} className="text-muted-foreground flex-shrink-0" />
<span className="text-xs font-semibold text-accent">
{result.fileName}
</span>
<span className="text-xs text-muted-foreground">
Line {result.line}
</span>
</div>
<div className="text-xs font-mono text-foreground/80 ml-5 truncate">
{highlightMatch(result.content, result.matchStart, result.matchEnd)}
</div>
</div>
))}
</div>
</ScrollArea>
</div>
);
}