From 31632178a156904eb478259c8bb4dabee597b54d Mon Sep 17 00:00:00 2001 From: Remi Schnekenburger Date: Thu, 15 Feb 2024 11:27:15 +0100 Subject: [PATCH] Better Support for ReadOnly message on editors Also implements VS Code api for readOnly messages on FileSystemProvider fixes #13353 contributed on behalf of STMicroelectronics Signed-off-by: Remi Schnekenburger --- CHANGELOG.md | 2 +- .../sample-file-system-capabilities.ts | 20 +++++++++ .../filesystem/src/browser/file-resource.ts | 41 +++++++++++++------ .../filesystem/src/browser/file-service.ts | 35 +++++++++++++++- packages/filesystem/src/common/files.ts | 13 ++++++ .../src/common/remote-file-system-provider.ts | 40 +++++++++++++++++- .../src/browser/monaco-editor-provider.ts | 9 ++++ packages/monaco/src/browser/monaco-editor.ts | 2 +- .../plugin-ext/src/common/plugin-api-rpc.ts | 2 +- .../src/main/browser/file-system-main-impl.ts | 14 ++++--- .../src/plugin/file-system-ext-impl.ts | 17 +++++++- .../plugin-ext/src/plugin/plugin-context.ts | 3 +- packages/plugin/src/theia.d.ts | 2 +- 13 files changed, 172 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe038a8dfe136..d7e9dbbb11da5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ ## v1.47.0 not yet released -- [component] add here +- [filesystem] Implement readonly markdown message for file system providers [#13414](() [Breaking Changes:](#breaking_changes_not_yet_released) - [monaco] Upgrade Monaco dependency to 1.83.1 [#13217](https://github.com/eclipse-theia/theia/pull/13217)- contributed on behalf of STMicroelectronics\ diff --git a/examples/api-samples/src/browser/file-system/sample-file-system-capabilities.ts b/examples/api-samples/src/browser/file-system/sample-file-system-capabilities.ts index e5ebf2e8418ea..2bf083a200a1a 100644 --- a/examples/api-samples/src/browser/file-system/sample-file-system-capabilities.ts +++ b/examples/api-samples/src/browser/file-system/sample-file-system-capabilities.ts @@ -18,6 +18,7 @@ import { CommandContribution, CommandRegistry } from '@theia/core'; import { inject, injectable, interfaces } from '@theia/core/shared/inversify'; import { RemoteFileSystemProvider } from '@theia/filesystem/lib/common/remote-file-system-provider'; import { FileSystemProviderCapabilities } from '@theia/filesystem/lib/common/files'; +import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering'; @injectable() export class SampleFileSystemCapabilities implements CommandContribution { @@ -39,6 +40,25 @@ export class SampleFileSystemCapabilities implements CommandContribution { } } }); + + commands.registerCommand({ + id: 'addFileSystemReadonlyMessage', + label: 'Add a File System ReadonlyMessage for readonly' + }, { + execute: () => { + const readonlyMessage = new MarkdownStringImpl(`Added new **Markdown** string '+${Date.now()}`); + this.remoteFileSystemProvider['setReadOnlyMessage'](readonlyMessage); + } + }); + + commands.registerCommand({ + id: 'removeFileSystemReadonlyMessage', + label: 'Remove File System ReadonlyMessage for readonly' + }, { + execute: () => { + this.remoteFileSystemProvider['setReadOnlyMessage'](undefined); + } + }); } } diff --git a/packages/filesystem/src/browser/file-resource.ts b/packages/filesystem/src/browser/file-resource.ts index bc8c69387d449..29f53d9463fc4 100644 --- a/packages/filesystem/src/browser/file-resource.ts +++ b/packages/filesystem/src/browser/file-resource.ts @@ -41,7 +41,7 @@ export namespace FileResourceVersion { } export interface FileResourceOptions { - isReadonly: boolean + readOnly: boolean | MarkdownString shouldOverwrite: () => Promise shouldOpenAsText: (error: string) => Promise } @@ -65,8 +65,8 @@ export class FileResource implements Resource { get encoding(): string | undefined { return this._version?.encoding; } - get readOnly(): boolean { - return this.options.isReadonly || this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Readonly); + get readOnly(): boolean | MarkdownString { + return this.options.readOnly; } constructor( @@ -92,11 +92,30 @@ export class FileResource implements Resource { console.error(e); } this.updateSavingContentChanges(); - this.toDispose.push(this.fileService.onDidChangeFileSystemProviderCapabilities(e => { + this.toDispose.push(this.fileService.onDidChangeFileSystemProviderCapabilities(async e => { if (e.scheme === this.uri.scheme) { - this.updateSavingContentChanges(); + this.updateReadOnly(); } })); + this.fileService.onDidChangeFileSystemProviderReadOnlyMessage(async e => { + if (e.scheme === this.uri.scheme) { + this.updateReadOnly(); + } + }); + } + + protected async updateReadOnly(): Promise { + const oldReadOnly = this.options.readOnly; + const readOnlyMessage = this.fileService.readOnlyMessage(this.uri); + if (readOnlyMessage) { + this.options.readOnly = readOnlyMessage; + } else { + this.options.readOnly = await this.fileService.isReadOnly(this.uri) || this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Readonly); + } + if (this.options.readOnly !== oldReadOnly) { + this.updateSavingContentChanges(); + this.onDidChangeReadOnlyEmitter.fire(this.options.readOnly); + } } dispose(): void { @@ -225,24 +244,17 @@ export class FileResource implements Resource { saveContents?: Resource['saveContents']; saveContentChanges?: Resource['saveContentChanges']; protected updateSavingContentChanges(): void { - let changed = false; if (this.readOnly) { - changed = Boolean(this.saveContents); delete this.saveContentChanges; delete this.saveContents; delete this.saveStream; } else { - changed = !Boolean(this.saveContents); this.saveContents = this.doWrite; this.saveStream = this.doWrite; if (this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Update)) { this.saveContentChanges = this.doSaveContentChanges; } } - if (changed) { - // Only actually bother to call the event if the value has changed. - this.onDidChangeReadOnlyEmitter.fire(this.readOnly); - } } protected doSaveContentChanges: Resource['saveContentChanges'] = async (changes, options) => { const version = options?.version || this._version; @@ -332,8 +344,11 @@ export class FileResourceResolver implements ResourceResolver { if (stat && stat.isDirectory) { throw new Error('The given uri is a directory: ' + this.labelProvider.getLongName(uri)); } + + const readOnlyMessage = this.fileService.readOnlyMessage(uri); + return new FileResource(uri, this.fileService, { - isReadonly: stat?.isReadonly ?? false, + readOnly: readOnlyMessage ?? stat?.isReadonly ?? false, shouldOverwrite: () => this.shouldOverwrite(uri), shouldOpenAsText: error => this.shouldOpenAsText(uri, error) }); diff --git a/packages/filesystem/src/browser/file-service.ts b/packages/filesystem/src/browser/file-service.ts index 31eb03f9084f1..79b766fd22204 100644 --- a/packages/filesystem/src/browser/file-service.ts +++ b/packages/filesystem/src/browser/file-service.ts @@ -51,7 +51,7 @@ import { toFileOperationResult, toFileSystemProviderErrorCode, ResolveFileResult, ResolveFileResultWithMetadata, MoveFileOptions, CopyFileOptions, BaseStatWithMetadata, FileDeleteOptions, FileOperationOptions, hasAccessCapability, hasUpdateCapability, - hasFileReadStreamCapability, FileSystemProviderWithFileReadStreamCapability + hasFileReadStreamCapability, FileSystemProviderWithFileReadStreamCapability, ReadOnlyMessageFileSystemProvider } from '../common/files'; import { BinaryBuffer, BinaryBufferReadable, BinaryBufferReadableStream, BinaryBufferReadableBufferedStream, BinaryBufferWriteableStream } from '@theia/core/lib/common/buffer'; import { ReadableStream, isReadableStream, isReadableBufferedStream, transform, consumeStream, peekStream, peekReadable, Readable } from '@theia/core/lib/common/stream'; @@ -68,6 +68,7 @@ import { readFileIntoStream } from '../common/io'; import { FileSystemWatcherErrorHandler } from './filesystem-watcher-error-handler'; import { FileSystemUtils } from '../common/filesystem-utils'; import { nls } from '@theia/core'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; export interface FileOperationParticipant { @@ -235,6 +236,15 @@ export interface FileSystemProviderCapabilitiesChangeEvent { scheme: string; } +export interface FileSystemProviderReadOnlyMessageChangeEvent { + /** The affected file system provider for which this event was fired. */ + provider: FileSystemProvider; + /** The uri for which the provider is registered */ + scheme: string; + /** The new read only message */ + message: MarkdownString | undefined; +} + /** * Represents the `FileSystemProviderActivation` event. * This event is fired by the {@link FileService} if it wants to activate the @@ -342,6 +352,9 @@ export class FileService { private onDidChangeFileSystemProviderCapabilitiesEmitter = new Emitter(); readonly onDidChangeFileSystemProviderCapabilities = this.onDidChangeFileSystemProviderCapabilitiesEmitter.event; + private onDidChangeFileSystemProviderReadOnlyMessageEmitter = new Emitter(); + readonly onDidChangeFileSystemProviderReadOnlyMessage = this.onDidChangeFileSystemProviderReadOnlyMessageEmitter.event; + private readonly providers = new Map(); private readonly activations = new Map>(); @@ -364,6 +377,9 @@ export class FileService { providerDisposables.push(provider.onDidChangeFile(changes => this.onDidFilesChangeEmitter.fire(new FileChangesEvent(changes)))); providerDisposables.push(provider.onFileWatchError(() => this.handleFileWatchError())); providerDisposables.push(provider.onDidChangeCapabilities(() => this.onDidChangeFileSystemProviderCapabilitiesEmitter.fire({ provider, scheme }))); + if(ReadOnlyMessageFileSystemProvider.is(provider)) { + providerDisposables.push(provider.onDidChangeReadOnlyMessage(message => this.onDidChangeFileSystemProviderReadOnlyMessageEmitter.fire( { provider, scheme, message}))); + } return Disposable.create(() => { this.onDidChangeFileSystemProviderRegistrationsEmitter.fire({ added: false, scheme, provider }); @@ -413,6 +429,23 @@ export class FileService { return this.providers.has(resource.scheme); } + readOnlyMessage(resource: URI): MarkdownString | undefined { + const provider = this.providers.get(resource.scheme); + if(ReadOnlyMessageFileSystemProvider.is(provider)) { + return provider.readOnlyMessage; + } + return undefined; + } + + async isReadOnly(resource: URI): Promise { + const exists = await this.exists(resource); + if(exists) { + const stat = await this.resolve(resource); + return stat?.isReadonly ?? false; + } + return false; + } + /** * Tests if the service (i.e the {@link FileSystemProvider} registered for the given uri scheme) provides the given capability. * @param resource `URI` of the resource to test. diff --git a/packages/filesystem/src/common/files.ts b/packages/filesystem/src/common/files.ts index 865a680c0b113..1f706a76f39ae 100644 --- a/packages/filesystem/src/common/files.ts +++ b/packages/filesystem/src/common/files.ts @@ -27,6 +27,7 @@ import type { TextDocumentContentChangeEvent } from '@theia/core/shared/vscode-l import { ReadableStreamEvents } from '@theia/core/lib/common/stream'; import { CancellationToken } from '@theia/core/lib/common/cancellation'; import { isObject } from '@theia/core/lib/common'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; export const enum FileOperation { CREATE, @@ -765,6 +766,18 @@ export function hasUpdateCapability(provider: FileSystemProvider): provider is F return !!(provider.capabilities & FileSystemProviderCapabilities.Update); } +export interface ReadOnlyMessageFileSystemProvider { + readOnlyMessage: MarkdownString | undefined; + readonly onDidChangeReadOnlyMessage: Event; +} + +export namespace ReadOnlyMessageFileSystemProvider { + export function is(arg: unknown): arg is ReadOnlyMessageFileSystemProvider { + return isObject(arg) + && 'readOnlyMessage' in arg; + } +} + /** * Subtype of {@link FileSystemProvider} that ensures that the optional functions, needed for providers * that should be able to read & write files, are implemented. diff --git a/packages/filesystem/src/common/remote-file-system-provider.ts b/packages/filesystem/src/common/remote-file-system-provider.ts index 62a60cb6d0ab2..f69ebdd3fe870 100644 --- a/packages/filesystem/src/common/remote-file-system-provider.ts +++ b/packages/filesystem/src/common/remote-file-system-provider.ts @@ -23,7 +23,8 @@ import { FileWriteOptions, FileOpenOptions, FileChangeType, FileSystemProviderCapabilities, FileChange, Stat, FileOverwriteOptions, WatchOptions, FileType, FileSystemProvider, FileDeleteOptions, hasOpenReadWriteCloseCapability, hasFileFolderCopyCapability, hasReadWriteCapability, hasAccessCapability, - FileSystemProviderError, FileSystemProviderErrorCode, FileUpdateOptions, hasUpdateCapability, FileUpdateResult, FileReadStreamOptions, hasFileReadStreamCapability + FileSystemProviderError, FileSystemProviderErrorCode, FileUpdateOptions, hasUpdateCapability, FileUpdateResult, FileReadStreamOptions, hasFileReadStreamCapability, + ReadOnlyMessageFileSystemProvider } from './files'; import { RpcServer, RpcProxy, RpcProxyFactory } from '@theia/core/lib/common/messaging/proxy-factory'; import { ApplicationError } from '@theia/core/lib/common/application-error'; @@ -31,6 +32,7 @@ import { Deferred } from '@theia/core/lib/common/promise-util'; import type { TextDocumentContentChangeEvent } from '@theia/core/shared/vscode-languageserver-protocol'; import { newWriteableStream, ReadableStreamEvents } from '@theia/core/lib/common/stream'; import { CancellationToken, cancelled } from '@theia/core/lib/common/cancellation'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; export const remoteFileSystemPath = '/services/remote-filesystem'; @@ -38,6 +40,7 @@ export const RemoteFileSystemServer = Symbol('RemoteFileSystemServer'); export interface RemoteFileSystemServer extends RpcServer { getCapabilities(): Promise stat(resource: string): Promise; + getReadOnlyMessage(): Promise; access(resource: string, mode?: number): Promise; fsPath(resource: string): Promise; open(resource: string, opts: FileOpenOptions): Promise; @@ -70,6 +73,7 @@ export interface RemoteFileSystemClient { notifyDidChangeFile(event: { changes: RemoteFileChange[] }): void; notifyFileWatchError(): void; notifyDidChangeCapabilities(capabilities: FileSystemProviderCapabilities): void; + notifyDidChangeReadOnlyMessage(readOnlyMessage: MarkdownString | undefined): void; onFileStreamData(handle: number, data: Uint8Array): void; onFileStreamEnd(handle: number, error: RemoteFileStreamError | undefined): void; } @@ -109,7 +113,7 @@ export class RemoteFileSystemProxyFactory extends RpcProxyFact * Wraps the remote filesystem provider living on the backend. */ @injectable() -export class RemoteFileSystemProvider implements Required, Disposable { +export class RemoteFileSystemProvider implements Required, Disposable, ReadOnlyMessageFileSystemProvider { private readonly onDidChangeFileEmitter = new Emitter(); readonly onDidChangeFile = this.onDidChangeFileEmitter.event; @@ -120,6 +124,9 @@ export class RemoteFileSystemProvider implements Required, D private readonly onDidChangeCapabilitiesEmitter = new Emitter(); readonly onDidChangeCapabilities = this.onDidChangeCapabilitiesEmitter.event; + private readonly onDidChangeReadOnlyMessageEmitter = new Emitter(); + readonly onDidChangeReadOnlyMessage = this.onDidChangeReadOnlyMessageEmitter.event; + private readonly onFileStreamDataEmitter = new Emitter<[number, Uint8Array]>(); private readonly onFileStreamData = this.onFileStreamDataEmitter.event; @@ -129,6 +136,7 @@ export class RemoteFileSystemProvider implements Required, D protected readonly toDispose = new DisposableCollection( this.onDidChangeFileEmitter, this.onDidChangeCapabilitiesEmitter, + this.onDidChangeReadOnlyMessageEmitter, this.onFileStreamDataEmitter, this.onFileStreamEndEmitter ); @@ -146,6 +154,11 @@ export class RemoteFileSystemProvider implements Required, D private _capabilities: FileSystemProviderCapabilities = 0; get capabilities(): FileSystemProviderCapabilities { return this._capabilities; } + private _readOnlyMessage: MarkdownString | undefined = undefined; + get readOnlyMessage(): MarkdownString | undefined { + return this._readOnlyMessage; + } + protected readonly readyDeferred = new Deferred(); readonly ready = this.readyDeferred.promise; @@ -161,6 +174,9 @@ export class RemoteFileSystemProvider implements Required, D this._capabilities = capabilities; this.readyDeferred.resolve(); }, this.readyDeferred.reject); + this.server.getReadOnlyMessage().then(readOnlyMessage => { + this._readOnlyMessage = readOnlyMessage; + }); this.server.setClient({ notifyDidChangeFile: ({ changes }) => { this.onDidChangeFileEmitter.fire(changes.map(event => ({ resource: new URI(event.resource), type: event.type }))); @@ -169,6 +185,7 @@ export class RemoteFileSystemProvider implements Required, D this.onFileWatchErrorEmitter.fire(); }, notifyDidChangeCapabilities: capabilities => this.setCapabilities(capabilities), + notifyDidChangeReadOnlyMessage: readOnlyMessage => this.setReadOnlyMessage(readOnlyMessage), onFileStreamData: (handle, data) => this.onFileStreamDataEmitter.fire([handle, data]), onFileStreamEnd: (handle, error) => this.onFileStreamEndEmitter.fire([handle, error]) }); @@ -188,6 +205,11 @@ export class RemoteFileSystemProvider implements Required, D this.onDidChangeCapabilitiesEmitter.fire(undefined); } + protected setReadOnlyMessage(readOnlyMessage: MarkdownString | undefined): void { + this._readOnlyMessage = readOnlyMessage; + this.onDidChangeReadOnlyMessageEmitter.fire(readOnlyMessage); + } + // --- forwarding calls stat(resource: URI): Promise { @@ -362,6 +384,14 @@ export class FileSystemProviderServer implements RemoteFileSystemServer { this.client.notifyDidChangeCapabilities(this.provider.capabilities); } })); + if (ReadOnlyMessageFileSystemProvider.is(this.provider)) { + const providerWithReadOnlyMessage: ReadOnlyMessageFileSystemProvider = this.provider; + this.toDispose.push(this.provider.onDidChangeReadOnlyMessage(() => { + if (this.client) { + this.client.notifyDidChangeReadOnlyMessage(providerWithReadOnlyMessage.readOnlyMessage); + } + })); + } this.toDispose.push(this.provider.onDidChangeFile(changes => { if (this.client) { this.client.notifyDidChangeFile({ @@ -380,6 +410,12 @@ export class FileSystemProviderServer implements RemoteFileSystemServer { return this.provider.capabilities; } + async getReadOnlyMessage(): Promise { + if (ReadOnlyMessageFileSystemProvider.is(this.provider)) { + return this.provider.readOnlyMessage; + } + } + stat(resource: string): Promise { return this.provider.stat(new URI(resource)); } diff --git a/packages/monaco/src/browser/monaco-editor-provider.ts b/packages/monaco/src/browser/monaco-editor-provider.ts index f2c6c0a65d3ba..eb6e964d55b65 100644 --- a/packages/monaco/src/browser/monaco-editor-provider.ts +++ b/packages/monaco/src/browser/monaco-editor-provider.ts @@ -43,6 +43,7 @@ import { KeyCodeChord } from '@theia/monaco-editor-core/esm/vs/base/common/keybi import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; import { ITextModelService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/resolverService'; import { IReference } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; export const MonacoEditorFactory = Symbol('MonacoEditorFactory'); export interface MonacoEditorFactory { @@ -205,13 +206,20 @@ export class MonacoEditorProvider { } })); toDispose.push(editor.onLanguageChanged(() => this.updateMonacoEditorOptions(editor))); + toDispose.push(editor.onDidChangeReadOnly(() => this.updateReadOnlyMessage(options, model.readOnly))); editor.document.onWillSaveModel(event => event.waitUntil(this.formatOnSave(editor, event))); return editor; } + + protected updateReadOnlyMessage(options: MonacoEditor.IOptions, readOnly: boolean | MarkdownString ): any { + options.readOnlyMessage = MarkdownString.is(readOnly) ? readOnly : undefined; + } + protected createMonacoEditorOptions(model: MonacoEditorModel): MonacoEditor.IOptions { const options = this.createOptions(this.preferencePrefixes, model.uri, model.languageId); options.model = model.textEditorModel; options.readOnly = model.readOnly; + this.updateReadOnlyMessage(options, model.readOnly); options.lineNumbersMinChars = model.lineNumbersMinChars; return options; } @@ -293,6 +301,7 @@ export class MonacoEditorProvider { const options = this.createOptions(this.diffPreferencePrefixes, modified.uri, modified.languageId); options.originalEditable = !original.readOnly; options.readOnly = modified.readOnly; + options.readOnlyMessage = MarkdownString.is(modified.readOnly) ? modified.readOnly : undefined; return options; } protected updateMonacoDiffEditorOptions(editor: MonacoDiffEditor, event?: EditorPreferenceChange, resourceUri?: string): void { diff --git a/packages/monaco/src/browser/monaco-editor.ts b/packages/monaco/src/browser/monaco-editor.ts index a01845db1df3f..350c6293e519f 100644 --- a/packages/monaco/src/browser/monaco-editor.ts +++ b/packages/monaco/src/browser/monaco-editor.ts @@ -653,7 +653,7 @@ export namespace MonacoEditor { export function createReadOnlyOptions(readOnly?: boolean | MarkdownString): monaco.editor.IEditorOptions { if (typeof readOnly === 'boolean') { - return { readOnly }; + return { readOnly, readOnlyMessage: undefined }; } if (readOnly) { return { readOnly: true, readOnlyMessage: readOnly }; diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index d0ef6f8f54997..28c86ec97f221 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -1984,7 +1984,7 @@ export interface IFileChangeDto { } export interface FileSystemMain { - $registerFileSystemProvider(handle: number, scheme: string, capabilities: files.FileSystemProviderCapabilities): void; + $registerFileSystemProvider(handle: number, scheme: string, capabilities: files.FileSystemProviderCapabilities, readonlyMessage?: MarkdownString): void; $unregisterProvider(handle: number): void; $onFileSystemChange(handle: number, resource: IFileChangeDto[]): void; diff --git a/packages/plugin-ext/src/main/browser/file-system-main-impl.ts b/packages/plugin-ext/src/main/browser/file-system-main-impl.ts index 143a660cd0c12..26ba51941e344 100644 --- a/packages/plugin-ext/src/main/browser/file-system-main-impl.ts +++ b/packages/plugin-ext/src/main/browser/file-system-main-impl.ts @@ -35,9 +35,10 @@ import { UriComponents } from '../../common/uri-components'; import { FileSystemProviderCapabilities, Stat, FileType, FileSystemProviderErrorCode, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions, FileWriteOptions, WatchOptions, FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, FileSystemProviderWithFileFolderCopyCapability, - FileStat, FileChange, FileOperationError, FileOperationResult + FileStat, FileChange, FileOperationError, FileOperationResult, ReadOnlyMessageFileSystemProvider } from '@theia/filesystem/lib/common/files'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { MarkdownString } from '../../common/plugin-api-rpc-model'; type IDisposable = Disposable; @@ -66,8 +67,8 @@ export class FileSystemMainImpl implements FileSystemMain, Disposable { this._disposables.dispose(); } - $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities): void { - this._fileProvider.set(handle, new RemoteFileSystemProvider(this._fileService, scheme, capabilities, handle, this._proxy)); + $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities, readonlyMessage?: MarkdownString): void { + this._fileProvider.set(handle, new RemoteFileSystemProvider(this._fileService, scheme, capabilities, handle, this._proxy, readonlyMessage)); } $unregisterProvider(handle: number): void { @@ -163,7 +164,7 @@ export class FileSystemMainImpl implements FileSystemMain, Disposable { } -class RemoteFileSystemProvider implements FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, FileSystemProviderWithFileFolderCopyCapability { +class RemoteFileSystemProvider implements FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, FileSystemProviderWithFileFolderCopyCapability, ReadOnlyMessageFileSystemProvider { private readonly _onDidChange = new Emitter(); private readonly _registration: IDisposable; @@ -174,12 +175,15 @@ class RemoteFileSystemProvider implements FileSystemProviderWithFileReadWriteCap readonly capabilities: FileSystemProviderCapabilities; readonly onDidChangeCapabilities: Event = Event.None; + readonly onDidChangeReadOnlyMessage: Event = Event.None; + constructor( fileService: FileService, scheme: string, capabilities: FileSystemProviderCapabilities, private readonly _handle: number, - private readonly _proxy: FileSystemExt + private readonly _proxy: FileSystemExt, + public readonly readOnlyMessage: MarkdownString | undefined = undefined ) { this.capabilities = capabilities; this._registration = fileService.registerProvider(scheme, this); diff --git a/packages/plugin-ext/src/plugin/file-system-ext-impl.ts b/packages/plugin-ext/src/plugin/file-system-ext-impl.ts index f4db210a78ae3..9eb59c06cc12c 100644 --- a/packages/plugin-ext/src/plugin/file-system-ext-impl.ts +++ b/packages/plugin-ext/src/plugin/file-system-ext-impl.ts @@ -41,6 +41,7 @@ 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 { Emitter } from '@theia/core/shared/vscode-languageserver-protocol'; +import { MarkdownString } from '../common/plugin-api-rpc-model'; type IDisposable = vscode.Disposable; @@ -223,7 +224,7 @@ export class FileSystemExtImpl implements FileSystemExt { this.onWillRegisterFileSystemProviderEmitter.dispose(); } - registerFileSystemProvider(scheme: string, provider: vscode.FileSystemProvider, options: { isCaseSensitive?: boolean, isReadonly?: boolean } = {}) { + 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`); @@ -252,7 +253,19 @@ export class FileSystemExtImpl implements FileSystemExt { capabilities += files.FileSystemProviderCapabilities.FileOpenReadWriteClose; } - this._proxy.$registerFileSystemProvider(handle, scheme, capabilities); + 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[] = []; diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index cf50897c194fc..fb2949e261f5b 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -738,7 +738,8 @@ export function createAPIFactory( registerTextDocumentContentProvider(scheme: string, provider: theia.TextDocumentContentProvider): theia.Disposable { return workspaceExt.registerTextDocumentContentProvider(scheme, provider); }, - registerFileSystemProvider(scheme: string, provider: theia.FileSystemProvider, options?: { isCaseSensitive?: boolean, isReadonly?: boolean }): theia.Disposable { + registerFileSystemProvider(scheme: string, provider: theia.FileSystemProvider, options?: { isCaseSensitive?: boolean, isReadonly?: boolean | MarkdownString}): + theia.Disposable { return fileSystemExt.registerFileSystemProvider(scheme, provider, options); }, getWorkspaceFolder(uri: theia.Uri): theia.WorkspaceFolder | undefined { diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index f02bebf55377d..3a94dab6d70b9 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -7593,7 +7593,7 @@ export module '@theia/plugin' { * @param options Immutable metadata about the provider. * @return A {@link Disposable disposable} that unregisters this provider when being disposed. */ - export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider, options?: { readonly isCaseSensitive?: boolean, readonly isReadonly?: boolean }): Disposable; + export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider, options?: { readonly isCaseSensitive?: boolean, readonly isReadonly?: boolean | MarkdownString }): Disposable; /** * Returns the {@link WorkspaceFolder workspace folder} that contains a given uri.