Skip to content

Files API

The Files API provides functionality for file system operations, file watching, and file management in Trae.

Overview

The Files API enables you to:

  • Read and write files
  • Watch for file system changes
  • Manage file permissions and metadata
  • Handle file operations (copy, move, delete)
  • Work with directories and file trees
  • Implement file providers and virtual file systems
  • Handle file encoding and binary data
  • Manage file associations and content types

Basic Usage

File Manager

typescript
import { TraeAPI } from '@trae/api';
import * as path from 'path';
import * as fs from 'fs/promises';

// File manager
class FileManager {
  private watchers: Map<string, TraeAPI.FileSystemWatcher> = new Map();
  private fileProviders: Map<string, FileProvider> = new Map();
  private eventEmitter = new TraeAPI.EventEmitter<FileEvent>();
  private disposables: TraeAPI.Disposable[] = [];
  private fileCache: Map<string, CachedFile> = new Map();
  private fileAssociations: Map<string, FileAssociation> = new Map();
  private contentTypeDetectors: ContentTypeDetector[] = [];
  private fileOperationQueue: FileOperation[] = [];
  private isProcessingQueue = false;

  constructor() {
    this.setupBuiltinProviders();
    this.setupFileAssociations();
    this.setupContentTypeDetectors();
    this.setupEventHandlers();
  }

  // File reading
  async readFile(uri: TraeAPI.Uri): Promise<Uint8Array> {
    try {
      const cached = this.fileCache.get(uri.toString());
      if (cached && !this.isFileModified(uri, cached.lastModified)) {
        return cached.content;
      }

      const content = await TraeAPI.workspace.fs.readFile(uri);
      
      // Cache the file
      this.cacheFile(uri, content);
      
      this.eventEmitter.fire({ type: 'fileRead', uri, size: content.length });
      
      return content;
    } catch (error) {
      this.eventEmitter.fire({ type: 'fileReadError', uri, error });
      throw error;
    }
  }

  async readTextFile(uri: TraeAPI.Uri, encoding: string = 'utf8'): Promise<string> {
    const content = await this.readFile(uri);
    return Buffer.from(content).toString(encoding);
  }

  async readJsonFile<T = any>(uri: TraeAPI.Uri): Promise<T> {
    const content = await this.readTextFile(uri);
    try {
      return JSON.parse(content);
    } catch (error) {
      throw new Error(`Invalid JSON in file ${uri.fsPath}: ${error}`);
    }
  }

  // File writing
  async writeFile(uri: TraeAPI.Uri, content: Uint8Array): Promise<void> {
    try {
      await this.queueFileOperation({
        type: 'write',
        uri,
        content,
        timestamp: new Date()
      });
      
      await TraeAPI.workspace.fs.writeFile(uri, content);
      
      // Update cache
      this.cacheFile(uri, content);
      
      this.eventEmitter.fire({ type: 'fileWritten', uri, size: content.length });
    } catch (error) {
      this.eventEmitter.fire({ type: 'fileWriteError', uri, error });
      throw error;
    }
  }

  async writeTextFile(uri: TraeAPI.Uri, content: string, encoding: string = 'utf8'): Promise<void> {
    const buffer = Buffer.from(content, encoding);
    await this.writeFile(uri, new Uint8Array(buffer));
  }

  async writeJsonFile(uri: TraeAPI.Uri, content: any, indent: number = 2): Promise<void> {
    const jsonString = JSON.stringify(content, null, indent);
    await this.writeTextFile(uri, jsonString);
  }

  // File operations
  async copyFile(source: TraeAPI.Uri, destination: TraeAPI.Uri, overwrite: boolean = false): Promise<void> {
    try {
      await this.queueFileOperation({
        type: 'copy',
        source,
        destination,
        overwrite,
        timestamp: new Date()
      });
      
      await TraeAPI.workspace.fs.copy(source, destination, { overwrite });
      
      this.eventEmitter.fire({ type: 'fileCopied', source, destination });
    } catch (error) {
      this.eventEmitter.fire({ type: 'fileCopyError', source, destination, error });
      throw error;
    }
  }

