Skip to content

Commit ac7a9ff

Browse files
authored
Handle file system permissions (eclipse-theia#11965)
1 parent 1446bca commit ac7a9ff

File tree

18 files changed

+161
-27
lines changed

18 files changed

+161
-27
lines changed

packages/core/src/browser/shell/tab-bars.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { IconThemeService } from '../icon-theme-service';
3131
import { BreadcrumbsRenderer, BreadcrumbsRendererFactory } from '../breadcrumbs/breadcrumbs-renderer';
3232
import { NavigatableWidget } from '../navigatable-types';
3333
import { IDragEvent } from '@phosphor/dragdrop';
34-
import { PINNED_CLASS } from '../widgets/widget';
34+
import { LOCKED_CLASS, PINNED_CLASS } from '../widgets/widget';
3535
import { CorePreferences } from '../core-preferences';
3636
import { HoverService } from '../hover-service';
3737

@@ -178,7 +178,8 @@ export class TabBarRenderer extends TabBar.Renderer {
178178
{ className: 'theia-tab-icon-label' },
179179
this.renderIcon(data, isInSidePanel),
180180
this.renderLabel(data, isInSidePanel),
181-
this.renderBadge(data, isInSidePanel)
181+
this.renderBadge(data, isInSidePanel),
182+
this.renderLock(data, isInSidePanel)
182183
),
183184
h.div({
184185
className: 'p-TabBar-tabCloseIcon action-label',
@@ -275,6 +276,12 @@ export class TabBarRenderer extends TabBar.Renderer {
275276
: h.div({ className: 'theia-badge-decorator-horizontal' }, `${limitedBadge}`);
276277
}
277278

279+
renderLock(data: SideBarRenderData, isInSidePanel?: boolean): VirtualElement {
280+
return !isInSidePanel && data.title.className.includes(LOCKED_CLASS)
281+
? h.div({ className: 'p-TabBar-tabLock' })
282+
: h.div({});
283+
}
284+
278285
protected readonly decorations = new Map<Title<Widget>, WidgetDecoration.Data[]>();
279286

280287
protected resetDecorations(title?: Title<Widget>): void {

packages/core/src/browser/style/tabs.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,18 @@
173173
background: none;
174174
}
175175

176+
.p-TabBar-tabLock:after {
177+
content: "\ebe7";
178+
opacity: 0.75;
179+
margin-left: 4px;
180+
color: inherit;
181+
font-family: codicon;
182+
font-size: 16px;
183+
font-weight: normal;
184+
display: inline-block;
185+
vertical-align: top;
186+
}
187+
176188
/* file icons */
177189
.p-TabBar[data-orientation='horizontal'] .p-TabBar-tabIcon.file-icon,
178190
.p-TabBar-tab.p-mod-drag-image .p-TabBar-tabIcon.file-icon {

packages/core/src/browser/widgets/widget.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const CODICON_LOADING_CLASSES = codiconArray('loading');
5353
export const SELECTED_CLASS = 'theia-mod-selected';
5454
export const FOCUS_CLASS = 'theia-mod-focus';
5555
export const PINNED_CLASS = 'theia-mod-pinned';
56+
export const LOCKED_CLASS = 'theia-mod-locked';
5657
export const DEFAULT_SCROLL_OPTIONS: PerfectScrollbar.Options = {
5758
suppressScrollX: true,
5859
minScrollbarLength: 35,
@@ -371,6 +372,12 @@ export function pin(title: Title<Widget>): void {
371372
}
372373
}
373374

375+
export function lock(title: Title<Widget>): void {
376+
if (!title.className.includes(LOCKED_CLASS)) {
377+
title.className += ` ${LOCKED_CLASS}`;
378+
}
379+
}
380+
374381
export function togglePinned(title?: Title<Widget>): void {
375382
if (title) {
376383
if (isPinned(title)) {

packages/core/src/common/resource.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export interface Resource extends Disposable {
5555
* Undefined if a resource did not read content yet.
5656
*/
5757
readonly encoding?: string | undefined;
58+
readonly isReadonly?: boolean;
5859
/**
5960
* Reads latest content of this resource.
6061
*

packages/editor/src/browser/editor-widget-factory.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import { injectable, inject } from '@theia/core/shared/inversify';
1818
import URI from '@theia/core/lib/common/uri';
19-
import { SelectionService } from '@theia/core/lib/common';
19+
import { nls, SelectionService } from '@theia/core/lib/common';
2020
import { NavigatableWidgetOptions, WidgetFactory, LabelProvider } from '@theia/core/lib/browser';
2121
import { EditorWidget } from './editor-widget';
2222
import { TextEditorProvider } from './editor';
@@ -72,6 +72,10 @@ export class EditorWidgetFactory implements WidgetFactory {
7272

7373
private setLabels(editor: EditorWidget, uri: URI): void {
7474
editor.title.caption = uri.path.fsPath();
75+
if (editor.editor.isReadonly) {
76+
// nls-todo: 'Read Only' be available with newer VSCode API
77+
editor.title.caption += ` • ${nls.localize('theia/editor/readOnly', 'Read Only')}`;
78+
}
7579
const icon = this.labelProvider.getIcon(uri);
7680
editor.title.label = this.labelProvider.getName(uri);
7781
editor.title.iconClass = icon + ' file-icon';

packages/editor/src/browser/editor-widget.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// *****************************************************************************
1616

1717
import { Disposable, SelectionService, Event, UNTITLED_SCHEME } from '@theia/core/lib/common';
18-
import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget } from '@theia/core/lib/browser';
18+
import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget, lock } from '@theia/core/lib/browser';
1919
import URI from '@theia/core/lib/common/uri';
2020
import { TextEditor } from './editor';
2121

@@ -27,6 +27,9 @@ export class EditorWidget extends BaseWidget implements SaveableSource, Navigata
2727
) {
2828
super(editor);
2929
this.addClass('theia-editor');
30+
if (editor.isReadonly) {
31+
lock(this.title);
32+
}
3033
this.toDispose.push(this.editor);
3134
this.toDispose.push(this.editor.onSelectionChanged(() => this.setSelection()));
3235
this.toDispose.push(this.editor.onFocusChanged(() => this.setSelection()));

packages/editor/src/browser/editor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ export interface TextEditor extends Disposable, TextEditorSelection, Navigatable
196196
readonly node: HTMLElement;
197197

198198
readonly uri: URI;
199+
readonly isReadonly: boolean;
199200
readonly document: TextEditorDocument;
200201
readonly onDocumentContentChanged: Event<TextDocumentChangeEvent>;
201202

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

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export namespace FileResourceVersion {
4040
}
4141

4242
export interface FileResourceOptions {
43+
isReadonly: boolean
4344
shouldOverwrite: () => Promise<boolean>
4445
shouldOpenAsText: (error: string) => Promise<boolean>
4546
}
@@ -60,6 +61,9 @@ export class FileResource implements Resource {
6061
get encoding(): string | undefined {
6162
return this._version?.encoding;
6263
}
64+
get isReadonly(): boolean {
65+
return this.options.isReadonly || this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Readonly);
66+
}
6367

6468
constructor(
6569
readonly uri: URI,
@@ -184,15 +188,7 @@ export class FileResource implements Resource {
184188
}
185189
}
186190

187-
saveContents(content: string, options?: ResourceSaveOptions): Promise<void> {
188-
return this.doWrite(content, options);
189-
}
190-
191-
saveStream(content: Readable<string>, options?: ResourceSaveOptions): Promise<void> {
192-
return this.doWrite(content, options);
193-
}
194-
195-
protected async doWrite(content: string | Readable<string>, options?: ResourceSaveOptions): Promise<void> {
191+
protected doWrite = async (content: string | Readable<string>, options?: ResourceSaveOptions): Promise<void> => {
196192
const version = options?.version || this._version;
197193
const current = FileResourceVersion.is(version) ? version : undefined;
198194
const etag = current?.etag;
@@ -218,14 +214,22 @@ export class FileResource implements Resource {
218214
}
219215
throw e;
220216
}
221-
}
217+
};
222218

219+
saveStream?: Resource['saveStream'];
220+
saveContents?: Resource['saveContents'];
223221
saveContentChanges?: Resource['saveContentChanges'];
224222
protected updateSavingContentChanges(): void {
225-
if (this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Update)) {
226-
this.saveContentChanges = this.doSaveContentChanges;
227-
} else {
223+
if (this.isReadonly) {
228224
delete this.saveContentChanges;
225+
delete this.saveContents;
226+
delete this.saveStream;
227+
} else {
228+
this.saveContents = this.doWrite;
229+
this.saveStream = this.doWrite;
230+
if (this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Update)) {
231+
this.saveContentChanges = this.doSaveContentChanges;
232+
}
229233
}
230234
}
231235
protected doSaveContentChanges: Resource['saveContentChanges'] = async (changes, options) => {
@@ -317,6 +321,7 @@ export class FileResourceResolver implements ResourceResolver {
317321
throw new Error('The given uri is a directory: ' + this.labelProvider.getLongName(uri));
318322
}
319323
return new FileResource(uri, this.fileService, {
324+
isReadonly: stat?.isReadonly ?? false,
320325
shouldOverwrite: () => this.shouldOverwrite(uri),
321326
shouldOpenAsText: error => this.shouldOpenAsText(uri, error)
322327
});

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,16 @@ export class FileService {
426426
return !!(provider && (provider.capabilities & capability));
427427
}
428428

429+
/**
430+
* List the schemes and capabilities for registered file system providers
431+
*/
432+
listCapabilities(): { scheme: string; capabilities: FileSystemProviderCapabilities }[] {
433+
return Array.from(this.providers.entries()).map(([scheme, provider]) => ({
434+
scheme,
435+
capabilities: provider.capabilities
436+
}));
437+
}
438+
429439
protected async withProvider(resource: URI): Promise<FileSystemProvider> {
430440
// Assert path is absolute
431441
if (!resource.path.isAbsolute) {

packages/filesystem/src/common/files.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,11 @@ export interface FileStat extends BaseStat {
234234
*/
235235
isSymbolicLink: boolean;
236236

237+
/**
238+
* The resource is read only.
239+
*/
240+
isReadonly: boolean;
241+
237242
/**
238243
* The children of the file stat or undefined if none.
239244
*/
@@ -277,6 +282,7 @@ export namespace FileStat {
277282
isFile: (stat.type & FileType.File) !== 0,
278283
isDirectory: (stat.type & FileType.Directory) !== 0,
279284
isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0,
285+
isReadonly: !!stat.permissions && (stat.permissions & FilePermission.Readonly) !== 0,
280286
mtime: stat.mtime,
281287
ctime: stat.ctime,
282288
size: stat.size,
@@ -485,6 +491,14 @@ export enum FileType {
485491
SymbolicLink = 64
486492
}
487493

494+
export enum FilePermission {
495+
496+
/**
497+
* File is readonly.
498+
*/
499+
Readonly = 1
500+
}
501+
488502
export interface Stat {
489503
type: FileType;
490504

@@ -499,6 +513,8 @@ export interface Stat {
499513
ctime: number;
500514

501515
size: number;
516+
517+
permissions?: FilePermission;
502518
}
503519

504520
export interface WatchOptions {

0 commit comments

Comments
 (0)