diff --git a/CHANGELOG.md b/CHANGELOG.md index 89eeeaa6e75cd..b481e3f85e490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## v1.2.0 +- [plugin] support all vscode.workspace.fs APIs for registered fs providers only (not yet real file system) - [application-manager] enabled clients to add `windowOptions` using an IPC-Event [#7803](https://github.com/eclipse-theia/theia/pull/7803) - [application-package] enabled client to change default `windowOptions` [#7803](https://github.com/eclipse-theia/theia/pull/7803) diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 90b4582b2285f..71424bb40387d 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -30,7 +30,7 @@ import { QuickInputButton } from '../plugin/types-impl'; import { UriComponents } from './uri-components'; -import { ConfigurationTarget } from '../plugin/types-impl'; +import { ConfigurationTarget, FileType, FileStat } from '../plugin/types-impl'; import { SerializedDocumentFilter, CompletionContext, @@ -1347,13 +1347,25 @@ export interface DebugMain { } export interface FileSystemExt { + $stat(handle: number, resource: UriComponents): Promise; + $readDirectory(handle: number, resource: UriComponents): Promise<[string, FileType][]>; + $createDirectory(handle: number, uri: UriComponents): Promise; $readFile(handle: number, resource: UriComponents, options?: { encoding?: string }): Promise; $writeFile(handle: number, resource: UriComponents, content: string, options?: { encoding?: string }): Promise; + $delete(handle: number, resource: UriComponents, options: { recursive: boolean }): Promise; + $rename(handle: number, source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise; + $copy(handle: number, source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise; } export interface FileSystemMain { + $stat(uri: UriComponents): Promise + $readDirectory(uri: UriComponents): Promise<[string, FileType][]>; + $createDirectory(uri: UriComponents): Promise $readFile(uri: UriComponents): Promise; $writeFile(uri: UriComponents, content: string): Promise; + $delete(uri: UriComponents, options: { recursive: boolean }): Promise; + $rename(source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise; + $copy(source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise; $registerFileSystemProvider(handle: number, scheme: string): void; $unregisterProvider(handle: number): void; } diff --git a/packages/plugin-ext/src/main/browser/file-system-main.ts b/packages/plugin-ext/src/main/browser/file-system-main.ts index 4c61a283414b7..42f6e1dc9cb31 100644 --- a/packages/plugin-ext/src/main/browser/file-system-main.ts +++ b/packages/plugin-ext/src/main/browser/file-system-main.ts @@ -22,6 +22,7 @@ import URI from '@theia/core/lib/common/uri'; import { MAIN_RPC_CONTEXT, FileSystemMain, FileSystemExt } from '../../common/plugin-api-rpc'; import { RPCProtocol } from '../../common/rpc-protocol'; import { UriComponents } from '../../common/uri-components'; +import { FileStat, FileType } from '../../plugin/types-impl'; export class FileSystemMainImpl implements FileSystemMain, Disposable { @@ -29,6 +30,7 @@ export class FileSystemMainImpl implements FileSystemMain, Disposable { private readonly resourceResolver: FSResourceResolver; private readonly resourceProvider: ResourceProvider; private readonly providers = new Map(); + private readonly providersBySchema = new Map(); private readonly toDispose = new DisposableCollection(); constructor(rpc: RPCProtocol, container: interfaces.Container) { @@ -44,9 +46,16 @@ export class FileSystemMainImpl implements FileSystemMain, Disposable { async $registerFileSystemProvider(handle: number, scheme: string): Promise { const toDispose = new DisposableCollection( this.resourceResolver.registerResourceProvider(handle, scheme, this.proxy), - Disposable.create(() => this.providers.delete(handle)) + Disposable.create(() => { + this.providers.delete(handle); + this.providersBySchema.delete(scheme); + }) ); this.providers.set(handle, toDispose); + if (this.providersBySchema.has(scheme)) { + throw new Error(`Resource Provider for scheme '${scheme}' is already registered`); + } + this.providersBySchema.set(scheme, handle); this.toDispose.push(toDispose); } @@ -57,6 +66,35 @@ export class FileSystemMainImpl implements FileSystemMain, Disposable { } } + private getHandle(uri: UriComponents): number { + const handle = this.providersBySchema.get(uri.scheme); + if (handle === undefined) { + throw new Error(`'No available file system provider for schema ${uri.scheme}`); + } + return handle; + } + + // currently only support registered file system providers (and not real file system) + async $stat(uriComponents: UriComponents): Promise { + const uri = Uri.revive(uriComponents); + const handle = this.getHandle(uri); + return this.proxy.$stat(handle, uri); + } + + // currently only support registered file system providers (and not real file system) + async $readDirectory(uriComponents: UriComponents): Promise<[string, FileType][]> { + const uri = Uri.revive(uriComponents); + const handle = this.getHandle(uri); + return this.proxy.$readDirectory(handle, uri); + } + + // currently only support registered file system providers (and not real file system) + async $createDirectory(uriComponents: UriComponents): Promise { + const uri = Uri.revive(uriComponents); + const handle = this.getHandle(uri); + return this.proxy.$createDirectory(handle, uri); + } + async $readFile(uriComponents: UriComponents): Promise { const uri = Uri.revive(uriComponents); const resource = await this.resourceProvider(new URI(uri)); @@ -72,6 +110,34 @@ export class FileSystemMainImpl implements FileSystemMain, Disposable { return resource.saveContents(content); } + // currently only support registered file system providers (and not real file system) + async $delete(uriComponents: UriComponents, options: { recursive: boolean }): Promise { + const uri = Uri.revive(uriComponents); + const handle = this.getHandle(uri); + return this.proxy.$delete(handle, uri, options); + } + + // currently only support registered file system providers (and not real file system) + async $rename(source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise { + const sourceUri = Uri.revive(source); + const targetUri = Uri.revive(target); + const sourceHandle = this.getHandle(sourceUri); + if (sourceHandle !== this.getHandle(targetUri)) { + throw new Error(`'No matching file system provider for ${sourceUri} and ${targetUri}`); + } + return this.proxy.$rename(sourceHandle, sourceUri, targetUri, options); + } + + // currently only support registered file system providers (and not real file system) + async $copy(source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise { + const sourceUri = Uri.revive(source); + const targetUri = Uri.revive(target); + const sourceHandle = this.getHandle(sourceUri); + if (sourceHandle !== this.getHandle(targetUri)) { + throw new Error(`'No matching file system provider for ${sourceUri} and ${targetUri}`); + } + return this.proxy.$copy(sourceHandle, sourceUri, targetUri, options); + } } @injectable() diff --git a/packages/plugin-ext/src/plugin/file-system.ts b/packages/plugin-ext/src/plugin/file-system.ts index fa6a73b3f5a52..6cfdcb9a63444 100644 --- a/packages/plugin-ext/src/plugin/file-system.ts +++ b/packages/plugin-ext/src/plugin/file-system.ts @@ -19,7 +19,7 @@ import * as theia from '@theia/plugin'; import { PLUGIN_RPC_CONTEXT, FileSystemExt, FileSystemMain } from '../common/plugin-api-rpc'; import { RPCProtocol } from '../common/rpc-protocol'; import { UriComponents, Schemes } from '../common/uri-components'; -import { Disposable } from './types-impl'; +import { Disposable, FileStat, FileType } from './types-impl'; import { InPluginFileSystemProxy } from './in-plugin-filesystem-proxy'; export class FileSystemExtImpl implements FileSystemExt { @@ -27,7 +27,7 @@ export class FileSystemExtImpl implements FileSystemExt { private readonly proxy: FileSystemMain; private readonly usedSchemes = new Set(); private readonly fsProviders = new Map(); - private fileSystem: InPluginFileSystemProxy; + private readonly fileSystem: InPluginFileSystemProxy; private handlePool: number = 0; @@ -68,21 +68,41 @@ export class FileSystemExtImpl implements FileSystemExt { }); } - private checkProviderExists(handle: number): void { - if (!this.fsProviders.has(handle)) { + private safeGetProvider(handle: number): theia.FileSystemProvider { + const provider = this.fsProviders.get(handle); + if (!provider) { const err = new Error(); err.name = 'ENOPRO'; err.message = 'no provider'; throw err; } + return provider; } // forwarding calls + $stat(handle: number, resource: UriComponents): Promise { + const fileSystemProvider = this.safeGetProvider(handle); + const uri = URI.revive(resource); + return Promise.resolve(fileSystemProvider.stat(uri)); + } + + $readDirectory(handle: number, resource: UriComponents): Promise<[string, FileType][]> { + const fileSystemProvider = this.safeGetProvider(handle); + const uri = URI.revive(resource); + return Promise.resolve(fileSystemProvider.readDirectory(uri)); + } + + $createDirectory(handle: number, resource: UriComponents): Promise { + const fileSystemProvider = this.safeGetProvider(handle); + const uri = URI.revive(resource); + return Promise.resolve(fileSystemProvider.createDirectory(uri)); + } + $readFile(handle: number, resource: UriComponents, options?: { encoding?: string }): Promise { - this.checkProviderExists(handle); + const fileSystemProvider = this.safeGetProvider(handle); - return Promise.resolve(this.fsProviders.get(handle)!.readFile(URI.revive(resource))).then(data => { + return Promise.resolve(fileSystemProvider.readFile(URI.revive(resource))).then(data => { const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength); const encoding = options === null ? undefined : options && options.encoding; return buffer.toString(encoding); @@ -91,11 +111,32 @@ export class FileSystemExtImpl implements FileSystemExt { } $writeFile(handle: number, resource: UriComponents, content: string, options?: { encoding?: string }): Promise { - this.checkProviderExists(handle); + const fileSystemProvider = this.safeGetProvider(handle); const uri = URI.revive(resource); const encoding = options === null ? undefined : options && options.encoding; const buffer = Buffer.from(content, encoding); const opts = { create: true, overwrite: true }; - return Promise.resolve(this.fsProviders.get(handle)!.writeFile(uri, buffer, opts)); + return Promise.resolve(fileSystemProvider.writeFile(uri, buffer, opts)); } + + $delete(handle: number, resource: UriComponents, options: { recursive: boolean }): Promise { + const fileSystemProvider = this.safeGetProvider(handle); + const uri = URI.revive(resource); + return Promise.resolve(fileSystemProvider.delete(uri, options)); + } + + $rename(handle: number, source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise { + const fileSystemProvider = this.safeGetProvider(handle); + const sourceUri = URI.revive(source); + const targetUri = URI.revive(target); + return Promise.resolve(fileSystemProvider.rename(sourceUri, targetUri, options)); + } + + $copy(handle: number, source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise { + const fileSystemProvider = this.safeGetProvider(handle); + const sourceUri = URI.revive(source); + const targetUri = URI.revive(target); + return Promise.resolve(fileSystemProvider.copy && fileSystemProvider.copy(sourceUri, targetUri, options)); + } + } diff --git a/packages/plugin-ext/src/plugin/in-plugin-filesystem-proxy.ts b/packages/plugin-ext/src/plugin/in-plugin-filesystem-proxy.ts index 3ca074a09acce..8293ac146edcd 100644 --- a/packages/plugin-ext/src/plugin/in-plugin-filesystem-proxy.ts +++ b/packages/plugin-ext/src/plugin/in-plugin-filesystem-proxy.ts @@ -18,7 +18,8 @@ import * as theia from '@theia/plugin'; import { TextEncoder, TextDecoder } from 'util'; import { FileSystemMain } from '../common/plugin-api-rpc'; import { UriComponents } from '../common/uri-components'; -import { FileSystemError } from './types-impl'; +import { FileSystemError, FileType } from './types-impl'; +import { FileStat, Uri } from '@theia/plugin'; /** * This class is managing FileSystem proxy @@ -31,6 +32,28 @@ export class InPluginFileSystemProxy implements theia.FileSystem { this.proxy = proxy; } + async stat(uri: Uri): Promise { + try { + return this.proxy.$stat(uri); + } catch (error) { + throw this.handleError(error); + } + } + async readDirectory(uri: UriComponents): Promise<[string, FileType][]> { + try { + return this.proxy.$readDirectory(uri); + } catch (error) { + throw this.handleError(error); + } + } + async createDirectory(uri: Uri): Promise { + try { + return this.proxy.$createDirectory(uri); + } catch (error) { + throw this.handleError(error); + } + + } async readFile(uri: UriComponents): Promise { try { const val = await this.proxy.$readFile(uri); @@ -43,7 +66,28 @@ export class InPluginFileSystemProxy implements theia.FileSystem { const encoded = new TextDecoder().decode(content); try { - await this.proxy.$writeFile(uri, encoded); + return this.proxy.$writeFile(uri, encoded); + } catch (error) { + throw this.handleError(error); + } + } + async delete(uri: Uri, options?: { recursive?: boolean, useTrash?: boolean }): Promise { + try { + return this.proxy.$delete(uri, { ...{ recursive: false }, ...options }); + } catch (error) { + throw this.handleError(error); + } + } + async rename(source: Uri, target: Uri, options?: { overwrite?: boolean }): Promise { + try { + return this.proxy.$rename(source, target, { ...{ overwrite: false }, ...options }); + } catch (error) { + throw this.handleError(error); + } + } + async copy(source: Uri, target: Uri, options?: { overwrite?: boolean }): Promise { + try { + return this.proxy.$copy(source, target, { ...{ overwrite: false }, ...options }); } catch (error) { throw this.handleError(error); } diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index e754bea4874e1..044ed3785eddd 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -1328,6 +1328,13 @@ export enum FileType { SymbolicLink = 64 } +export interface FileStat { + readonly type: FileType; + readonly ctime: number; + readonly mtime: number; + readonly size: number; +} + export class ProgressOptions { /** * The location at which progress should show. diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 03d5f98e3bf13..c77dd59b95c95 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -4527,6 +4527,32 @@ declare module '@theia/plugin' { */ export interface FileSystem { + /** + * Retrieve metadata about a file. + * + * @param uri The uri of the file to retrieve metadata about. + * @return The file metadata about the file. + */ + stat(uri: Uri): PromiseLike; + + /** + * Retrieve all entries of a [directory](#FileType.Directory). + * + * @param uri The uri of the folder. + * @return An array of name/type-tuples or a PromiseLike that resolves to such. + */ + readDirectory(uri: Uri): PromiseLike<[string, FileType][]>; + + /** + * Create a new directory (Note, that new files are created via `write`-calls). + * + * *Note* that missing directories are created automatically, e.g this call has + * `mkdirp` semantics. + * + * @param uri The uri of the new folder. + */ + createDirectory(uri: Uri): PromiseLike; + /** * Read the entire contents of a file. * @@ -4542,6 +4568,32 @@ declare module '@theia/plugin' { * @param content The new content of the file. */ writeFile(uri: Uri, content: Uint8Array): PromiseLike; + + /** + * Delete a file. + * + * @param uri The resource that is to be deleted. + * @param options Defines if trash can should be used and if deletion of folders is recursive + */ + delete(uri: Uri, options?: { recursive?: boolean, useTrash?: boolean }): PromiseLike; + + /** + * Rename a file or folder. + * + * @param source The existing file. + * @param target The new location. + * @param options Defines if existing files should be overwritten. + */ + rename(source: Uri, target: Uri, options?: { overwrite?: boolean }): PromiseLike; + + /** + * Copy files or folders. + * + * @param source The existing file. + * @param target The destination location. + * @param options Defines if existing files should be overwritten. + */ + copy(source: Uri, target: Uri, options?: { overwrite?: boolean }): PromiseLike; } /**