  async moveFile(source: TraeAPI.Uri, destination: TraeAPI.Uri, overwrite: boolean = false): Promise<void> {
    try {
      await this.queueFileOperation({
        type: 'move',
        source,
        destination,
        overwrite,
        timestamp: new Date()
      });
      
      await TraeAPI.workspace.fs.rename(source, destination, { overwrite });
      
      // Update cache
      const cached = this.fileCache.get(source.toString());
      if (cached) {
        this.fileCache.delete(source.toString());
        this.fileCache.set(destination.toString(), {
          ...cached,
          uri: destination
        });
      }
      
      this.eventEmitter.fire({ type: 'fileMoved', source, destination });
    } catch (error) {
      this.eventEmitter.fire({ type: 'fileMoveError', source, destination, error });
      throw error;
    }
  }

  async deleteFile(uri: TraeAPI.Uri, recursive: boolean = false): Promise<void> {
    try {
      await this.queueFileOperation({
        type: 'delete',
        uri,
        recursive,
        timestamp: new Date()
      });
      
      await TraeAPI.workspace.fs.delete(uri, { recursive, useTrash: true });
      
      // Remove from cache
      this.fileCache.delete(uri.toString());
      
      this.eventEmitter.fire({ type: 'fileDeleted', uri });
    } catch (error) {
      this.eventEmitter.fire({ type: 'fileDeleteError', uri, error });
      throw error;
    }
  }

  // Directory operations
  async createDirectory(uri: TraeAPI.Uri): Promise<void> {
    try {
      await TraeAPI.workspace.fs.createDirectory(uri);
      this.eventEmitter.fire({ type: 'directoryCreated', uri });
    } catch (error) {
      this.eventEmitter.fire({ type: 'directoryCreateError', uri, error });
      throw error;
    }
  }

  async readDirectory(uri: TraeAPI.Uri): Promise<[string, TraeAPI.FileType][]> {
    try {
      const entries = await TraeAPI.workspace.fs.readDirectory(uri);
      this.eventEmitter.fire({ type: 'directoryRead', uri, entryCount: entries.length });
      return entries;
    } catch (error) {
      this.eventEmitter.fire({ type: 'directoryReadError', uri, error });
      throw error;
    }
  }

  async getDirectoryTree(uri: TraeAPI.Uri, maxDepth: number = 10): Promise<FileTreeNode> {
    const stat = await this.stat(uri);
    const node: FileTreeNode = {
      uri,
      name: path.basename(uri.fsPath),
      type: stat.type,
      size: stat.size,
      lastModified: stat.mtime,
      children: []
    };

    if (stat.type === TraeAPI.FileType.Directory && maxDepth > 0) {
      try {
        const entries = await this.readDirectory(uri);
        
        for (const [name, type] of entries) {
          const childUri = TraeAPI.Uri.joinPath(uri, name);
          const childNode = await this.getDirectoryTree(childUri, maxDepth - 1);
          node.children!.push(childNode);
        }
        
        // Sort children: directories first, then files
        node.children!.sort((a, b) => {
          if (a.type !== b.type) {
            return a.type === TraeAPI.FileType.Directory ? -1 : 1;
          }
          return a.name.localeCompare(b.name);
        });
      } catch (error) {
        // Directory might not be readable
        console.warn(`Cannot read directory ${uri.fsPath}:`, error);
      }
    }

    return node;
  }

  // File information
  async stat(uri: TraeAPI.Uri): Promise<TraeAPI.FileStat> {
    try {
      const stat = await TraeAPI.workspace.fs.stat(uri);
      this.eventEmitter.fire({ type: 'fileStat', uri, stat });
      return stat;
    } catch (error) {
      this.eventEmitter.fire({ type: 'fileStatError', uri, error });
      throw error;
    }
  }

  async exists(uri: TraeAPI.Uri): Promise<boolean> {
    try {
      await this.stat(uri);
      return true;
    } catch {
      return false;
    }
  }

  async isFile(uri: TraeAPI.Uri): Promise<boolean> {
    try {
      const stat = await this.stat(uri);
      return stat.type === TraeAPI.FileType.File;
    } catch {
      return false;
    }
  }

  async isDirectory(uri: TraeAPI.Uri): Promise<boolean> {
    try {
      const stat = await this.stat(uri);
      return stat.type === TraeAPI.FileType.Directory;
    } catch {
      return false;
    }
  }

