// ***************************************************************************** // Copyright (C) 2020 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ /** * **IMPORTANT** this code is running in the plugin host process and should be closed as possible to VS Code counterpart: * https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/api/common/extHostFileSystem.ts * One should be able to diff them to see differences. */ /* eslint-disable arrow-body-style */ /* eslint-disable @typescript-eslint/quotes */ /* eslint-disable @typescript-eslint/tslint/config */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Schemes as Schemas, UriComponents } from '../common/uri-components'; import { FileChangeType, FileSystemError, URI } from './types-impl'; import { RPCProtocol } from '../common/rpc-protocol'; import { PLUGIN_RPC_CONTEXT, FileSystemExt, FileSystemMain, IFileChangeDto } from '../common/plugin-api-rpc'; import * as vscode from '@theia/plugin'; import * as files from '@theia/filesystem/lib/common/files'; import * as typeConverter from './type-converters'; import { State, StateMachine, LinkComputer, Edge } from '../common/link-computer'; import { commonPrefixLength } from '@theia/core/lib/common/strings'; import { CharCode } from '@theia/core/lib/common/char-code'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; import { MarkdownString } from '../common/plugin-api-rpc-model'; import { Emitter } from '@theia/core/lib/common'; import { createAPIObject } from './plugin-context'; type IDisposable = vscode.Disposable; export class FsLinkProvider { private _schemes: string[] = []; private _stateMachine?: StateMachine; add(scheme: string): void { this._stateMachine = undefined; this._schemes.push(scheme); } delete(scheme: string): void { const idx = this._schemes.indexOf(scheme); if (idx >= 0) { this._schemes.splice(idx, 1); this._stateMachine = undefined; } } private _initStateMachine(): void { if (!this._stateMachine) { // sort and compute common prefix with previous scheme // then build state transitions based on the data const schemes = this._schemes.sort(); const edges: Edge[] = []; let prevScheme: string | undefined; let prevState: State; let lastState = State.LastKnownState; let nextState = State.LastKnownState; for (const scheme of schemes) { // skip the common prefix of the prev scheme // and continue with its last state let pos = !prevScheme ? 0 : commonPrefixLength(prevScheme, scheme); if (pos === 0) { prevState = State.Start; } else { prevState = nextState; } for (; pos < scheme.length; pos++) { // keep creating new (next) states until the // end (and the BeforeColon-state) is reached if (pos + 1 === scheme.length) { // Save the last state here, because we need to continue for the next scheme lastState = nextState; nextState = State.BeforeColon; } else { nextState += 1; } edges.push([prevState, scheme.toUpperCase().charCodeAt(pos), nextState]); edges.push([prevState, scheme.toLowerCase().charCodeAt(pos), nextState]); prevState = nextState; } prevScheme = scheme; // Restore the last state nextState = lastState; } // all link must match this pattern `:/` edges.push([State.BeforeColon, CharCode.Colon, State.AfterColon]); edges.push([State.AfterColon, CharCode.Slash, State.End]); this._stateMachine = new StateMachine(edges); } } provideDocumentLinks(document: vscode.TextDocument): vscode.ProviderResult { this._initStateMachine(); const result: vscode.DocumentLink[] = []; const links = LinkComputer.computeLinks({ getLineContent(lineNumber: number): string { return document.lineAt(lineNumber - 1).text; }, getLineCount(): number { return document.lineCount; } }, this._stateMachine); for (const link of links) { const docLink = typeConverter.DocumentLink.to(link); if (docLink.target) { result.push(docLink); } } return result; } } class ConsumerFileSystem implements vscode.FileSystem { apiObject: vscode.FileSystem; constructor(private _proxy: FileSystemMain, private _capabilities: Map) { this.apiObject = createAPIObject(this); } stat(uri: vscode.Uri): Promise { return this._proxy.$stat(uri).catch(ConsumerFileSystem._handleError); } readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> { return this._proxy.$readdir(uri).catch(ConsumerFileSystem._handleError); } createDirectory(uri: vscode.Uri): Promise { return this._proxy.$mkdir(uri).catch(ConsumerFileSystem._handleError); } readFile(uri: vscode.Uri): Promise { return this._proxy.$readFile(uri).then(buff => buff.buffer).catch(ConsumerFileSystem._handleError); } writeFile(uri: vscode.Uri, content: Uint8Array): Promise { return this._proxy.$writeFile(uri, BinaryBuffer.wrap(content)).catch(ConsumerFileSystem._handleError); } delete(uri: vscode.Uri, options?: { recursive?: boolean; useTrash?: boolean; }): Promise { return this._proxy.$delete(uri, { ...{ recursive: false, useTrash: false }, ...options }).catch(ConsumerFileSystem._handleError); } rename(oldUri: vscode.Uri, newUri: vscode.Uri, options?: { overwrite?: boolean; }): Promise { return this._proxy.$rename(oldUri, newUri, { ...{ overwrite: false }, ...options }).catch(ConsumerFileSystem._handleError); } copy(source: vscode.Uri, destination: vscode.Uri, options?: { overwrite?: boolean }): Promise { return this._proxy.$copy(source, destination, { ...{ overwrite: false }, ...options }).catch(ConsumerFileSystem._handleError); } isWritableFileSystem(scheme: string): boolean | undefined { const capabilities = this._capabilities.get(scheme); if (typeof capabilities === 'number') { return (capabilities & files.FileSystemProviderCapabilities.Readonly) === 0; } return undefined; } private static _handleError(err: any): never { // generic error if (!(err instanceof Error)) { throw new FileSystemError(String(err)); } // no provider (unknown scheme) error if (err.name === 'ENOPRO') { throw FileSystemError.Unavailable(err.message); } // file system error switch (err.name) { case files.FileSystemProviderErrorCode.FileExists: throw FileSystemError.FileExists(err.message); case files.FileSystemProviderErrorCode.FileNotFound: throw FileSystemError.FileNotFound(err.message); case files.FileSystemProviderErrorCode.FileNotADirectory: throw FileSystemError.FileNotADirectory(err.message); case files.FileSystemProviderErrorCode.FileIsADirectory: throw FileSystemError.FileIsADirectory(err.message); case files.FileSystemProviderErrorCode.NoPermissions: throw FileSystemError.NoPermissions(err.message); case files.FileSystemProviderErrorCode.Unavailable: throw FileSystemError.Unavailable(err.message); default: throw new FileSystemError(err.message, err.name as files.FileSystemProviderErrorCode); } } } export class FileSystemExtImpl implements FileSystemExt { private readonly _proxy: FileSystemMain; private readonly _linkProvider = new FsLinkProvider(); private readonly _fsProvider = new Map(); private readonly _capabilities = new Map(); private readonly _usedSchemes = new Set(); private readonly _watches = new Map(); private readonly onWillRegisterFileSystemProviderEmitter = new Emitter(); readonly onWillRegisterFileSystemProvider = this.onWillRegisterFileSystemProviderEmitter.event; private _handlePool: number = 0; readonly fileSystem: ConsumerFileSystem; constructor(rpc: RPCProtocol) { this._proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.FILE_SYSTEM_MAIN); this.fileSystem = new ConsumerFileSystem(this._proxy, this._capabilities); // register used schemes Object.keys(Schemas).forEach(scheme => this._usedSchemes.add(scheme)); } dispose(): void { this.onWillRegisterFileSystemProviderEmitter.dispose(); } registerFileSystemProvider(scheme: string, provider: vscode.FileSystemProvider, options: { isCaseSensitive?: boolean, isReadonly?: boolean | MarkdownString } = {}) { if (this._usedSchemes.has(scheme)) { throw new Error(`a provider for the scheme '${scheme}' is already registered`); } this.onWillRegisterFileSystemProviderEmitter.fire(this._linkProvider); const handle = this._handlePool++; this._linkProvider.add(scheme); this._usedSchemes.add(scheme); this._fsProvider.set(handle, provider); let capabilities = files.FileSystemProviderCapabilities.FileReadWrite; if (options.isCaseSensitive) { capabilities += files.FileSystemProviderCapabilities.PathCaseSensitive; } if (options.isReadonly) { capabilities += files.FileSystemProviderCapabilities.Readonly; } if (typeof provider.copy === 'function') { capabilities += files.FileSystemProviderCapabilities.FileFolderCopy; } if (typeof provider.open === 'function' && typeof provider.close === 'function' && typeof provider.read === 'function' && typeof provider.write === 'function' ) { capabilities += files.FileSystemProviderCapabilities.FileOpenReadWriteClose; } let readonlyMessage: MarkdownString | undefined; if (options.isReadonly && MarkdownString.is(options.isReadonly)) { readonlyMessage = { value: options.isReadonly.value, isTrusted: options.isReadonly.isTrusted, supportThemeIcons: options.isReadonly.supportThemeIcons, supportHtml: options.isReadonly.supportHtml, baseUri: options.isReadonly.baseUri, uris: options.isReadonly.uris }; } this._proxy.$registerFileSystemProvider(handle, scheme, capabilities, readonlyMessage); const subscription = provider.onDidChangeFile(event => { const mapped: IFileChangeDto[] = []; for (const e of event) { const { uri: resource, type } = e; if (resource.scheme !== scheme) { // dropping events for wrong scheme continue; } let newType: files.FileChangeType | undefined; switch (type) { case FileChangeType.Changed: newType = files.FileChangeType.UPDATED; break; case FileChangeType.Created: newType = files.FileChangeType.ADDED; break; case FileChangeType.Deleted: newType = files.FileChangeType.DELETED; break; default: throw new Error('Unknown FileChangeType'); } mapped.push({ resource, type: newType }); } this._proxy.$onFileSystemChange(handle, mapped); }); return { dispose: () => { subscription.dispose(); this._linkProvider.delete(scheme); this._usedSchemes.delete(scheme); this._fsProvider.delete(handle); this._proxy.$unregisterProvider(handle); } }; } private static _asIStat(stat: vscode.FileStat): files.Stat { const { type, ctime, mtime, size, permissions } = stat; return { type, ctime, mtime, size, permissions }; } $acceptProviderInfos(scheme: string, capabilities?: files.FileSystemProviderCapabilities): void { if (typeof capabilities === 'number') { this._capabilities.set(scheme, capabilities); } else { this._capabilities.delete(scheme); } } $stat(handle: number, resource: UriComponents): Promise { return Promise.resolve(this._getFsProvider(handle).stat(URI.revive(resource))).then(FileSystemExtImpl._asIStat); } $readdir(handle: number, resource: UriComponents): Promise<[string, files.FileType][]> { return Promise.resolve(this._getFsProvider(handle).readDirectory(URI.revive(resource))); } $readFile(handle: number, resource: UriComponents): Promise { return Promise.resolve(this._getFsProvider(handle).readFile(URI.revive(resource))).then(data => BinaryBuffer.wrap(data)); } $writeFile(handle: number, resource: UriComponents, content: BinaryBuffer, opts: files.FileWriteOptions): Promise { return Promise.resolve(this._getFsProvider(handle).writeFile(URI.revive(resource), content.buffer, opts)); } $delete(handle: number, resource: UriComponents, opts: files.FileDeleteOptions): Promise { return Promise.resolve(this._getFsProvider(handle).delete(URI.revive(resource), opts)); } $rename(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOverwriteOptions): Promise { return Promise.resolve(this._getFsProvider(handle).rename(URI.revive(oldUri), URI.revive(newUri), opts)); } $copy(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOverwriteOptions): Promise { const provider = this._getFsProvider(handle); if (!provider.copy) { throw new Error('FileSystemProvider does not implement "copy"'); } return Promise.resolve(provider.copy(URI.revive(oldUri), URI.revive(newUri), opts)); } $mkdir(handle: number, resource: UriComponents): Promise { return Promise.resolve(this._getFsProvider(handle).createDirectory(URI.revive(resource))); } $watch(handle: number, session: number, resource: UriComponents, opts: files.WatchOptions): void { const subscription = this._getFsProvider(handle).watch(URI.revive(resource), opts); this._watches.set(session, subscription); } $unwatch(_handle: number, session: number): void { const subscription = this._watches.get(session); if (subscription) { subscription.dispose(); this._watches.delete(session); } } $open(handle: number, resource: UriComponents, opts: files.FileOpenOptions): Promise { const provider = this._getFsProvider(handle); if (!provider.open) { throw new Error('FileSystemProvider does not implement "open"'); } return Promise.resolve(provider.open(URI.revive(resource), opts)); } $close(handle: number, fd: number): Promise { const provider = this._getFsProvider(handle); if (!provider.close) { throw new Error('FileSystemProvider does not implement "close"'); } return Promise.resolve(provider.close(fd)); } $read(handle: number, fd: number, pos: number, length: number): Promise { const provider = this._getFsProvider(handle); if (!provider.read) { throw new Error('FileSystemProvider does not implement "read"'); } const data = BinaryBuffer.alloc(length); return Promise.resolve(provider.read(fd, pos, data.buffer, 0, length)).then(read => { return data.slice(0, read); // don't send zeros }); } $write(handle: number, fd: number, pos: number, data: BinaryBuffer): Promise { const provider = this._getFsProvider(handle); if (!provider.write) { throw new Error('FileSystemProvider does not implement "write"'); } return Promise.resolve(provider.write(fd, pos, data.buffer, 0, data.byteLength)); } private _getFsProvider(handle: number): vscode.FileSystemProvider { const provider = this._fsProvider.get(handle); if (!provider) { const err = new Error(); err.name = 'ENOPRO'; err.message = `no provider`; throw err; } return provider; } }