/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import { CancellationToken, Disposable, Event, EventEmitter, FileChangeEvent, FileChangeType, FileSearchOptions, FileSearchProvider, FileSearchQuery, FileStat, FileSystemError, FileSystemProvider, FileType, Progress, TextSearchOptions, TextSearchProvider, TextSearchQuery, TextSearchResult, Uri, workspace, } from 'vscode'; import { IWritableChangeStore } from './changeStore'; import { ContextStore } from './contextStore'; import { GitHubApiContext } from './github/api'; const emptyDisposable = { dispose: () => { /* noop */ } }; const textEncoder = new TextEncoder(); export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSearchProvider, Disposable { private _onDidChangeFile = new EventEmitter(); get onDidChangeFile(): Event { return this._onDidChangeFile.event; } private readonly disposable: Disposable; constructor( readonly scheme: string, private readonly originalScheme: string, contextStore: ContextStore, private readonly changeStore: IWritableChangeStore, private readonly fs: FileSystemProvider & FileSearchProvider & TextSearchProvider ) { // TODO@eamodio listen for workspace folder changes for (const folder of workspace.workspaceFolders ?? []) { const uri = this.getOriginalResource(folder.uri); // If we have a saved context, but no longer have any changes, reset the context // We only do this on startup/reload to keep things consistent if (contextStore.get(uri) !== undefined && !changeStore.hasChanges(folder.uri)) { contextStore.delete(uri); } } this.disposable = Disposable.from( workspace.registerFileSystemProvider(scheme, this, { isCaseSensitive: true, }), workspace.registerFileSearchProvider(scheme, this), workspace.registerTextSearchProvider(scheme, this), changeStore.onDidChange(e => { switch (e.type) { case 'created': this._onDidChangeFile.fire([{ type: FileChangeType.Created, uri: e.uri }]); break; case 'changed': this._onDidChangeFile.fire([{ type: FileChangeType.Changed, uri: e.uri }]); break; case 'deleted': this._onDidChangeFile.fire([{ type: FileChangeType.Deleted, uri: e.uri }]); break; } }), ); } dispose() { this.disposable?.dispose(); } private getOriginalResource(uri: Uri): Uri { return uri.with({ scheme: this.originalScheme }); } private getVirtualResource(uri: Uri): Uri { return uri.with({ scheme: this.scheme }); } //#region FileSystemProvider watch(): Disposable { return emptyDisposable; } async stat(uri: Uri): Promise { let stat = this.changeStore.getStat(uri); if (stat !== undefined) { return stat; } stat = await this.fs.stat(this.getOriginalResource(uri)); return stat; } async readDirectory(uri: Uri): Promise<[string, FileType][]> { let entries = await this.fs.readDirectory(this.getOriginalResource(uri)); entries = this.changeStore.updateDirectoryEntries(uri, entries); return entries; } createDirectory(_uri: Uri): void | Thenable { // TODO@eamodio only support files for now throw FileSystemError.NoPermissions(); } async readFile(uri: Uri): Promise { const content = this.changeStore.getContent(uri); if (content !== undefined) { return textEncoder.encode(content); } const data = await this.fs.readFile(this.getOriginalResource(uri)); return data; } async writeFile(uri: Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): Promise { let stat; try { stat = await this.stat(uri); if (!options.overwrite) { throw FileSystemError.FileExists(); } } catch (ex) { if (ex instanceof FileSystemError && ex.code === 'FileNotFound') { if (!options.create) { throw FileSystemError.FileNotFound(); } } else { throw ex; } } if (stat === undefined) { await this.changeStore.onFileCreated(uri, content); } else { await this.changeStore.onFileChanged(uri, content, () => this.fs.readFile(this.getOriginalResource(uri))); } } async delete(uri: Uri, _options: { recursive: boolean }): Promise { const stat = await this.stat(uri); if (stat.type !== FileType.File) { throw FileSystemError.NoPermissions(); } await this.changeStore.onFileDeleted(uri); } async rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }): Promise { const stat = await this.stat(oldUri); // TODO@eamodio only support files for now if (stat.type !== FileType.File) { throw FileSystemError.NoPermissions(); } const content = await this.readFile(oldUri); await this.writeFile(newUri, content, { create: true, overwrite: options.overwrite }); await this.delete(oldUri, { recursive: false }); } async copy(source: Uri, destination: Uri, options: { overwrite: boolean }): Promise { const stat = await this.stat(source); // TODO@eamodio only support files for now if (stat.type !== FileType.File) { throw FileSystemError.NoPermissions(); } const content = await this.readFile(source); await this.writeFile(destination, content, { create: true, overwrite: options.overwrite }); } //#endregion //#region FileSearchProvider provideFileSearchResults( query: FileSearchQuery, options: FileSearchOptions, token: CancellationToken, ) { return this.fs.provideFileSearchResults(query, { ...options, folder: this.getOriginalResource(options.folder) }, token); } //#endregion //#region TextSearchProvider provideTextSearchResults( query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken, ) { return this.fs.provideTextSearchResults( query, { ...options, folder: this.getOriginalResource(options.folder) }, { report: (result: TextSearchResult) => progress.report({ ...result, uri: this.getVirtualResource(result.uri) }) }, token ); } //#endregion }