  // File watching
  watchFile(uri: TraeAPI.Uri, options?: FileWatchOptions): TraeAPI.Disposable {
    const watcherId = uri.toString();
    
    if (this.watchers.has(watcherId)) {
      throw new Error(`File is already being watched: ${uri.fsPath}`);
    }

    const pattern = new TraeAPI.RelativePattern(
      TraeAPI.workspace.getWorkspaceFolder(uri) || TraeAPI.workspace.workspaceFolders![0],
      path.relative(TraeAPI.workspace.workspaceFolders![0].uri.fsPath, uri.fsPath)
    );

    const watcher = TraeAPI.workspace.createFileSystemWatcher(pattern);
    
    const disposables: TraeAPI.Disposable[] = [];
    
    if (options?.watchCreate !== false) {
      disposables.push(watcher.onDidCreate(uri => {
        this.eventEmitter.fire({ type: 'fileCreated', uri });
        options?.onCreate?.(uri);
      }));
    }
    
    if (options?.watchChange !== false) {
      disposables.push(watcher.onDidChange(uri => {
        // Invalidate cache
        this.fileCache.delete(uri.toString());
        
        this.eventEmitter.fire({ type: 'fileChanged', uri });
        options?.onChange?.(uri);
      }));
    }
    
    if (options?.watchDelete !== false) {
      disposables.push(watcher.onDidDelete(uri => {
        // Remove from cache
        this.fileCache.delete(uri.toString());
        
        this.eventEmitter.fire({ type: 'fileDeleted', uri });
        options?.onDelete?.(uri);
      }));
    }

    this.watchers.set(watcherId, watcher);
    
    this.eventEmitter.fire({ type: 'watcherCreated', uri });

    return {
      dispose: () => {
        watcher.dispose();
        disposables.forEach(d => d.dispose());
        this.watchers.delete(watcherId);
        this.eventEmitter.fire({ type: 'watcherDisposed', uri });
      }
    };
  }

  watchDirectory(uri: TraeAPI.Uri, options?: DirectoryWatchOptions): TraeAPI.Disposable {
    const watcherId = uri.toString();
    
    if (this.watchers.has(watcherId)) {
      throw new Error(`Directory is already being watched: ${uri.fsPath}`);
    }

    const pattern = new TraeAPI.RelativePattern(
      TraeAPI.workspace.getWorkspaceFolder(uri) || TraeAPI.workspace.workspaceFolders![0],
      path.relative(TraeAPI.workspace.workspaceFolders![0].uri.fsPath, uri.fsPath) + '/**'
    );

    const watcher = TraeAPI.workspace.createFileSystemWatcher(pattern);
    
    const disposables: TraeAPI.Disposable[] = [];
    
    disposables.push(watcher.onDidCreate(uri => {
      this.eventEmitter.fire({ type: 'fileCreated', uri });
      options?.onCreate?.(uri);
    }));
    
    disposables.push(watcher.onDidChange(uri => {
      // Invalidate cache
      this.fileCache.delete(uri.toString());
      
      this.eventEmitter.fire({ type: 'fileChanged', uri });
      options?.onChange?.(uri);
    }));
    
    disposables.push(watcher.onDidDelete(uri => {
      // Remove from cache
      this.fileCache.delete(uri.toString());
      
      this.eventEmitter.fire({ type: 'fileDeleted', uri });
      options?.onDelete?.(uri);
    }));

    this.watchers.set(watcherId, watcher);
    
    this.eventEmitter.fire({ type: 'watcherCreated', uri });

    return {
      dispose: () => {
        watcher.dispose();
        disposables.forEach(d => d.dispose());
        this.watchers.delete(watcherId);
        this.eventEmitter.fire({ type: 'watcherDisposed', uri });
      }
    };
  }

  // File search
  async findFiles(
    include: TraeAPI.GlobPattern,
    exclude?: TraeAPI.GlobPattern,
    maxResults?: number,
    token?: TraeAPI.CancellationToken
  ): Promise<TraeAPI.Uri[]> {
    try {
      const files = await TraeAPI.workspace.findFiles(include, exclude, maxResults, token);
      this.eventEmitter.fire({ type: 'filesFound', pattern: include.toString(), count: files.length });
      return files;
    } catch (error) {
      this.eventEmitter.fire({ type: 'fileSearchError', pattern: include.toString(), error });
      throw error;
    }
  }

