Skip to content

Commit

Permalink
Better Support for ReadOnly message on editors (#13414)
Browse files Browse the repository at this point in the history
Also implements VS Code api for readOnly messages on FileSystemProvider

fixes #13353

contributed on behalf of STMicroelectronics

Signed-off-by: Remi Schnekenburger <[email protected]>
  • Loading branch information
rschnekenbu authored Feb 28, 2024
1 parent ddc7257 commit b96a84a
Show file tree
Hide file tree
Showing 13 changed files with 167 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

## v1.47.0 not yet released

- [filesystem] Implement readonly markdown message for file system providers [#13414]((<https://github.com/eclipse-theia/theia/pull/13414>) - contributed on behalf of STMicroelectronics
- [plugin] Add command to install plugins from the command line [#13406](https://github.com/eclipse-theia/theia/issues/13406) - contributed on behalf of STMicroelectronics
- [testing] support TestRunProfile onDidChangeDefault introduced in VS Code 1.86.0 [#13388](https://github.com/eclipse-theia/theia/pull/13388) - contributed on behalf of STMicroelectronics

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
}
});
}

}
Expand Down
43 changes: 30 additions & 13 deletions packages/filesystem/src/browser/file-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export namespace FileResourceVersion {
}

export interface FileResourceOptions {
isReadonly: boolean
readOnly: boolean | MarkdownString
shouldOverwrite: () => Promise<boolean>
shouldOpenAsText: (error: string) => Promise<boolean>
}
Expand All @@ -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(
Expand All @@ -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<void> {
const oldReadOnly = this.options.readOnly;
const readOnlyMessage = this.fileService.getReadOnlyMessage(this.uri);
if (readOnlyMessage) {
this.options.readOnly = readOnlyMessage;
} else {
this.options.readOnly = this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Readonly);
}
if (this.options.readOnly !== oldReadOnly) {
this.updateSavingContentChanges();
this.onDidChangeReadOnlyEmitter.fire(this.options.readOnly);
}
}

dispose(): void {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -332,8 +344,13 @@ 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.getReadOnlyMessage(uri);
const isFileSystemReadOnly = this.fileService.hasCapability(uri, FileSystemProviderCapabilities.Readonly);
const readOnly = readOnlyMessage ?? (isFileSystemReadOnly ? isFileSystemReadOnly : (stat?.isReadonly ?? false));

return new FileResource(uri, this.fileService, {
isReadonly: stat?.isReadonly ?? false,
readOnly: readOnly,
shouldOverwrite: () => this.shouldOverwrite(uri),
shouldOpenAsText: error => this.shouldOpenAsText(uri, error)
});
Expand Down
26 changes: 25 additions & 1 deletion packages/filesystem/src/browser/file-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -342,6 +352,9 @@ export class FileService {
private onDidChangeFileSystemProviderCapabilitiesEmitter = new Emitter<FileSystemProviderCapabilitiesChangeEvent>();
readonly onDidChangeFileSystemProviderCapabilities = this.onDidChangeFileSystemProviderCapabilitiesEmitter.event;

private onDidChangeFileSystemProviderReadOnlyMessageEmitter = new Emitter<FileSystemProviderReadOnlyMessageChangeEvent>();
readonly onDidChangeFileSystemProviderReadOnlyMessage = this.onDidChangeFileSystemProviderReadOnlyMessageEmitter.event;

private readonly providers = new Map<string, FileSystemProvider>();
private readonly activations = new Map<string, Promise<FileSystemProvider>>();

Expand All @@ -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 });
Expand Down Expand Up @@ -413,6 +429,14 @@ export class FileService {
return this.providers.has(resource.scheme);
}

getReadOnlyMessage(resource: URI): MarkdownString | undefined {
const provider = this.providers.get(resource.scheme);
if (ReadOnlyMessageFileSystemProvider.is(provider)) {
return provider.readOnlyMessage;
}
return undefined;
}

