Skip to content

Commit

Permalink
Better Support for ReadOnly message on editors
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 <rschnekenburger@eclipsesource.com>
  • Loading branch information
rschnekenbu committed Feb 28, 2024
1 parent ddc7257 commit fb077fa
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 fb077fa

Please sign in to comment.