  async searchInFiles(
    query: string,
    options?: SearchOptions
  ): Promise<SearchResult[]> {
    const results: SearchResult[] = [];
    
    const files = await this.findFiles(
      options?.include || '**/*',
      options?.exclude,
      options?.maxResults
    );
    
    for (const file of files) {
      try {
        const content = await this.readTextFile(file);
        const lines = content.split('\n');
        
        for (let i = 0; i < lines.length; i++) {
          const line = lines[i];
          const matches = this.findMatches(line, query, options);
          
          for (const match of matches) {
            results.push({
              uri: file,
              line: i + 1,
              column: match.start + 1,
              text: line,
              match: {
                start: match.start,
                end: match.end,
                text: match.text
              }
            });
          }
        }
      } catch (error) {
        // Skip files that can't be read
        console.warn(`Cannot search in file ${file.fsPath}:`, error);
      }
    }
    
    this.eventEmitter.fire({ type: 'searchCompleted', query, resultCount: results.length });
    
    return results;
  }

  private findMatches(text: string, query: string, options?: SearchOptions): TextMatch[] {
    const matches: TextMatch[] = [];
    
    if (options?.useRegex) {
      try {
        const flags = options.caseSensitive ? 'g' : 'gi';
        const regex = new RegExp(query, flags);
        let match;
        
        while ((match = regex.exec(text)) !== null) {
          matches.push({
            start: match.index,
            end: match.index + match[0].length,
            text: match[0]
          });
        }
      } catch (error) {
        // Invalid regex, fall back to literal search
        return this.findLiteralMatches(text, query, options);
      }
    } else {
      return this.findLiteralMatches(text, query, options);
    }
    
    return matches;
  }

  private findLiteralMatches(text: string, query: string, options?: SearchOptions): TextMatch[] {
    const matches: TextMatch[] = [];
    const searchText = options?.caseSensitive ? text : text.toLowerCase();
    const searchQuery = options?.caseSensitive ? query : query.toLowerCase();
    
    let index = 0;
    while ((index = searchText.indexOf(searchQuery, index)) !== -1) {
      matches.push({
        start: index,
        end: index + query.length,
        text: text.substring(index, index + query.length)
      });
      index += query.length;
    }
    
    return matches;
  }

  // File associations
  registerFileAssociation(association: FileAssociation): void {
    this.fileAssociations.set(association.pattern, association);
    this.eventEmitter.fire({ type: 'fileAssociationRegistered', association });
  }

  getFileAssociation(uri: TraeAPI.Uri): FileAssociation | undefined {
    const fileName = path.basename(uri.fsPath);
    
    for (const [pattern, association] of this.fileAssociations) {
      if (this.matchesPattern(fileName, pattern)) {
        return association;
      }
    }
    
    return undefined;
  }

  private matchesPattern(fileName: string, pattern: string): boolean {
    // Simple glob pattern matching
    const regex = new RegExp(
      '^' + pattern
        .replace(/\./g, '\\.')
        .replace(/\*/g, '.*')
        .replace(/\?/g, '.') + '$',
      'i'
    );
    
    return regex.test(fileName);
  }

  // Content type detection
  registerContentTypeDetector(detector: ContentTypeDetector): void {
    this.contentTypeDetectors.push(detector);
    this.eventEmitter.fire({ type: 'contentTypeDetectorRegistered', detector });
  }

  async detectContentType(uri: TraeAPI.Uri): Promise<string> {
    // Try registered detectors first
    for (const detector of this.contentTypeDetectors) {
      try {
        const contentType = await detector.detect(uri);
        if (contentType) {
          return contentType;
        }
      } catch (error) {
        console.warn('Content type detector failed:', error);
      }
    }
    
    // Fall back to file extension
    const ext = path.extname(uri.fsPath).toLowerCase();
    return this.getContentTypeByExtension(ext);
  }

  private getContentTypeByExtension(extension: string): string {
    const mimeTypes: { [key: string]: string } = {
      '.txt': 'text/plain',
      '.md': 'text/markdown',
      '.json': 'application/json',
      '.js': 'text/javascript',
      '.ts': 'text/typescript',
      '.html': 'text/html',
      '.css': 'text/css',
      '.xml': 'text/xml',
      '.yaml': 'text/yaml',
      '.yml': 'text/yaml',
      '.png': 'image/png',
      '.jpg': 'image/jpeg',
      '.jpeg': 'image/jpeg',
      '.gif': 'image/gif',
      '.svg': 'image/svg+xml',
      '.pdf': 'application/pdf',
      '.zip': 'application/zip'
    };
    
    return mimeTypes[extension] || 'application/octet-stream';
  }

  // File operations queue
  private async queueFileOperation(operation: FileOperation): Promise<void> {
    this.fileOperationQueue.push(operation);
    
    if (!this.isProcessingQueue) {
      await this.processFileOperationQueue();
    }
  }

