Skip to content

Commit b96a84a

Browse files
authoredFeb 28, 2024··
Better Support for ReadOnly message on editors (#13414)
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>
1 parent ddc7257 commit b96a84a

File tree

13 files changed

+167
-27
lines changed

13 files changed

+167
-27
lines changed
 

‎CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
## v1.47.0 not yet released
88

9+
- [filesystem] Implement readonly markdown message for file system providers [#13414]((<https://github.com/eclipse-theia/theia/pull/13414>) - contributed on behalf of STMicroelectronics
910
- [plugin] Add command to install plugins from the command line [#13406](https://github.com/eclipse-theia/theia/issues/13406) - contributed on behalf of STMicroelectronics
1011
- [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
1112

‎examples/api-samples/src/browser/file-system/sample-file-system-capabilities.ts

+20
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { CommandContribution, CommandRegistry } from '@theia/core';
1818
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
1919
import { RemoteFileSystemProvider } from '@theia/filesystem/lib/common/remote-file-system-provider';
2020
import { FileSystemProviderCapabilities } from '@theia/filesystem/lib/common/files';
21+
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
2122

2223
@injectable()
2324
export class SampleFileSystemCapabilities implements CommandContribution {
@@ -39,6 +40,25 @@ export class SampleFileSystemCapabilities implements CommandContribution {
3940
}
4041
}
4142
});
43+
44+
commands.registerCommand({
45+
id: 'addFileSystemReadonlyMessage',
46+
label: 'Add a File System ReadonlyMessage for readonly'
47+
}, {
48+
execute: () => {
49+
const readonlyMessage = new MarkdownStringImpl(`Added new **Markdown** string '+${Date.now()}`);
50+
this.remoteFileSystemProvider['setReadOnlyMessage'](readonlyMessage);
51+
}
52+
});
53+
54+
commands.registerCommand({
55+
id: 'removeFileSystemReadonlyMessage',
56+
label: 'Remove File System ReadonlyMessage for readonly'
57+
}, {
58+
execute: () => {
59+
this.remoteFileSystemProvider['setReadOnlyMessage'](undefined);
60+
}
61+
});
4262
}
4363

4464
}

‎packages/filesystem/src/browser/file-resource.ts

+30-13
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export namespace FileResourceVersion {
4141
}
4242