/**
* 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.
Expand Down
13 changes: 13 additions & 0 deletions packages/filesystem/src/common/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<MarkdownString | undefined>;
}

export namespace ReadOnlyMessageFileSystemProvider {
export function is(arg: unknown): arg is ReadOnlyMessageFileSystemProvider {
return isObject<ReadOnlyMessageFileSystemProvider>(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.
Expand Down
42 changes: 40 additions & 2 deletions packages/filesystem/src/common/remote-file-system-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,24 @@ 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';
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';

export const RemoteFileSystemServer = Symbol('RemoteFileSystemServer');
export interface RemoteFileSystemServer extends RpcServer<RemoteFileSystemClient> {
getCapabilities(): Promise<FileSystemProviderCapabilities>
stat(resource: string): Promise<Stat>;
getReadOnlyMessage(): Promise<MarkdownString | undefined>;
access(resource: string, mode?: number): Promise<void>;
fsPath(resource: string): Promise<string>;
open(resource: string, opts: FileOpenOptions): Promise<number>;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -109,7 +113,7 @@ export class RemoteFileSystemProxyFactory<T extends object> extends RpcProxyFact
* Wraps the remote filesystem provider living on the backend.
*/
@injectable()
export class RemoteFileSystemProvider implements Required<FileSystemProvider>, Disposable {
export class RemoteFileSystemProvider implements Required<FileSystemProvider>, Disposable, ReadOnlyMessageFileSystemProvider {

private readonly onDidChangeFileEmitter = new Emitter<readonly FileChange[]>();
readonly onDidChangeFile = this.onDidChangeFileEmitter.event;
Expand All @@ -120,6 +124,9 @@ export class RemoteFileSystemProvider implements Required<FileSystemProvider>, D
private readonly onDidChangeCapabilitiesEmitter = new Emitter<void>();
readonly onDidChangeCapabilities = this.onDidChangeCapabilitiesEmitter.event;

private readonly onDidChangeReadOnlyMessageEmitter = new Emitter<MarkdownString | undefined>();
readonly onDidChangeReadOnlyMessage = this.onDidChangeReadOnlyMessageEmitter.event;

private readonly onFileStreamDataEmitter = new Emitter<[number, Uint8Array]>();
private readonly onFileStreamData = this.onFileStreamDataEmitter.event;

Expand All @@ -129,6 +136,7 @@ export class RemoteFileSystemProvider implements Required<FileSystemProvider>, D
protected readonly toDispose = new DisposableCollection(
this.onDidChangeFileEmitter,
this.onDidChangeCapabilitiesEmitter,
this.onDidChangeReadOnlyMessageEmitter,
this.onFileStreamDataEmitter,
this.onFileStreamEndEmitter
);
Expand All @@ -146,6 +154,11 @@ export class RemoteFileSystemProvider implements Required<FileSystemProvider>, 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<void>();
readonly ready = this.readyDeferred.promise;

Expand All @@ -161,6 +174,9 @@ export class RemoteFileSystemProvider implements Required<FileSystemProvider>, 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 })));
Expand All @@ -169,6 +185,7 @@ export class RemoteFileSystemProvider implements Required<FileSystemProvider>, 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])
});
Expand All @@ -188,6 +205,11 @@ export class RemoteFileSystemProvider implements Required<FileSystemProvider>, D
this.onDidChangeCapabilitiesEmitter.fire(undefined);
}

protected setReadOnlyMessage(readOnlyMessage: MarkdownString | undefined): void {
this._readOnlyMessage = readOnlyMessage;
this.onDidChangeReadOnlyMessageEmitter.fire(readOnlyMessage);
}

// --- forwarding calls

stat(resource: URI): Promise<Stat> {
Expand Down Expand Up @@ -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({
Expand All @@ -380,6 +410,14 @@ export class FileSystemProviderServer implements RemoteFileSystemServer {
return this.provider.capabilities;
}

async getReadOnlyMessage(): Promise<MarkdownString | undefined> {
if (ReadOnlyMessageFileSystemProvider.is(this.provider)) {
return this.provider.readOnlyMessage;
} else {
return undefined;
}
}

stat(resource: string): Promise<Stat> {
return this.provider.stat(new URI(resource));
}
Expand Down
Loading

0 comments on commit b96a84a

Please sign in to comment.