Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better Support for ReadOnly message on editors #13414

Merged
merged 1 commit into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
msujew marked this conversation as resolved.
Show resolved Hide resolved
}

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;
msujew marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
* 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
Loading