  private async processFileOperationQueue(): Promise<void> {
    this.isProcessingQueue = true;
    
    try {
      while (this.fileOperationQueue.length > 0) {
        const operation = this.fileOperationQueue.shift()!;
        
        try {
          await this.executeFileOperation(operation);
        } catch (error) {
          console.error('File operation failed:', operation, error);
        }
      }
    } finally {
      this.isProcessingQueue = false;
    }
  }

  private async executeFileOperation(operation: FileOperation): Promise<void> {
    // File operations are handled by the calling methods
    // This is a placeholder for additional operation processing
    this.eventEmitter.fire({ type: 'fileOperationExecuted', operation });
  }

  // Cache management
  private cacheFile(uri: TraeAPI.Uri, content: Uint8Array): void {
    this.fileCache.set(uri.toString(), {
      uri,
      content,
      lastModified: new Date(),
      size: content.length
    });
  }

  private async isFileModified(uri: TraeAPI.Uri, cachedTime: Date): Promise<boolean> {
    try {
      const stat = await this.stat(uri);
      return stat.mtime > cachedTime.getTime();
    } catch {
      return true; // Assume modified if we can't check
    }
  }

  clearCache(): void {
    this.fileCache.clear();
    this.eventEmitter.fire({ type: 'cacheCleared' });
  }

  getCacheSize(): number {
    return this.fileCache.size;
  }

  getCacheStats(): CacheStats {
    let totalSize = 0;
    for (const cached of this.fileCache.values()) {
      totalSize += cached.size;
    }
    
    return {
      entryCount: this.fileCache.size,
      totalSize,
      averageSize: this.fileCache.size > 0 ? totalSize / this.fileCache.size : 0
    };
  }

  // Utility methods
  private setupBuiltinProviders(): void {
    // Setup built-in file providers
  }

  private setupFileAssociations(): void {
    // Register common file associations
    this.registerFileAssociation({
      pattern: '*.ts',
      language: 'typescript',
      icon: 'typescript',
      description: 'TypeScript file'
    });
    
    this.registerFileAssociation({
      pattern: '*.js',
      language: 'javascript',
      icon: 'javascript',
      description: 'JavaScript file'
    });
    
    this.registerFileAssociation({
      pattern: '*.json',
      language: 'json',
      icon: 'json',
      description: 'JSON file'
    });
    
    this.registerFileAssociation({
      pattern: '*.md',
      language: 'markdown',
      icon: 'markdown',
      description: 'Markdown file'
    });
  }

  private setupContentTypeDetectors(): void {
    // Register built-in content type detectors
    this.registerContentTypeDetector({
      detect: async (uri: TraeAPI.Uri) => {
        // Detect by file signature/magic bytes
        try {
          const content = await this.readFile(uri);
          if (content.length >= 4) {
            // PNG signature
            if (content[0] === 0x89 && content[1] === 0x50 && content[2] === 0x4E && content[3] === 0x47) {
              return 'image/png';
            }
            // JPEG signature
            if (content[0] === 0xFF && content[1] === 0xD8) {
              return 'image/jpeg';
            }
            // PDF signature
            if (content[0] === 0x25 && content[1] === 0x50 && content[2] === 0x44 && content[3] === 0x46) {
              return 'application/pdf';
            }
          }
        } catch {
          // Ignore errors
        }
        
        return undefined;
      }
    });
  }

  private setupEventHandlers(): void {
    // Setup additional event handlers
  }

  // Event handling
  onDidChangeFile(listener: (event: FileEvent) => void): TraeAPI.Disposable {
    return this.eventEmitter.event(listener);
  }

  // Dispose
  dispose(): void {
    // Dispose all watchers
    for (const watcher of this.watchers.values()) {
      watcher.dispose();
    }
    this.watchers.clear();
    
    // Dispose other resources
    this.disposables.forEach(d => d.dispose());
    this.disposables = [];
    
    // Clear caches
    this.fileCache.clear();
    this.fileAssociations.clear();
    this.contentTypeDetectors = [];
    this.fileOperationQueue = [];
    
    this.eventEmitter.dispose();
  }
}

// Initialize file manager
const fileManager = new FileManager();

Advanced File Operations

File Provider Implementation

typescript
// Custom file provider
class CustomFileProvider implements FileProvider {
  private scheme: string;
  private files: Map<string, VirtualFile> = new Map();
  private eventEmitter = new TraeAPI.EventEmitter<TraeAPI.FileChangeEvent>();