4343
export interface FileResourceOptions {
44-
isReadonly: boolean
44+
readOnly: boolean | MarkdownString
4545
shouldOverwrite: () => Promise<boolean>
4646
shouldOpenAsText: (error: string) => Promise<boolean>
4747
}
@@ -65,8 +65,8 @@ export class FileResource implements Resource {
6565
get encoding(): string | undefined {
6666
return this._version?.encoding;
6767
}
68-
get readOnly(): boolean {
69-
return this.options.isReadonly || this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Readonly);
68+
get readOnly(): boolean | MarkdownString {
69+
return this.options.readOnly;
7070
}
7171

7272
constructor(
@@ -92,11 +92,30 @@ export class FileResource implements Resource {
9292
console.error(e);
9393
}
9494
this.updateSavingContentChanges();
95-
this.toDispose.push(this.fileService.onDidChangeFileSystemProviderCapabilities(e => {
95+
this.toDispose.push(this.fileService.onDidChangeFileSystemProviderCapabilities(async e => {
9696
if (e.scheme === this.uri.scheme) {
97-
this.updateSavingContentChanges();
97+
this.updateReadOnly();
9898
}
9999
}));
100+
this.fileService.onDidChangeFileSystemProviderReadOnlyMessage(async e => {
101+
if (e.scheme === this.uri.scheme) {
102+
this.updateReadOnly();
103+
}
104+
});
105+
}
106+
107+
protected async updateReadOnly(): Promise<void> {
108+
const oldReadOnly = this.options.readOnly;
109+
const readOnlyMessage = this.fileService.getReadOnlyMessage(this.uri);
110+
if (readOnlyMessage) {
111+
this.options.readOnly = readOnlyMessage;
112+
} else {
113+
this.options.readOnly = this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Readonly);
114+
}
115+
if (this.options.readOnly !== oldReadOnly) {
116+
this.updateSavingContentChanges();
117+
this.onDidChangeReadOnlyEmitter.fire(this.options.readOnly);
118+
}
100119
}
101120

102121
dispose(): void {
@@ -225,24 +244,17 @@ export class FileResource implements Resource {
225244
saveContents?: Resource['saveContents'];
226245
saveContentChanges?: Resource['saveContentChanges'];
227246
protected updateSavingContentChanges(): void {
228-
let changed = false;
229247
if (this.readOnly) {
230-
changed = Boolean(this.saveContents);
231248
delete this.saveContentChanges;
232249
delete this.saveContents;
233250
delete this.saveStream;
234251
} else {
235-
changed = !Boolean(this.saveContents);
236252
this.saveContents = this.doWrite;
237253
this.saveStream = this.doWrite;
238254
if (this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Update)) {
239255
this.saveContentChanges = this.doSaveContentChanges;
240256
}
241257
}
242-
if (changed) {
243-
// Only actually bother to call the event if the value has changed.
244-
this.onDidChangeReadOnlyEmitter.fire(this.readOnly);
245-
}
246258
}
247259
protected doSaveContentChanges: Resource['saveContentChanges'] = async (changes, options) => {
248260
const version = options?.version || this._version;
@@ -332,8 +344,13 @@ export class FileResourceResolver implements ResourceResolver {
332344
if (stat && stat.isDirectory) {
333345
throw new Error('The given uri is a directory: ' + this.labelProvider.getLongName(uri));
334346
}
347+
348+
const readOnlyMessage = this.fileService.getReadOnlyMessage(uri);
349+
const isFileSystemReadOnly = this.fileService.hasCapability(uri, FileSystemProviderCapabilities.Readonly);
350+
const readOnly = readOnlyMessage ?? (isFileSystemReadOnly ? isFileSystemReadOnly : (stat?.isReadonly ?? false));
351+
335352
return new FileResource(uri, this.fileService, {
336-
isReadonly: stat?.isReadonly ?? false,
353+
readOnly: readOnly,
337354
shouldOverwrite: () => this.shouldOverwrite(uri),
338355
shouldOpenAsText: error => this.shouldOpenAsText(uri, error)
339356
});

‎packages/filesystem/src/browser/file-service.ts

+25-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import {
5151
toFileOperationResult, toFileSystemProviderErrorCode,
5252
ResolveFileResult, ResolveFileResultWithMetadata,
5353
MoveFileOptions, CopyFileOptions, BaseStatWithMetadata, FileDeleteOptions, FileOperationOptions, hasAccessCapability, hasUpdateCapability,
54-
hasFileReadStreamCapability, FileSystemProviderWithFileReadStreamCapability
54+
hasFileReadStreamCapability, FileSystemProviderWithFileReadStreamCapability, ReadOnlyMessageFileSystemProvider
5555
} from '../common/files';
5656
import { BinaryBuffer, BinaryBufferReadable, BinaryBufferReadableStream, BinaryBufferReadableBufferedStream, BinaryBufferWriteableStream } from '@theia/core/lib/common/buffer';
5757
import { ReadableStream, isReadableStream, isReadableBufferedStream, transform, consumeStream, peekStream, peekReadable, Readable } from '@theia/core/lib/common/stream';
@@ -68,6 +68,7 @@ import { readFileIntoStream } from '../common/io';
6868
import { FileSystemWatcherErrorHandler } from './filesystem-watcher-error-handler';
6969
import { FileSystemUtils } from '../common/filesystem-utils';
7070
import { nls } from '@theia/core';
71+
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
7172

7273
export interface FileOperationParticipant {
7374

@@ -235,6 +236,15 @@ export interface FileSystemProviderCapabilitiesChangeEvent {
235236
scheme: string;
236237
}
237238

239+
export interface FileSystemProviderReadOnlyMessageChangeEvent {
240+
/** The affected file system provider for which this event was fired. */
241+
provider: FileSystemProvider;
242+
/** The uri for which the provider is registered */
243+
scheme: string;
244+
/** The new read only message */
245+
message: MarkdownString | undefined;
246+
}
247+
238248
/**
239249
* Represents the `FileSystemProviderActivation` event.
240250
* This event is fired by the {@link FileService} if it wants to activate the
@@ -342,6 +352,9 @@ export class FileService {
342352
private onDidChangeFileSystemProviderCapabilitiesEmitter = new Emitter<FileSystemProviderCapabilitiesChangeEvent>();
343353
readonly onDidChangeFileSystemProviderCapabilities = this.onDidChangeFileSystemProviderCapabilitiesEmitter.event;
344354

355+
private onDidChangeFileSystemProviderReadOnlyMessageEmitter = new Emitter<FileSystemProviderReadOnlyMessageChangeEvent>();
356+
readonly onDidChangeFileSystemProviderReadOnlyMessage = this.onDidChangeFileSystemProviderReadOnlyMessageEmitter.event;
357+
345358
private readonly providers = new Map<string, FileSystemProvider>();
346359
private readonly activations = new Map<string, Promise<FileSystemProvider>>();
347360

@@ -364,6 +377,9 @@ export class FileService {
364377
providerDisposables.push(provider.onDidChangeFile(changes => this.onDidFilesChangeEmitter.fire(new FileChangesEvent(changes))));
365378
providerDisposables.push(provider.onFileWatchError(() => this.handleFileWatchError()));
366379
providerDisposables.push(provider.onDidChangeCapabilities(() => this.onDidChangeFileSystemProviderCapabilitiesEmitter.fire({ provider, scheme })));
380+
if (ReadOnlyMessageFileSystemProvider.is(provider)) {
381+
providerDisposables.push(provider.onDidChangeReadOnlyMessage(message => this.onDidChangeFileSystemProviderReadOnlyMessageEmitter.fire({ provider, scheme, message})));
382+
}
367383

368384
return Disposable.create(() => {
369385
this.onDidChangeFileSystemProviderRegistrationsEmitter.fire({ added: false, scheme, provider });
@@ -413,6 +429,14 @@ export class FileService {
413429
return this.providers.has(resource.scheme);
414430
}
415431

432+
getReadOnlyMessage(resource: URI): MarkdownString | undefined {
433+
const provider = this.providers.get(resource.scheme);
434+
if (ReadOnlyMessageFileSystemProvider.is(provider)) {
435+
return provider.readOnlyMessage;
436+
}
437+
return undefined;
438+
}
439+
416440
/**
417441
* Tests if the service (i.e the {@link FileSystemProvider} registered for the given uri scheme) provides the given capability.
418442
* @param resource `URI` of the resource to test.

‎packages/filesystem/src/common/files.ts

+13
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type { TextDocumentContentChangeEvent } from '@theia/core/shared/vscode-l
2727
import { ReadableStreamEvents } from '@theia/core/lib/common/stream';
2828
import { CancellationToken } from '@theia/core/lib/common/cancellation';
2929
import { isObject } from '@theia/core/lib/common';
30+
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
3031

3132
export const enum FileOperation {
3233
CREATE,
@@ -765,6 +766,18 @@ export function hasUpdateCapability(provider: FileSystemProvider): provider is F
765766
return !!(provider.capabilities & FileSystemProviderCapabilities.Update);
766767
}
767768

769+
export interface ReadOnlyMessageFileSystemProvider {
770+
readOnlyMessage: MarkdownString | undefined;
771+
readonly onDidChangeReadOnlyMessage: Event<MarkdownString | undefined>;
772+
}
773+
774+
export namespace ReadOnlyMessageFileSystemProvider {
775+
export function is(arg: unknown): arg is ReadOnlyMessageFileSystemProvider {
776+
return isObject<ReadOnlyMessageFileSystemProvider>(arg)
777+
&& 'readOnlyMessage' in arg;
778+
}
779+
}
780+
768781
/**
769782
* Subtype of {@link FileSystemProvider} that ensures that the optional functions, needed for providers
770783
* that should be able to read & write files, are implemented.

‎packages/filesystem/src/common/remote-file-system-provider.ts

+40-2
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,24 @@ import {
2323
FileWriteOptions, FileOpenOptions, FileChangeType,
2424
FileSystemProviderCapabilities, FileChange, Stat, FileOverwriteOptions, WatchOptions, FileType, FileSystemProvider, FileDeleteOptions,
2525
hasOpenReadWriteCloseCapability, hasFileFolderCopyCapability, hasReadWriteCapability, hasAccessCapability,
26-
FileSystemProviderError, FileSystemProviderErrorCode, FileUpdateOptions, hasUpdateCapability, FileUpdateResult, FileReadStreamOptions, hasFileReadStreamCapability
26+
FileSystemProviderError, FileSystemProviderErrorCode, FileUpdateOptions, hasUpdateCapability, FileUpdateResult, FileReadStreamOptions, hasFileReadStreamCapability,
27+
ReadOnlyMessageFileSystemProvider
2728
} from './files';
2829
import { RpcServer, RpcProxy, RpcProxyFactory } from '@theia/core/lib/common/messaging/proxy-factory';
2930
import { ApplicationError } from '@theia/core/lib/common/application-error';
3031
import { Deferred } from '@theia/core/lib/common/promise-util';
3132
import type { TextDocumentContentChangeEvent } from '@theia/core/shared/vscode-languageserver-protocol';
3233
import { newWriteableStream, ReadableStreamEvents } from '@theia/core/lib/common/stream';
3334
import { CancellationToken, cancelled } from '@theia/core/lib/common/cancellation';
35+
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
3436

3537
export const remoteFileSystemPath = '/services/remote-filesystem';
3638

3739
export const RemoteFileSystemServer = Symbol('RemoteFileSystemServer');
3840
export interface RemoteFileSystemServer extends RpcServer<RemoteFileSystemClient> {
3941
getCapabilities(): Promise<FileSystemProviderCapabilities>
4042
stat(resource: string): Promise<Stat>;
43+
getReadOnlyMessage(): Promise<MarkdownString | undefined>;
4144
access(resource: string, mode?: number): Promise<void>;
4245
fsPath(resource: string): Promise<string>;
4346
open(resource: string, opts: FileOpenOptions): Promise<number>;
@@ -70,6 +73,7 @@ export interface RemoteFileSystemClient {
7073
notifyDidChangeFile(event: { changes: RemoteFileChange[] }): void;
7174
notifyFileWatchError(): void;
7275
notifyDidChangeCapabilities(capabilities: FileSystemProviderCapabilities): void;
76+
notifyDidChangeReadOnlyMessage(readOnlyMessage: MarkdownString | undefined): void;
7377
onFileStreamData(handle: number, data: Uint8Array): void;
7478
onFileStreamEnd(handle: number, error: RemoteFileStreamError | undefined): void;
7579
}
@@ -109,7 +113,7 @@ export class RemoteFileSystemProxyFactory<T extends object> extends RpcProxyFact
109113
* Wraps the remote filesystem provider living on the backend.
110114
*/
111115
@injectable()
112-
export class RemoteFileSystemProvider implements Required<FileSystemProvider>, Disposable {
116+
export class RemoteFileSystemProvider implements Required<FileSystemProvider>, Disposable, ReadOnlyMessageFileSystemProvider {
113117

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

127+
private readonly onDidChangeReadOnlyMessageEmitter = new Emitter<MarkdownString | undefined>();
128+
readonly onDidChangeReadOnlyMessage = this.onDidChangeReadOnlyMessageEmitter.event;
129+
123130
private readonly onFileStreamDataEmitter = new Emitter<[number, Uint8Array]>();
124131
private readonly onFileStreamData = this.onFileStreamDataEmitter.event;
125132

@@ -129,6 +136,7 @@ export class RemoteFileSystemProvider implements Required<FileSystemProvider>, D
129136
protected readonly toDispose = new DisposableCollection(
130137
this.onDidChangeFileEmitter,
131138
this.onDidChangeCapabilitiesEmitter,
139+
this.onDidChangeReadOnlyMessageEmitter,
132140
this.onFileStreamDataEmitter,
133141
this.onFileStreamEndEmitter
134142
);
@@ -146,6 +154,11 @@ export class RemoteFileSystemProvider implements Required<FileSystemProvider>, D
146154
private _capabilities: FileSystemProviderCapabilities = 0;
147155
get capabilities(): FileSystemProviderCapabilities { return this._capabilities; }
148156

157+
private _readOnlyMessage: MarkdownString | undefined = undefined;
158+
get readOnlyMessage(): MarkdownString | undefined {
159+
return this._readOnlyMessage;
160+
}
161+
149162
protected readonly readyDeferred = new Deferred<void>();
150163
readonly ready = this.readyDeferred.promise;
151164

@@ -161,6 +174,9 @@ export class RemoteFileSystemProvider implements Required<FileSystemProvider>, D
161174
this._capabilities = capabilities;
162175
this.readyDeferred.resolve();
163176
}, this.readyDeferred.reject);
177+
this.server.getReadOnlyMessage().then(readOnlyMessage => {
178+
this._readOnlyMessage = readOnlyMessage;
179+
});
164180
this.server.setClient({
165181
notifyDidChangeFile: ({ changes }) => {
166182
this.onDidChangeFileEmitter.fire(changes.map(event => ({ resource: new URI(event.resource), type: event.type })));
@@ -169,6 +185,7 @@ export class RemoteFileSystemProvider implements Required<FileSystemProvider>, D
169185
this.onFileWatchErrorEmitter.fire();
170186
},
171187
notifyDidChangeCapabilities: capabilities => this.setCapabilities(capabilities),
188+
notifyDidChangeReadOnlyMessage: readOnlyMessage => this.setReadOnlyMessage(readOnlyMessage),
172189
onFileStreamData: (handle, data) => this.onFileStreamDataEmitter.fire([handle, data]),
173190
onFileStreamEnd: (handle, error) => this.onFileStreamEndEmitter.fire([handle, error])
174191
});
@@ -188,6 +205,11 @@ export class RemoteFileSystemProvider implements Required<FileSystemProvider>, D
188205
this.onDidChangeCapabilitiesEmitter.fire(undefined);
189206
}
190207

208+
protected setReadOnlyMessage(readOnlyMessage: MarkdownString | undefined): void {
209+
this._readOnlyMessage = readOnlyMessage;
210+
this.onDidChangeReadOnlyMessageEmitter.fire(readOnlyMessage);
211+
}
212+
191213
// --- forwarding calls
192214

193215
stat(resource: URI): Promise<Stat> {
@@ -362,6 +384,14 @@ export class FileSystemProviderServer implements RemoteFileSystemServer {
362384
this.client.notifyDidChangeCapabilities(this.provider.capabilities);
363385
}
364386
}));
387+
if (ReadOnlyMessageFileSystemProvider.is(this.provider)) {
388+
const providerWithReadOnlyMessage: ReadOnlyMessageFileSystemProvider = this.provider;
389+
this.toDispose.push(this.provider.onDidChangeReadOnlyMessage(() => {
390+
if (this.client) {
391+
this.client.notifyDidChangeReadOnlyMessage(providerWithReadOnlyMessage.readOnlyMessage);
392+
}
393+
}));
394+
}
365395
this.toDispose.push(this.provider.onDidChangeFile(changes => {
366396
if (this.client) {
367397
this.client.notifyDidChangeFile({
@@ -380,6 +410,14 @@ export class FileSystemProviderServer implements RemoteFileSystemServer {
380410
return this.provider.capabilities;
381411
}
382412

413+
async getReadOnlyMessage(): Promise<MarkdownString | undefined> {
414+
if (ReadOnlyMessageFileSystemProvider.is(this.provider)) {
415+
return this.provider.readOnlyMessage;
416+
} else {
417+
return undefined;
418+
}
419+
}
420+
383421
stat(resource: string): Promise<Stat> {
384422
return this.provider.stat(new URI(resource));
385423
}

0 commit comments

Comments
 (0)
Please sign in to comment.