Search API
The Search API provides comprehensive search functionality across files, symbols, and content within the development environment.
Overview
The Search API enables you to:
- Search for files by name and path
- Search for text content across files
- Search for symbols (functions, classes, variables)
- Perform regex-based searches
- Search within specific scopes and directories
- Provide search suggestions and autocomplete
- Index and cache search results
- Support fuzzy matching and ranking
Basic Usage
Search Manager
import { TraeAPI } from '@trae/api';
// Search manager
class SearchManager {
private fileIndex: Map<string, FileIndexEntry> = new Map();
private symbolIndex: Map<string, SymbolIndexEntry[]> = new Map();
private contentIndex: Map<string, ContentIndexEntry[]> = new Map();
private searchHistory: SearchHistoryEntry[] = [];
private eventEmitter = new TraeAPI.EventEmitter<SearchEvent>();
private indexingInProgress = false;
private watchers: TraeAPI.FileSystemWatcher[] = [];
constructor() {
this.initializeIndexing();
this.setupFileWatchers();
}
// Initialize indexing
private async initializeIndexing(): Promise<void> {
if (this.indexingInProgress) return;
this.indexingInProgress = true;
console.log('Starting search indexing...');
this.eventEmitter.fire({ type: 'indexingStarted' });
try {
await this.buildFileIndex();
await this.buildSymbolIndex();
await this.buildContentIndex();
console.log('Search indexing completed');
this.eventEmitter.fire({ type: 'indexingCompleted' });
} catch (error) {
console.error('Search indexing failed:', error);
this.eventEmitter.fire({ type: 'indexingFailed', error: error as Error });
} finally {
this.indexingInProgress = false;
}
}
// Build file index
private async buildFileIndex(): Promise<void> {
const workspaceFolders = TraeAPI.workspace.workspaceFolders;
if (!workspaceFolders) return;
for (const folder of workspaceFolders) {
await this.indexDirectory(folder.uri.fsPath);
}
}
private async indexDirectory(dirPath: string): Promise<void> {
try {
const entries = await TraeAPI.workspace.fs.readDirectory(TraeAPI.Uri.file(dirPath));
for (const [name, type] of entries) {
const fullPath = TraeAPI.path.join(dirPath, name);
if (type === TraeAPI.FileType.Directory) {
// Skip common directories that shouldn't be indexed
if (this.shouldSkipDirectory(name)) {
continue;
}
await this.indexDirectory(fullPath);
} else if (type === TraeAPI.FileType.File) {
await this.indexFile(fullPath);
}
}
} catch (error) {
console.error(`Failed to index directory ${dirPath}:`, error);
}
}
private shouldSkipDirectory(name: string): boolean {
const skipDirs = [
'node_modules', '.git', '.svn', '.hg',
'dist', 'build', 'out', 'target',
'.vscode', '.idea', '__pycache__',
'coverage', '.nyc_output'
];
return skipDirs.includes(name) || name.startsWith('.');
}
private async indexFile(filePath: string): Promise<void> {
try {
const stat = await TraeAPI.workspace.fs.stat(TraeAPI.Uri.file(filePath));
const fileName = TraeAPI.path.basename(filePath);
const extension = TraeAPI.path.extname(filePath);
const relativePath = TraeAPI.workspace.asRelativePath(filePath);
const entry: FileIndexEntry = {
path: filePath,
relativePath,
name: fileName,
extension,
size: stat.size,
lastModified: stat.mtime,
language: this.getLanguageFromExtension(extension),
searchTerms: this.generateFileSearchTerms(fileName, relativePath)
};
this.fileIndex.set(filePath, entry);
} catch (error) {
console.error(`Failed to index file ${filePath}:`, error);
}
}
private getLanguageFromExtension(extension: string): string {
const languageMap: { [key: string]: string } = {
'.js': 'javascript',
'.ts': 'typescript',
'.jsx': 'javascriptreact',
'.tsx': 'typescriptreact',
'.py': 'python',
'.java': 'java',
'.cpp': 'cpp',
'.c': 'c',
'.cs': 'csharp',
'.php': 'php',
'.rb': 'ruby',
'.go': 'go',
'.rs': 'rust',
'.swift': 'swift',
'.kt': 'kotlin',
'.scala': 'scala',
'.html': 'html',
'.css': 'css',
'.scss': 'scss',
'.less': 'less',
'.json': 'json',
'.xml': 'xml',
'.yaml': 'yaml',
'.yml': 'yaml',
'.md': 'markdown',
'.txt': 'plaintext'
};
return languageMap[extension.toLowerCase()] || 'plaintext';
}
private generateFileSearchTerms(fileName: string, relativePath: string): string[] {
const terms = new Set<string>();
// Add filename without extension
const nameWithoutExt = TraeAPI.path.parse(fileName).name;
terms.add(nameWithoutExt.toLowerCase());
// Add full filename
terms.add(fileName.toLowerCase());
// Add path segments
const pathSegments = relativePath.split(TraeAPI.path.sep);
pathSegments.forEach(segment => {
if (segment) {
terms.add(segment.toLowerCase());
}
});
// Add camelCase and snake_case splits
const camelCaseSplit = nameWithoutExt.split(/(?=[A-Z])/);
camelCaseSplit.forEach(part => {
if (part) {
terms.add(part.toLowerCase());
}
});
const snakeCaseSplit = nameWithoutExt.split(/[_-]/);
snakeCaseSplit.forEach(part => {
if (part) {
terms.add(part.toLowerCase());
}
});
return Array.from(terms);
}
// Build symbol index
private async buildSymbolIndex(): Promise<void> {
for (const [filePath, fileEntry] of this.fileIndex) {
if (this.isCodeFile(fileEntry.language)) {
await this.indexFileSymbols(filePath);
}
}
}
private isCodeFile(language: string): boolean {
const codeLanguages = [
'javascript', 'typescript', 'javascriptreact', 'typescriptreact',
'python', 'java', 'cpp', 'c', 'csharp', 'php', 'ruby',
'go', 'rust', 'swift', 'kotlin', 'scala'
];
return codeLanguages.includes(language);
}
private async indexFileSymbols(filePath: string): Promise<void> {
try {
const document = await TraeAPI.workspace.openTextDocument(TraeAPI.Uri.file(filePath));
const symbols = await TraeAPI.commands.executeCommand<TraeAPI.DocumentSymbol[]>(
'vscode.executeDocumentSymbolProvider',
document.uri
);
if (symbols) {
const symbolEntries = this.flattenSymbols(symbols, filePath);
this.symbolIndex.set(filePath, symbolEntries);
}
} catch (error) {
console.error(`Failed to index symbols for ${filePath}:`, error);
}
}
private flattenSymbols(symbols: TraeAPI.DocumentSymbol[], filePath: string, parent?: string): SymbolIndexEntry[] {
const entries: SymbolIndexEntry[] = [];
for (const symbol of symbols) {
const fullName = parent ? `${parent}.${symbol.name}` : symbol.name;
entries.push({
name: symbol.name,
fullName,
kind: symbol.kind,
filePath,
range: symbol.range,
selectionRange: symbol.selectionRange,
searchTerms: this.generateSymbolSearchTerms(symbol.name, fullName)
});
// Recursively process children
if (symbol.children) {
entries.push(...this.flattenSymbols(symbol.children, filePath, fullName));
}
}
return entries;
}
private generateSymbolSearchTerms(name: string, fullName: string): string[] {
const terms = new Set<string>();
terms.add(name.toLowerCase());
terms.add(fullName.toLowerCase());
// Add camelCase splits
const camelCaseSplit = name.split(/(?=[A-Z])/);
camelCaseSplit.forEach(part => {
if (part) {
terms.add(part.toLowerCase());
}
});
// Add snake_case splits
const snakeCaseSplit = name.split(/[_-]/);
snakeCaseSplit.forEach(part => {
if (part) {
terms.add(part.toLowerCase());
}
});
return Array.from(terms);
}
// Build content index
private async buildContentIndex(): Promise<void> {
for (const [filePath, fileEntry] of this.fileIndex) {
if (this.shouldIndexContent(fileEntry)) {
await this.indexFileContent(filePath);
}
}
}
private shouldIndexContent(fileEntry: FileIndexEntry): boolean {
// Skip binary files and very large files
if (fileEntry.size > 1024 * 1024) { // 1MB limit
return false;
}
const textExtensions = [
'.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cpp', '.c',
'.cs', '.php', '.rb', '.go', '.rs', '.swift', '.kt', '.scala',
'.html', '.css', '.scss', '.less', '.json', '.xml', '.yaml',
'.yml', '.md', '.txt', '.log', '.config', '.ini'
];
return textExtensions.includes(fileEntry.extension.toLowerCase());
}
private async indexFileContent(filePath: string): Promise<void> {
try {
const document = await TraeAPI.workspace.openTextDocument(TraeAPI.Uri.file(filePath));
const content = document.getText();
const lines = content.split('\n');
const entries: ContentIndexEntry[] = [];
lines.forEach((line, lineNumber) => {
const trimmedLine = line.trim();
if (trimmedLine.length > 0) {
entries.push({
filePath,
lineNumber: lineNumber + 1,
content: line,
trimmedContent: trimmedLine,
searchTerms: this.generateContentSearchTerms(trimmedLine)
});
}
});
this.contentIndex.set(filePath, entries);
} catch (error) {
console.error(`Failed to index content for ${filePath}:`, error);
}
}
private generateContentSearchTerms(content: string): string[] {
const terms = new Set<string>();
// Split by common delimiters
const words = content.toLowerCase().split(/[\s\W]+/);
words.forEach(word => {
if (word.length > 2) { // Skip very short words
terms.add(word);
}
});
return Array.from(terms);
}
// Search files
async searchFiles(query: string, options: FileSearchOptions = {}): Promise<FileSearchResult[]> {
const results: FileSearchResult[] = [];
const queryLower = query.toLowerCase();
const maxResults = options.maxResults || 100;
for (const [filePath, entry] of this.fileIndex) {
if (results.length >= maxResults) break;
// Apply filters
if (options.includePatterns && !this.matchesPatterns(entry.relativePath, options.includePatterns)) {
continue;
}
if (options.excludePatterns && this.matchesPatterns(entry.relativePath, options.excludePatterns)) {
continue;
}
if (options.fileTypes && !options.fileTypes.includes(entry.extension)) {
continue;
}
// Calculate match score
const score = this.calculateFileMatchScore(entry, queryLower);
if (score > 0) {
results.push({
file: entry,
score,
matches: this.getFileMatches(entry, queryLower)
});
}
}
// Sort by score (descending)
results.sort((a, b) => b.score - a.score);
// Add to search history
this.addToSearchHistory({
type: 'file',
query,
timestamp: Date.now(),
resultCount: results.length
});
return results;
}
private calculateFileMatchScore(entry: FileIndexEntry, query: string): number {
let score = 0;
// Exact filename match (highest score)
if (entry.name.toLowerCase() === query) {
score += 100;
}
// Filename starts with query
if (entry.name.toLowerCase().startsWith(query)) {
score += 80;
}
// Filename contains query
if (entry.name.toLowerCase().includes(query)) {
score += 60;
}
// Path contains query
if (entry.relativePath.toLowerCase().includes(query)) {
score += 40;
}
// Search terms match
for (const term of entry.searchTerms) {
if (term === query) {
score += 50;
} else if (term.startsWith(query)) {
score += 30;
} else if (term.includes(query)) {
score += 20;
}
}
// Fuzzy match bonus
const fuzzyScore = this.calculateFuzzyScore(entry.name.toLowerCase(), query);
score += fuzzyScore * 10;
return score;
}
private calculateFuzzyScore(text: string, query: string): number {
if (query.length === 0) return 0;
if (text.length === 0) return 0;
let score = 0;
let queryIndex = 0;
let lastMatchIndex = -1;
for (let i = 0; i < text.length && queryIndex < query.length; i++) {
if (text[i] === query[queryIndex]) {
score += 1;
// Bonus for consecutive matches
if (i === lastMatchIndex + 1) {
score += 0.5;
}
lastMatchIndex = i;
queryIndex++;
}
}
// Normalize score
return queryIndex === query.length ? score / query.length : 0;
}
private getFileMatches(entry: FileIndexEntry, query: string): FileMatch[] {
const matches: FileMatch[] = [];
// Check filename match
const nameIndex = entry.name.toLowerCase().indexOf(query);
if (nameIndex !== -1) {
matches.push({
type: 'filename',
text: entry.name,
startIndex: nameIndex,
length: query.length
});
}
// Check path match
const pathIndex = entry.relativePath.toLowerCase().indexOf(query);
if (pathIndex !== -1) {
matches.push({
type: 'path',
text: entry.relativePath,
startIndex: pathIndex,
length: query.length
});
}
return matches;
}
private matchesPatterns(path: string, patterns: string[]): boolean {
return patterns.some(pattern => {
// Simple glob pattern matching
const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'));
return regex.test(path);
});
}
// Search symbols
async searchSymbols(query: string, options: SymbolSearchOptions = {}): Promise<SymbolSearchResult[]> {
const results: SymbolSearchResult[] = [];
const queryLower = query.toLowerCase();
const maxResults = options.maxResults || 100;
for (const [filePath, symbols] of this.symbolIndex) {
if (results.length >= maxResults) break;
// Apply file filters
const fileEntry = this.fileIndex.get(filePath);
if (!fileEntry) continue;
if (options.fileTypes && !options.fileTypes.includes(fileEntry.extension)) {
continue;
}
for (const symbol of symbols) {
if (results.length >= maxResults) break;
// Apply symbol kind filter
if (options.symbolKinds && !options.symbolKinds.includes(symbol.kind)) {
continue;
}
// Calculate match score
const score = this.calculateSymbolMatchScore(symbol, queryLower);
if (score > 0) {
results.push({
symbol,
file: fileEntry,
score,
matches: this.getSymbolMatches(symbol, queryLower)
});
}
}
}
// Sort by score (descending)
results.sort((a, b) => b.score - a.score);
// Add to search history
this.addToSearchHistory({
type: 'symbol',
query,
timestamp: Date.now(),
resultCount: results.length
});
return results;
}
private calculateSymbolMatchScore(symbol: SymbolIndexEntry, query: string): number {
let score = 0;
// Exact name match
if (symbol.name.toLowerCase() === query) {
score += 100;
}
// Name starts with query
if (symbol.name.toLowerCase().startsWith(query)) {
score += 80;
}
// Name contains query
if (symbol.name.toLowerCase().includes(query)) {
score += 60;
}
// Full name contains query
if (symbol.fullName.toLowerCase().includes(query)) {
score += 40;
}
// Search terms match
for (const term of symbol.searchTerms) {
if (term === query) {
score += 50;
} else if (term.startsWith(query)) {
score += 30;
} else if (term.includes(query)) {
score += 20;
}
}
// Symbol kind bonus (functions and classes get higher scores)
if (symbol.kind === TraeAPI.SymbolKind.Function || symbol.kind === TraeAPI.SymbolKind.Method) {
score += 10;
} else if (symbol.kind === TraeAPI.SymbolKind.Class) {
score += 15;
}
return score;
}
private getSymbolMatches(symbol: SymbolIndexEntry, query: string): SymbolMatch[] {
const matches: SymbolMatch[] = [];
// Check name match
const nameIndex = symbol.name.toLowerCase().indexOf(query);
if (nameIndex !== -1) {
matches.push({
type: 'name',
text: symbol.name,
startIndex: nameIndex,
length: query.length
});
}
// Check full name match
const fullNameIndex = symbol.fullName.toLowerCase().indexOf(query);
if (fullNameIndex !== -1 && symbol.fullName !== symbol.name) {
matches.push({
type: 'fullName',
text: symbol.fullName,
startIndex: fullNameIndex,
length: query.length
});
}
return matches;
}
// Search content
async searchContent(query: string, options: ContentSearchOptions = {}): Promise<ContentSearchResult[]> {
const results: ContentSearchResult[] = [];
const maxResults = options.maxResults || 1000;
const isRegex = options.isRegex || false;
const isCaseSensitive = options.isCaseSensitive || false;
let searchRegex: RegExp;
try {
if (isRegex) {
searchRegex = new RegExp(query, isCaseSensitive ? 'g' : 'gi');
} else {
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
searchRegex = new RegExp(escapedQuery, isCaseSensitive ? 'g' : 'gi');
}
} catch (error) {
throw new Error(`Invalid search pattern: ${error}`);
}
for (const [filePath, contentEntries] of this.contentIndex) {
if (results.length >= maxResults) break;
// Apply file filters
const fileEntry = this.fileIndex.get(filePath);
if (!fileEntry) continue;
if (options.includePatterns && !this.matchesPatterns(fileEntry.relativePath, options.includePatterns)) {
continue;
}
if (options.excludePatterns && this.matchesPatterns(fileEntry.relativePath, options.excludePatterns)) {
continue;
}
if (options.fileTypes && !options.fileTypes.includes(fileEntry.extension)) {
continue;
}
for (const contentEntry of contentEntries) {
if (results.length >= maxResults) break;
const matches = Array.from(contentEntry.content.matchAll(searchRegex));
if (matches.length > 0) {
results.push({
file: fileEntry,
lineNumber: contentEntry.lineNumber,
content: contentEntry.content,
matches: matches.map(match => ({
startIndex: match.index || 0,
length: match[0].length,
text: match[0]
}))
});
}
}
}
// Add to search history
this.addToSearchHistory({
type: 'content',
query,
timestamp: Date.now(),
resultCount: results.length
});
return results;
}
// Search suggestions
async getSearchSuggestions(query: string, type: SearchType): Promise<string[]> {
const suggestions = new Set<string>();
const queryLower = query.toLowerCase();
// Get suggestions from search history
const historySuggestions = this.searchHistory
.filter(entry => entry.type === type && entry.query.toLowerCase().startsWith(queryLower))
.map(entry => entry.query)
.slice(0, 5);
historySuggestions.forEach(suggestion => suggestions.add(suggestion));
// Get suggestions based on type
switch (type) {
case 'file':
// Suggest file names and paths
for (const entry of this.fileIndex.values()) {
if (suggestions.size >= 10) break;
if (entry.name.toLowerCase().startsWith(queryLower)) {
suggestions.add(entry.name);
}
for (const term of entry.searchTerms) {
if (term.startsWith(queryLower)) {
suggestions.add(term);
}
}
}
break;
case 'symbol':
// Suggest symbol names
for (const symbols of this.symbolIndex.values()) {
if (suggestions.size >= 10) break;
for (const symbol of symbols) {
if (symbol.name.toLowerCase().startsWith(queryLower)) {
suggestions.add(symbol.name);
}
for (const term of symbol.searchTerms) {
if (term.startsWith(queryLower)) {
suggestions.add(term);
}
}
}
}
break;
case 'content':
// Suggest common words from content
for (const contentEntries of this.contentIndex.values()) {
if (suggestions.size >= 10) break;
for (const entry of contentEntries) {
for (const term of entry.searchTerms) {
if (term.startsWith(queryLower)) {
suggestions.add(term);
}
}
}
}
break;
}
return Array.from(suggestions).slice(0, 10);
}
// Search history management
private addToSearchHistory(entry: SearchHistoryEntry): void {
// Remove duplicate entries
this.searchHistory = this.searchHistory.filter(
existing => !(existing.type === entry.type && existing.query === entry.query)
);
// Add new entry at the beginning
this.searchHistory.unshift(entry);
// Limit history size
if (this.searchHistory.length > 100) {
this.searchHistory = this.searchHistory.slice(0, 100);
}
}
getSearchHistory(type?: SearchType): SearchHistoryEntry[] {
if (type) {
return this.searchHistory.filter(entry => entry.type === type);
}
return [...this.searchHistory];
}
clearSearchHistory(type?: SearchType): void {
if (type) {
this.searchHistory = this.searchHistory.filter(entry => entry.type !== type);
} else {
this.searchHistory = [];
}
}
// File system watchers
private setupFileWatchers(): void {
const workspaceFolders = TraeAPI.workspace.workspaceFolders;
if (!workspaceFolders) return;
for (const folder of workspaceFolders) {
const pattern = new TraeAPI.RelativePattern(folder, '**/*');
const watcher = TraeAPI.workspace.createFileSystemWatcher(pattern);
watcher.onDidCreate(uri => this.onFileCreated(uri.fsPath));
watcher.onDidChange(uri => this.onFileChanged(uri.fsPath));
watcher.onDidDelete(uri => this.onFileDeleted(uri.fsPath));
this.watchers.push(watcher);
}
}
private async onFileCreated(filePath: string): Promise<void> {
await this.indexFile(filePath);
const fileEntry = this.fileIndex.get(filePath);
if (fileEntry) {
if (this.isCodeFile(fileEntry.language)) {
await this.indexFileSymbols(filePath);
}
if (this.shouldIndexContent(fileEntry)) {
await this.indexFileContent(filePath);
}
}
this.eventEmitter.fire({ type: 'fileIndexed', filePath });
}
private async onFileChanged(filePath: string): Promise<void> {
const fileEntry = this.fileIndex.get(filePath);
if (!fileEntry) return;
// Update file entry
await this.indexFile(filePath);
// Update symbols if it's a code file
if (this.isCodeFile(fileEntry.language)) {
await this.indexFileSymbols(filePath);
}
// Update content if applicable
if (this.shouldIndexContent(fileEntry)) {
await this.indexFileContent(filePath);
}
this.eventEmitter.fire({ type: 'fileUpdated', filePath });
}
private onFileDeleted(filePath: string): void {
this.fileIndex.delete(filePath);
this.symbolIndex.delete(filePath);
this.contentIndex.delete(filePath);
this.eventEmitter.fire({ type: 'fileRemoved', filePath });
}
// Utility methods
getIndexStats(): IndexStats {
const symbolCount = Array.from(this.symbolIndex.values())
.reduce((total, symbols) => total + symbols.length, 0);
const contentLineCount = Array.from(this.contentIndex.values())
.reduce((total, entries) => total + entries.length, 0);
return {
fileCount: this.fileIndex.size,
symbolCount,
contentLineCount,
isIndexing: this.indexingInProgress
};
}
async refreshIndex(): Promise<void> {
this.fileIndex.clear();
this.symbolIndex.clear();
this.contentIndex.clear();
await this.initializeIndexing();
}
// Event handling
onDidChangeIndex(listener: (event: SearchEvent) => void): TraeAPI.Disposable {
return this.eventEmitter.event(listener);
}
dispose(): void {
this.watchers.forEach(watcher => watcher.dispose());
this.watchers = [];
this.fileIndex.clear();
this.symbolIndex.clear();
this.contentIndex.clear();
this.searchHistory = [];
this.eventEmitter.dispose();
}
}
// Initialize search manager
const searchManager = new SearchManager();Search UI Integration
// Search UI provider
class SearchUIProvider {
private searchPanel: TraeAPI.WebviewPanel | null = null;
private currentSearchType: SearchType = 'file';
private currentQuery = '';
private searchResults: any[] = [];
constructor(private searchManager: SearchManager) {
this.setupCommands();
this.setupEventListeners();
}
private setupCommands(): void {
// Register search commands
TraeAPI.commands.registerCommand('search.showPanel', () => {
this.showSearchPanel();
});
TraeAPI.commands.registerCommand('search.searchFiles', async () => {
const query = await TraeAPI.window.showInputBox({
prompt: 'Search for files',
placeHolder: 'Enter file name or pattern...'
});
if (query) {
await this.performSearch(query, 'file');
}
});
TraeAPI.commands.registerCommand('search.searchSymbols', async () => {
const query = await TraeAPI.window.showInputBox({
prompt: 'Search for symbols',
placeHolder: 'Enter symbol name...'
});
if (query) {
await this.performSearch(query, 'symbol');
}
});
TraeAPI.commands.registerCommand('search.searchContent', async () => {
const query = await TraeAPI.window.showInputBox({
prompt: 'Search in files',
placeHolder: 'Enter search text...'
});
if (query) {
await this.performSearch(query, 'content');
}
});
}
private async showSearchPanel(): Promise<void> {
if (this.searchPanel) {
this.searchPanel.reveal();
return;
}
this.searchPanel = TraeAPI.window.createWebviewPanel(
'search',
'Search',
TraeAPI.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true
}
);
this.searchPanel.webview.html = this.getSearchHTML();
this.setupWebviewMessageHandling();
this.searchPanel.onDidDispose(() => {
this.searchPanel = null;
});
}
private getSearchHTML(): string {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Search</title>
<style>
body {
margin: 0;
padding: 20px;
background: #1e1e1e;
color: #d4d4d4;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
}
.search-container {
max-width: 800px;
margin: 0 auto;
}
.search-header {
margin-bottom: 20px;
}
.search-tabs {
display: flex;
margin-bottom: 15px;
border-bottom: 1px solid #3e3e42;
}
.search-tab {
padding: 8px 16px;
background: none;
border: none;
color: #cccccc;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.search-tab.active {
color: #ffffff;
border-bottom-color: #007acc;
}
.search-input-container {
display: flex;
margin-bottom: 15px;
}
.search-input {
flex: 1;
padding: 8px 12px;
background: #2d2d30;
border: 1px solid #3e3e42;
color: #d4d4d4;
border-radius: 3px;
font-size: 14px;
}
.search-input:focus {
outline: none;
border-color: #007acc;
}
.search-button {
margin-left: 8px;
padding: 8px 16px;
background: #0e639c;
border: none;
color: #ffffff;
border-radius: 3px;
cursor: pointer;
}
.search-button:hover {
background: #1177bb;
}
.search-options {
display: flex;
gap: 15px;
margin-bottom: 20px;
font-size: 12px;
}
.search-option {
display: flex;
align-items: center;
gap: 5px;
}
.search-option input[type="checkbox"] {
margin: 0;
}
.search-results {
border-top: 1px solid #3e3e42;
padding-top: 15px;
}
.search-stats {
margin-bottom: 15px;
color: #cccccc;
font-size: 12px;
}
.search-result {
margin-bottom: 10px;
padding: 10px;
background: #2d2d30;
border-radius: 3px;
cursor: pointer;
}
.search-result:hover {
background: #37373d;
}
.result-title {
font-weight: 500;
margin-bottom: 5px;
}
.result-path {
color: #cccccc;
font-size: 11px;
margin-bottom: 5px;
}
.result-content {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
color: #d4d4d4;
white-space: pre-wrap;
}
.result-match {
background: #515c6a;
color: #ffffff;
padding: 1px 2px;
border-radius: 2px;
}
.no-results {
text-align: center;
color: #cccccc;
padding: 40px;
}
.loading {
text-align: center;
color: #cccccc;
padding: 20px;
}
.suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #2d2d30;
border: 1px solid #3e3e42;
border-top: none;
border-radius: 0 0 3px 3px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
}
.suggestion {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #3e3e42;
}
.suggestion:hover {
background: #37373d;
}
.suggestion:last-child {
border-bottom: none;
}
</style>
</head>
<body>
<div class="search-container">
<div class="search-header">
<div class="search-tabs">
<button class="search-tab active" data-type="file">Files</button>
<button class="search-tab" data-type="symbol">Symbols</button>
<button class="search-tab" data-type="content">Content</button>
</div>
<div class="search-input-container" style="position: relative;">
<input type="text" class="search-input" id="searchInput" placeholder="Enter search query...">
<button class="search-button" onclick="performSearch()">Search</button>
<div class="suggestions" id="suggestions" style="display: none;"></div>
</div>
<div class="search-options">
<div class="search-option">
<input type="checkbox" id="caseSensitive">
<label for="caseSensitive">Case sensitive</label>
</div>
<div class="search-option">
<input type="checkbox" id="useRegex">
<label for="useRegex">Use regex</label>
</div>
<div class="search-option">
<input type="checkbox" id="wholeWord">
<label for="wholeWord">Whole word</label>
</div>
</div>
</div>
<div class="search-results">
<div class="search-stats" id="searchStats"></div>
<div id="resultsContainer">
<div class="no-results">Enter a search query to begin</div>
</div>
</div>
</div>
<script>
const vscode = acquireVsCodeApi();
let currentSearchType = 'file';
let searchTimeout = null;
// Setup event listeners
document.addEventListener('DOMContentLoaded', () => {
const searchInput = document.getElementById('searchInput');
const tabs = document.querySelectorAll('.search-tab');
// Tab switching
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentSearchType = tab.dataset.type;
updatePlaceholder();
clearResults();
});
});
// Search input
searchInput.addEventListener('input', (e) => {
const query = e.target.value;
// Debounce search
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
if (query.length > 0) {
getSuggestions(query);
if (query.length >= 2) {
performSearch();
}
} else {
hideSuggestions();
clearResults();
}
}, 300);
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
performSearch();
hideSuggestions();
} else if (e.key === 'Escape') {
hideSuggestions();
}
});
// Hide suggestions when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.search-input-container')) {
hideSuggestions();
}
});
updatePlaceholder();
});
function updatePlaceholder() {
const input = document.getElementById('searchInput');
const placeholders = {
file: 'Search for files by name...',
symbol: 'Search for symbols (functions, classes, etc.)...',
content: 'Search for text in files...'
};
input.placeholder = placeholders[currentSearchType];
}
function performSearch() {
const query = document.getElementById('searchInput').value.trim();
if (!query) return;
showLoading();
const options = {
isCaseSensitive: document.getElementById('caseSensitive').checked,
isRegex: document.getElementById('useRegex').checked,
wholeWord: document.getElementById('wholeWord').checked
};
vscode.postMessage({
type: 'search',
searchType: currentSearchType,
query,
options
});
}
function getSuggestions(query) {
vscode.postMessage({
type: 'getSuggestions',
searchType: currentSearchType,
query
});
}
function showSuggestions(suggestions) {
const suggestionsEl = document.getElementById('suggestions');
if (suggestions.length === 0) {
hideSuggestions();
return;
}
suggestionsEl.innerHTML = suggestions.map(suggestion =>
`<div class="suggestion" onclick="selectSuggestion('${suggestion}')">${suggestion}</div>`
).join('');
suggestionsEl.style.display = 'block';
}
function hideSuggestions() {
document.getElementById('suggestions').style.display = 'none';
}
function selectSuggestion(suggestion) {
document.getElementById('searchInput').value = suggestion;
hideSuggestions();
performSearch();
}
function showLoading() {
document.getElementById('resultsContainer').innerHTML =
'<div class="loading">Searching...</div>';
}
function clearResults() {
document.getElementById('resultsContainer').innerHTML =
'<div class="no-results">Enter a search query to begin</div>';
document.getElementById('searchStats').textContent = '';
}
function showResults(results, stats) {
const container = document.getElementById('resultsContainer');
const statsEl = document.getElementById('searchStats');
statsEl.textContent = `${results.length} results found in ${stats.searchTime}ms`;
if (results.length === 0) {
container.innerHTML = '<div class="no-results">No results found</div>';
return;
}
container.innerHTML = results.map(result => {
switch (currentSearchType) {
case 'file':
return createFileResult(result);
case 'symbol':
return createSymbolResult(result);
case 'content':
return createContentResult(result);
default:
return '';
}
}).join('');
}
function createFileResult(result) {
return `
<div class="search-result" onclick="openFile('${result.file.path}')">
<div class="result-title">${result.file.name}</div>
<div class="result-path">${result.file.relativePath}</div>
</div>
`;
}
function createSymbolResult(result) {
return `
<div class="search-result" onclick="openSymbol('${result.file.path}', ${result.symbol.range.start.line})">
<div class="result-title">${result.symbol.name}</div>
<div class="result-path">${result.file.relativePath}:${result.symbol.range.start.line + 1}</div>
</div>
`;
}
function createContentResult(result) {
let content = result.content;
// Highlight matches
result.matches.forEach(match => {
const before = content.substring(0, match.startIndex);
const matchText = content.substring(match.startIndex, match.startIndex + match.length);
const after = content.substring(match.startIndex + match.length);
content = before + `<span class="result-match">${matchText}</span>` + after;
});
return `
<div class="search-result" onclick="openFile('${result.file.path}', ${result.lineNumber})">
<div class="result-path">${result.file.relativePath}:${result.lineNumber}</div>
<div class="result-content">${content}</div>
</div>
`;
}
function openFile(filePath, lineNumber) {
vscode.postMessage({
type: 'openFile',
filePath,
lineNumber
});
}
function openSymbol(filePath, lineNumber) {
vscode.postMessage({
type: 'openFile',
filePath,
lineNumber
});
}
// Handle messages from extension
window.addEventListener('message', event => {
const message = event.data;
switch (message.type) {
case 'searchResults':
showResults(message.results, message.stats);
break;
case 'suggestions':
showSuggestions(message.suggestions);
break;
}
});
</script>
</body>
</html>
`;
}
private setupWebviewMessageHandling(): void {
if (!this.searchPanel) return;
this.searchPanel.webview.onDidReceiveMessage(async message => {
switch (message.type) {
case 'search':
await this.handleSearchRequest(message);
break;
case 'getSuggestions':
await this.handleSuggestionsRequest(message);
break;
case 'openFile':
await this.handleOpenFile(message);
break;
}
});
}
private async handleSearchRequest(message: any): Promise<void> {
const startTime = Date.now();
let results: any[] = [];
try {
switch (message.searchType) {
case 'file':
results = await this.searchManager.searchFiles(message.query, message.options);
break;
case 'symbol':
results = await this.searchManager.searchSymbols(message.query, message.options);
break;
case 'content':
results = await this.searchManager.searchContent(message.query, message.options);
break;
}
} catch (error) {
console.error('Search failed:', error);
results = [];
}
const searchTime = Date.now() - startTime;
this.sendMessage({
type: 'searchResults',
results,
stats: { searchTime }
});
}
private async handleSuggestionsRequest(message: any): Promise<void> {
try {
const suggestions = await this.searchManager.getSearchSuggestions(
message.query,
message.searchType
);
this.sendMessage({
type: 'suggestions',
suggestions
});
} catch (error) {
console.error('Failed to get suggestions:', error);
}
}
private async handleOpenFile(message: any): Promise<void> {
try {
const uri = TraeAPI.Uri.file(message.filePath);
const document = await TraeAPI.workspace.openTextDocument(uri);
const editor = await TraeAPI.window.showTextDocument(document);
if (message.lineNumber) {
const line = message.lineNumber - 1; // Convert to 0-based
const position = new TraeAPI.Position(line, 0);
editor.selection = new TraeAPI.Selection(position, position);
editor.revealRange(new TraeAPI.Range(position, position));
}
} catch (error) {
console.error('Failed to open file:', error);
TraeAPI.window.showErrorMessage(`Failed to open file: ${message.filePath}`);
}
}
private async performSearch(query: string, type: SearchType): Promise<void> {
this.currentQuery = query;
this.currentSearchType = type;
try {
let results: any[] = [];
switch (type) {
case 'file':
results = await this.searchManager.searchFiles(query);
break;
case 'symbol':
results = await this.searchManager.searchSymbols(query);
break;
case 'content':
results = await this.searchManager.searchContent(query);
break;
}
this.searchResults = results;
await this.showSearchResults(results, type);
} catch (error) {
console.error('Search failed:', error);
TraeAPI.window.showErrorMessage(`Search failed: ${error}`);
}
}
private async showSearchResults(results: any[], type: SearchType): Promise<void> {
if (results.length === 0) {
TraeAPI.window.showInformationMessage('No results found');
return;
}
// Show quick pick for results
const items = results.slice(0, 50).map(result => {
switch (type) {
case 'file':
return {
label: result.file.name,
description: result.file.relativePath,
detail: `Score: ${result.score}`,
result
};
case 'symbol':
return {
label: result.symbol.name,
description: `${result.file.relativePath}:${result.symbol.range.start.line + 1}`,
detail: `${this.getSymbolKindName(result.symbol.kind)} - Score: ${result.score}`,
result
};
case 'content':
return {
label: result.content.trim(),
description: `${result.file.relativePath}:${result.lineNumber}`,
detail: `${result.matches.length} match(es)`,
result
};
default:
return { label: '', description: '', result };
}
});
const selected = await TraeAPI.window.showQuickPick(items, {
placeHolder: `Select from ${results.length} search results`,
matchOnDescription: true,
matchOnDetail: true
});
if (selected) {
await this.openSearchResult(selected.result, type);
}
}
private async openSearchResult(result: any, type: SearchType): Promise<void> {
let filePath: string;
let lineNumber: number | undefined;
switch (type) {
case 'file':
filePath = result.file.path;
break;
case 'symbol':
filePath = result.file.path;
lineNumber = result.symbol.range.start.line;
break;
case 'content':
filePath = result.file.path;
lineNumber = result.lineNumber - 1; // Convert to 0-based
break;
default:
return;
}
try {
const uri = TraeAPI.Uri.file(filePath);
const document = await TraeAPI.workspace.openTextDocument(uri);
const editor = await TraeAPI.window.showTextDocument(document);
if (lineNumber !== undefined) {
const position = new TraeAPI.Position(lineNumber, 0);
editor.selection = new TraeAPI.Selection(position, position);
editor.revealRange(new TraeAPI.Range(position, position));
}
} catch (error) {
console.error('Failed to open search result:', error);
TraeAPI.window.showErrorMessage(`Failed to open file: ${filePath}`);
}
}
private getSymbolKindName(kind: TraeAPI.SymbolKind): string {
const kindNames: { [key: number]: string } = {
[TraeAPI.SymbolKind.File]: 'File',
[TraeAPI.SymbolKind.Module]: 'Module',
[TraeAPI.SymbolKind.Namespace]: 'Namespace',
[TraeAPI.SymbolKind.Package]: 'Package',
[TraeAPI.SymbolKind.Class]: 'Class',
[TraeAPI.SymbolKind.Method]: 'Method',
[TraeAPI.SymbolKind.Property]: 'Property',
[TraeAPI.SymbolKind.Field]: 'Field',
[TraeAPI.SymbolKind.Constructor]: 'Constructor',
[TraeAPI.SymbolKind.Enum]: 'Enum',
[TraeAPI.SymbolKind.Interface]: 'Interface',
[TraeAPI.SymbolKind.Function]: 'Function',
[TraeAPI.SymbolKind.Variable]: 'Variable',
[TraeAPI.SymbolKind.Constant]: 'Constant',
[TraeAPI.SymbolKind.String]: 'String',
[TraeAPI.SymbolKind.Number]: 'Number',
[TraeAPI.SymbolKind.Boolean]: 'Boolean',
[TraeAPI.SymbolKind.Array]: 'Array',
[TraeAPI.SymbolKind.Object]: 'Object',
[TraeAPI.SymbolKind.Key]: 'Key',
[TraeAPI.SymbolKind.Null]: 'Null',
[TraeAPI.SymbolKind.EnumMember]: 'EnumMember',
[TraeAPI.SymbolKind.Struct]: 'Struct',
[TraeAPI.SymbolKind.Event]: 'Event',
[TraeAPI.SymbolKind.Operator]: 'Operator',
[TraeAPI.SymbolKind.TypeParameter]: 'TypeParameter'
};
return kindNames[kind] || 'Unknown';
}
private sendMessage(message: any): void {
if (this.searchPanel) {
this.searchPanel.webview.postMessage(message);
}
}
private setupEventListeners(): void {
this.searchManager.onDidChangeIndex(event => {
// Update search panel if open
if (this.searchPanel && this.currentQuery) {
// Re-run current search to get updated results
this.performSearch(this.currentQuery, this.currentSearchType);
}
});
}
}
// Initialize search UI
const searchUIProvider = new SearchUIProvider(searchManager);Interfaces
interface FileIndexEntry {
path: string;
relativePath: string;
name: string;
extension: string;
size: number;
lastModified: number;
language: string;
searchTerms: string[];
}
interface SymbolIndexEntry {
name: string;
fullName: string;
kind: TraeAPI.SymbolKind;
filePath: string;
range: TraeAPI.Range;
selectionRange: TraeAPI.Range;
searchTerms: string[];
}
interface ContentIndexEntry {
filePath: string;
lineNumber: number;
content: string;
trimmedContent: string;
searchTerms: string[];
}
interface SearchHistoryEntry {
type: SearchType;
query: string;
timestamp: number;
resultCount: number;
}
interface FileSearchOptions {
maxResults?: number;
includePatterns?: string[];
excludePatterns?: string[];
fileTypes?: string[];
}
interface SymbolSearchOptions {
maxResults?: number;
fileTypes?: string[];
symbolKinds?: TraeAPI.SymbolKind[];
}
interface ContentSearchOptions {
maxResults?: number;
includePatterns?: string[];
excludePatterns?: string[];
fileTypes?: string[];
isCaseSensitive?: boolean;
isRegex?: boolean;
wholeWord?: boolean;
}
interface FileSearchResult {
file: FileIndexEntry;
score: number;
matches: FileMatch[];
}
interface SymbolSearchResult {
symbol: SymbolIndexEntry;
file: FileIndexEntry;
score: number;
matches: SymbolMatch[];
}
interface ContentSearchResult {
file: FileIndexEntry;
lineNumber: number;
content: string;
matches: ContentMatch[];
}
interface FileMatch {
type: 'filename' | 'path';
text: string;
startIndex: number;
length: number;
}
interface SymbolMatch {
type: 'name' | 'fullName';
text: string;
startIndex: number;
length: number;
}
interface ContentMatch {
startIndex: number;
length: number;
text: string;
}
interface IndexStats {
fileCount: number;
symbolCount: number;
contentLineCount: number;
isIndexing: boolean;
}
interface SearchEvent {
type: 'indexingStarted' | 'indexingCompleted' | 'indexingFailed' | 'fileIndexed' | 'fileUpdated' | 'fileRemoved';
filePath?: string;
error?: Error;
}
type SearchType = 'file' | 'symbol' | 'content';API Reference
SearchManager
Methods
searchFiles(query: string, options?: FileSearchOptions): Promise<FileSearchResult[]>- Search for files by name and path
- Returns ranked results with match information
searchSymbols(query: string, options?: SymbolSearchOptions): Promise<SymbolSearchResult[]>- Search for symbols (functions, classes, variables)
- Supports filtering by symbol kind and file type
searchContent(query: string, options?: ContentSearchOptions): Promise<ContentSearchResult[]>- Search for text content within files
- Supports regex patterns and case sensitivity
getSearchSuggestions(query: string, type: SearchType): Promise<string[]>- Get search suggestions based on query and type
- Returns relevant suggestions from history and index
getSearchHistory(type?: SearchType): SearchHistoryEntry[]- Get search history entries
- Optionally filter by search type
clearSearchHistory(type?: SearchType): void- Clear search history
- Optionally clear only specific type
getIndexStats(): IndexStats- Get current indexing statistics
- Returns file, symbol, and content counts
refreshIndex(): Promise<void>- Rebuild the entire search index
- Useful after major file system changes
onDidChangeIndex(listener: (event: SearchEvent) => void): TraeAPI.Disposable- Listen for index change events
- Returns disposable to stop listening
dispose(): void- Clean up resources and stop file watchers
SearchUIProvider
Methods
showSearchPanel(): Promise<void>- Show the search webview panel
- Creates panel if it doesn't exist
performSearch(query: string, type: SearchType): Promise<void>- Perform search and show results
- Updates UI with search results
Best Practices
Performance Optimization
// Limit search results for better performance
const results = await searchManager.searchFiles(query, {
maxResults: 50
});
// Use specific file type filters
const jsResults = await searchManager.searchSymbols(query, {
fileTypes: ['.js', '.ts'],
symbolKinds: [TraeAPI.SymbolKind.Function, TraeAPI.SymbolKind.Class]
});
// Use exclude patterns for better performance
const contentResults = await searchManager.searchContent(query, {
excludePatterns: ['node_modules/**', 'dist/**', '*.min.js']
});Search Query Optimization
// Use specific queries for better results
const specificResults = await searchManager.searchFiles('UserService.ts');
// Use fuzzy matching for partial matches
const fuzzyResults = await searchManager.searchFiles('usrserv');
// Use regex for complex patterns
const regexResults = await searchManager.searchContent('function\\s+\\w+\\(', {
isRegex: true
});Index Management
// Monitor index status
searchManager.onDidChangeIndex(event => {
switch (event.type) {
case 'indexingStarted':
console.log('Search indexing started');
break;
case 'indexingCompleted':
console.log('Search indexing completed');
break;
case 'indexingFailed':
console.error('Search indexing failed:', event.error);
break;
}
});
// Check index stats
const stats = searchManager.getIndexStats();
console.log(`Indexed ${stats.fileCount} files, ${stats.symbolCount} symbols`);
// Refresh index when needed
if (majorChangesDetected) {
await searchManager.refreshIndex();
}Error Handling
try {
const results = await searchManager.searchContent(userQuery, {
isRegex: true
});
displayResults(results);
} catch (error) {
if (error.message.includes('Invalid search pattern')) {
TraeAPI.window.showErrorMessage('Invalid regex pattern. Please check your search query.');
} else {
TraeAPI.window.showErrorMessage(`Search failed: ${error.message}`);
}
}Memory Management
// Dispose search manager when no longer needed
class MyExtension {
private searchManager: SearchManager;
activate() {
this.searchManager = new SearchManager();
}
deactivate() {
this.searchManager.dispose();
}
}
// Limit content indexing for large files
const shouldIndex = (fileEntry: FileIndexEntry) => {
return fileEntry.size < 1024 * 1024; // 1MB limit
};Related APIs
- Workspace API - File system operations
- Editor API - Text editor integration
- Commands API - Search command registration
- UI API - Search UI components
- Language Services API - Symbol providers