  constructor(scheme: string) {
    this.scheme = scheme;
  }

  get onDidChangeFile(): TraeAPI.Event<TraeAPI.FileChangeEvent> {
    return this.eventEmitter.event;
  }

  watch(uri: TraeAPI.Uri, options: { recursive: boolean; excludes: string[] }): TraeAPI.Disposable {
    // Implement file watching for virtual files
    return {
      dispose: () => {
        // Cleanup watcher
      }
    };
  }

  stat(uri: TraeAPI.Uri): TraeAPI.FileStat | Thenable<TraeAPI.FileStat> {
    const file = this.files.get(uri.toString());
    if (!file) {
      throw TraeAPI.FileSystemError.FileNotFound(uri);
    }

    return {
      type: file.type,
      ctime: file.created.getTime(),
      mtime: file.modified.getTime(),
      size: file.content.length
    };
  }

  readDirectory(uri: TraeAPI.Uri): [string, TraeAPI.FileType][] | Thenable<[string, TraeAPI.FileType][]> {
    const entries: [string, TraeAPI.FileType][] = [];
    const prefix = uri.toString() + '/';

    for (const [path, file] of this.files) {
      if (path.startsWith(prefix)) {
        const relativePath = path.substring(prefix.length);
        if (!relativePath.includes('/')) {
          entries.push([relativePath, file.type]);
        }
      }
    }

    return entries;
  }

  createDirectory(uri: TraeAPI.Uri): void | Thenable<void> {
    this.files.set(uri.toString(), {
      type: TraeAPI.FileType.Directory,
      content: new Uint8Array(),
      created: new Date(),
      modified: new Date()
    });

    this.eventEmitter.fire({
      type: TraeAPI.FileChangeType.Created,
      uri
    });
  }

  readFile(uri: TraeAPI.Uri): Uint8Array | Thenable<Uint8Array> {
    const file = this.files.get(uri.toString());
    if (!file) {
      throw TraeAPI.FileSystemError.FileNotFound(uri);
    }

    if (file.type !== TraeAPI.FileType.File) {
      throw TraeAPI.FileSystemError.FileIsADirectory(uri);
    }

    return file.content;
  }

  writeFile(
    uri: TraeAPI.Uri,
    content: Uint8Array,
    options: { create: boolean; overwrite: boolean }
  ): void | Thenable<void> {
    const exists = this.files.has(uri.toString());

    if (exists && !options.overwrite) {
      throw TraeAPI.FileSystemError.FileExists(uri);
    }

    if (!exists && !options.create) {
      throw TraeAPI.FileSystemError.FileNotFound(uri);
    }

    const now = new Date();
    this.files.set(uri.toString(), {
      type: TraeAPI.FileType.File,
      content,
      created: exists ? this.files.get(uri.toString())!.created : now,
      modified: now
    });

    this.eventEmitter.fire({
      type: exists ? TraeAPI.FileChangeType.Changed : TraeAPI.FileChangeType.Created,
      uri
    });
  }

  delete(uri: TraeAPI.Uri, options: { recursive: boolean }): void | Thenable<void> {
    const file = this.files.get(uri.toString());
    if (!file) {
      throw TraeAPI.FileSystemError.FileNotFound(uri);
    }

    if (file.type === TraeAPI.FileType.Directory && !options.recursive) {
      // Check if directory is empty
      const prefix = uri.toString() + '/';
      for (const path of this.files.keys()) {
        if (path.startsWith(prefix)) {
          throw TraeAPI.FileSystemError.NoPermissions('Directory not empty');
        }
      }
    }

    // Delete file and all children if recursive
    const toDelete = [uri.toString()];
    if (options.recursive) {
      const prefix = uri.toString() + '/';
      for (const path of this.files.keys()) {
        if (path.startsWith(prefix)) {
          toDelete.push(path);
        }
      }
    }

    for (const path of toDelete) {
      this.files.delete(path);
    }

    this.eventEmitter.fire({
      type: TraeAPI.FileChangeType.Deleted,
      uri
    });
  }

  rename(
    oldUri: TraeAPI.Uri,
    newUri: TraeAPI.Uri,
    options: { overwrite: boolean }
  ): void | Thenable<void> {
    const file = this.files.get(oldUri.toString());
    if (!file) {
      throw TraeAPI.FileSystemError.FileNotFound(oldUri);
    }

    const newExists = this.files.has(newUri.toString());
    if (newExists && !options.overwrite) {
      throw TraeAPI.FileSystemError.FileExists(newUri);
    }

    // Move file
    this.files.set(newUri.toString(), file);
    this.files.delete(oldUri.toString());

    // Move children if directory
    if (file.type === TraeAPI.FileType.Directory) {
      const oldPrefix = oldUri.toString() + '/';
      const newPrefix = newUri.toString() + '/';
      
      const toMove: [string, VirtualFile][] = [];
      for (const [path, childFile] of this.files) {
        if (path.startsWith(oldPrefix)) {
          toMove.push([newPrefix + path.substring(oldPrefix.length), childFile]);
        }
      }
      
      for (const [oldPath] of toMove) {
        this.files.delete(oldPath);
      }
      
      for (const [newPath, childFile] of toMove) {
        this.files.set(newPath, childFile);
      }
    }

    this.eventEmitter.fire({
      type: TraeAPI.FileChangeType.Deleted,
      uri: oldUri
    });
    
    this.eventEmitter.fire({
      type: TraeAPI.FileChangeType.Created,
      uri: newUri
    });
  }

  copy?(
    source: TraeAPI.Uri,
    destination: TraeAPI.Uri,
    options: { overwrite: boolean }
  ): void | Thenable<void> {
    const file = this.files.get(source.toString());
    if (!file) {
      throw TraeAPI.FileSystemError.FileNotFound(source);
    }

    const destExists = this.files.has(destination.toString());
    if (destExists && !options.overwrite) {
      throw TraeAPI.FileSystemError.FileExists(destination);
    }

    // Copy file
    const now = new Date();
    this.files.set(destination.toString(), {
      ...file,
      created: now,
      modified: now,
      content: new Uint8Array(file.content) // Deep copy content
    });

    // Copy children if directory
    if (file.type === TraeAPI.FileType.Directory) {
      const sourcePrefix = source.toString() + '/';
      const destPrefix = destination.toString() + '/';
      
      for (const [path, childFile] of this.files) {
        if (path.startsWith(sourcePrefix)) {
          const relativePath = path.substring(sourcePrefix.length);
          this.files.set(destPrefix + relativePath, {
            ...childFile,
            created: now,
            modified: now,
            content: new Uint8Array(childFile.content)
          });
        }
      }
    }

    this.eventEmitter.fire({
      type: TraeAPI.FileChangeType.Created,
      uri: destination
    });
  }
}

// Register custom file provider
const customProvider = new CustomFileProvider('custom');
TraeAPI.workspace.registerFileSystemProvider('custom', customProvider);

Interface Definitions

typescript
// File tree node
interface FileTreeNode {
  uri: TraeAPI.Uri;
  name: string;
  type: TraeAPI.FileType;
  size: number;
  lastModified: number;
  children?: FileTreeNode[];
}

// File watch options
interface FileWatchOptions {
  watchCreate?: boolean;
  watchChange?: boolean;
  watchDelete?: boolean;
  onCreate?(uri: TraeAPI.Uri): void;
  onChange?(uri: TraeAPI.Uri): void;
  onDelete?(uri: TraeAPI.Uri): void;
}

interface DirectoryWatchOptions {
  onCreate?(uri: TraeAPI.Uri): void;
  onChange?(uri: TraeAPI.Uri): void;
  onDelete?(uri: TraeAPI.Uri): void;
}

// Search options
interface SearchOptions {
  include?: TraeAPI.GlobPattern;
  exclude?: TraeAPI.GlobPattern;
  maxResults?: number;
  caseSensitive?: boolean;
  useRegex?: boolean;
}

interface SearchResult {
  uri: TraeAPI.Uri;
  line: number;
  column: number;
  text: string;
  match: TextMatch;
}

interface TextMatch {
  start: number;
  end: number;
  text: string;
}

// File association
interface FileAssociation {
  pattern: string;
  language?: string;
  icon?: string;
  description?: string;
  defaultEditor?: string;
}

// Content type detector
interface ContentTypeDetector {
  detect(uri: TraeAPI.Uri): Promise<string | undefined>;
}

// File provider
interface FileProvider extends TraeAPI.FileSystemProvider {
  // Additional methods can be added here
}

// Virtual file
interface VirtualFile {
  type: TraeAPI.FileType;
  content: Uint8Array;
  created: Date;
  modified: Date;
}

// Cached file
interface CachedFile {
  uri: TraeAPI.Uri;
  content: Uint8Array;
  lastModified: Date;
  size: number;
}

// Cache stats
interface CacheStats {
  entryCount: number;
  totalSize: number;
  averageSize: number;
}

// File operation
interface FileOperation {
  type: 'read' | 'write' | 'copy' | 'move' | 'delete';
  uri?: TraeAPI.Uri;
  source?: TraeAPI.Uri;
  destination?: TraeAPI.Uri;
  content?: Uint8Array;
  overwrite?: boolean;
  recursive?: boolean;
  timestamp: Date;
}

// File event types
interface FileEvent {
  type: 'fileRead' | 'fileReadError' | 'fileWritten' | 'fileWriteError' |
        'fileCopied' | 'fileCopyError' | 'fileMoved' | 'fileMoveError' |
        'fileDeleted' | 'fileDeleteError' | 'fileCreated' | 'fileChanged' |
        'directoryCreated' | 'directoryCreateError' | 'directoryRead' | 'directoryReadError' |
        'fileStat' | 'fileStatError' | 'watcherCreated' | 'watcherDisposed' |
        'filesFound' | 'fileSearchError' | 'searchCompleted' |
        'fileAssociationRegistered' | 'contentTypeDetectorRegistered' |
        'fileOperationExecuted' | 'cacheCleared';
  uri?: TraeAPI.Uri;
  source?: TraeAPI.Uri;
  destination?: TraeAPI.Uri;
  size?: number;
  error?: any;
  stat?: TraeAPI.FileStat;
  pattern?: string;
  count?: number;
  entryCount?: number;
  query?: string;
  resultCount?: number;
  association?: FileAssociation;
  detector?: ContentTypeDetector;
  operation?: FileOperation;
}

API Reference

FileManager

Methods

  • readFile(uri): Promise<Uint8Array> - Read file as bytes
  • readTextFile(uri, encoding?): Promise<string> - Read file as text
  • readJsonFile<T>(uri): Promise<T> - Read and parse JSON file
  • writeFile(uri, content): Promise<void> - Write bytes to file
  • writeTextFile(uri, content, encoding?): Promise<void> - Write text to file
  • writeJsonFile(uri, content, indent?): Promise<void> - Write JSON to file
  • copyFile(source, destination, overwrite?): Promise<void> - Copy file
  • moveFile(source, destination, overwrite?): Promise<void> - Move file
  • deleteFile(uri, recursive?): Promise<void> - Delete file
  • createDirectory(uri): Promise<void> - Create directory
  • readDirectory(uri): Promise<[string, FileType][]> - Read directory entries
  • getDirectoryTree(uri, maxDepth?): Promise<FileTreeNode> - Get directory tree
  • stat(uri): Promise<FileStat> - Get file statistics
  • exists(uri): Promise<boolean> - Check if file exists
  • isFile(uri): Promise<boolean> - Check if path is file
  • isDirectory(uri): Promise<boolean> - Check if path is directory
  • watchFile(uri, options?): Disposable - Watch file for changes
  • watchDirectory(uri, options?): Disposable - Watch directory for changes
  • findFiles(include, exclude?, maxResults?, token?): Promise<Uri[]> - Find files by pattern
  • searchInFiles(query, options?): Promise<SearchResult[]> - Search text in files
  • registerFileAssociation(association): void - Register file association
  • getFileAssociation(uri): FileAssociation | undefined - Get file association
  • registerContentTypeDetector(detector): void - Register content type detector
  • detectContentType(uri): Promise<string> - Detect file content type
  • clearCache(): void - Clear file cache
  • getCacheSize(): number - Get cache entry count
  • getCacheStats(): CacheStats - Get cache statistics

Events

  • onDidChangeFile(listener): Disposable - File system changes

Best Practices

  1. Error Handling: Always handle file system errors gracefully
  2. Performance: Use caching for frequently accessed files
  3. Memory Management: Dispose file watchers when no longer needed
  4. Security: Validate file paths and prevent directory traversal
  5. Encoding: Specify encoding explicitly when working with text files
  6. Large Files: Use streaming for large file operations
  7. Concurrency: Queue file operations to prevent conflicts
  8. Backup: Create backups before destructive operations
  9. Permissions: Check file permissions before operations
  10. Cross-platform: Use path utilities for cross-platform compatibility

Your Ultimate AI-Powered IDE Learning Guide