diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts
index f0237776da929..6d3faade9659c 100644
--- a/packages/core/src/browser/shell/tab-bars.ts
+++ b/packages/core/src/browser/shell/tab-bars.ts
@@ -31,7 +31,7 @@ import { IconThemeService } from '../icon-theme-service';
import { BreadcrumbsRenderer, BreadcrumbsRendererFactory } from '../breadcrumbs/breadcrumbs-renderer';
import { NavigatableWidget } from '../navigatable-types';
import { IDragEvent } from '@phosphor/dragdrop';
-import { PINNED_CLASS } from '../widgets/widget';
+import { LOCKED_CLASS, PINNED_CLASS } from '../widgets/widget';
import { CorePreferences } from '../core-preferences';
import { HoverService } from '../hover-service';
@@ -178,7 +178,8 @@ export class TabBarRenderer extends TabBar.Renderer {
{ className: 'theia-tab-icon-label' },
this.renderIcon(data, isInSidePanel),
this.renderLabel(data, isInSidePanel),
- this.renderBadge(data, isInSidePanel)
+ this.renderBadge(data, isInSidePanel),
+ this.renderLock(data, isInSidePanel)
),
h.div({
className: 'p-TabBar-tabCloseIcon action-label',
@@ -275,6 +276,12 @@ export class TabBarRenderer extends TabBar.Renderer {
: h.div({ className: 'theia-badge-decorator-horizontal' }, `${limitedBadge}`);
}
+ renderLock(data: SideBarRenderData, isInSidePanel?: boolean): VirtualElement {
+ return !isInSidePanel && data.title.className.includes(LOCKED_CLASS)
+ ? h.div({ className: 'p-TabBar-tabLock' })
+ : h.div({});
+ }
+
protected readonly decorations = new Map
, WidgetDecoration.Data[]>();
protected resetDecorations(title?: Title): void {
diff --git a/packages/core/src/browser/style/tabs.css b/packages/core/src/browser/style/tabs.css
index cb35d7e7f5990..51150ef36507d 100644
--- a/packages/core/src/browser/style/tabs.css
+++ b/packages/core/src/browser/style/tabs.css
@@ -173,6 +173,18 @@
background: none;
}
+.p-TabBar-tabLock:after {
+ content: "\ebe7";
+ opacity: 0.75;
+ margin-left: 4px;
+ color: inherit;
+ font-family: codicon;
+ font-size: 16px;
+ font-weight: normal;
+ display: inline-block;
+ vertical-align: top;
+}
+
/* file icons */
.p-TabBar[data-orientation='horizontal'] .p-TabBar-tabIcon.file-icon,
.p-TabBar-tab.p-mod-drag-image .p-TabBar-tabIcon.file-icon {
diff --git a/packages/core/src/browser/widgets/widget.ts b/packages/core/src/browser/widgets/widget.ts
index 5274d0f2386a6..09e920a7cf032 100644
--- a/packages/core/src/browser/widgets/widget.ts
+++ b/packages/core/src/browser/widgets/widget.ts
@@ -53,6 +53,7 @@ export const CODICON_LOADING_CLASSES = codiconArray('loading');
export const SELECTED_CLASS = 'theia-mod-selected';
export const FOCUS_CLASS = 'theia-mod-focus';
export const PINNED_CLASS = 'theia-mod-pinned';
+export const LOCKED_CLASS = 'theia-mod-locked';
export const DEFAULT_SCROLL_OPTIONS: PerfectScrollbar.Options = {
suppressScrollX: true,
minScrollbarLength: 35,
@@ -371,6 +372,12 @@ export function pin(title: Title): void {
}
}
+export function lock(title: Title): void {
+ if (!title.className.includes(LOCKED_CLASS)) {
+ title.className += ` ${LOCKED_CLASS}`;
+ }
+}
+
export function togglePinned(title?: Title): void {
if (title) {
if (isPinned(title)) {
diff --git a/packages/core/src/common/resource.ts b/packages/core/src/common/resource.ts
index 607697993bea7..4f0586a2e8935 100644
--- a/packages/core/src/common/resource.ts
+++ b/packages/core/src/common/resource.ts
@@ -55,6 +55,7 @@ export interface Resource extends Disposable {
* Undefined if a resource did not read content yet.
*/
readonly encoding?: string | undefined;
+ readonly isReadonly?: boolean;
/**
* Reads latest content of this resource.
*
diff --git a/packages/editor/src/browser/editor-widget-factory.ts b/packages/editor/src/browser/editor-widget-factory.ts
index da30689b14bf3..f29daafa4643e 100644
--- a/packages/editor/src/browser/editor-widget-factory.ts
+++ b/packages/editor/src/browser/editor-widget-factory.ts
@@ -16,7 +16,7 @@
import { injectable, inject } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
-import { SelectionService } from '@theia/core/lib/common';
+import { nls, SelectionService } from '@theia/core/lib/common';
import { NavigatableWidgetOptions, WidgetFactory, LabelProvider } from '@theia/core/lib/browser';
import { EditorWidget } from './editor-widget';
import { TextEditorProvider } from './editor';
@@ -72,6 +72,10 @@ export class EditorWidgetFactory implements WidgetFactory {
private setLabels(editor: EditorWidget, uri: URI): void {
editor.title.caption = uri.path.fsPath();
+ if (editor.editor.isReadonly) {
+ // nls-todo: 'Read Only' be available with newer VSCode API
+ editor.title.caption += ` • ${nls.localize('theia/editor/readOnly', 'Read Only')}`;
+ }
const icon = this.labelProvider.getIcon(uri);
editor.title.label = this.labelProvider.getName(uri);
editor.title.iconClass = icon + ' file-icon';
diff --git a/packages/editor/src/browser/editor-widget.ts b/packages/editor/src/browser/editor-widget.ts
index 324e2728f59d0..669dff4b97f44 100644
--- a/packages/editor/src/browser/editor-widget.ts
+++ b/packages/editor/src/browser/editor-widget.ts
@@ -15,7 +15,7 @@
// *****************************************************************************
import { Disposable, SelectionService, Event, UNTITLED_SCHEME } from '@theia/core/lib/common';
-import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget } from '@theia/core/lib/browser';
+import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget, lock } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { TextEditor } from './editor';
@@ -27,6 +27,9 @@ export class EditorWidget extends BaseWidget implements SaveableSource, Navigata
) {
super(editor);
this.addClass('theia-editor');
+ if (editor.isReadonly) {
+ lock(this.title);
+ }
this.toDispose.push(this.editor);
this.toDispose.push(this.editor.onSelectionChanged(() => this.setSelection()));
this.toDispose.push(this.editor.onFocusChanged(() => this.setSelection()));
diff --git a/packages/editor/src/browser/editor.ts b/packages/editor/src/browser/editor.ts
index 95997a05bd2e1..ce0a2ce198257 100644
--- a/packages/editor/src/browser/editor.ts
+++ b/packages/editor/src/browser/editor.ts
@@ -196,6 +196,7 @@ export interface TextEditor extends Disposable, TextEditorSelection, Navigatable
readonly node: HTMLElement;
readonly uri: URI;
+ readonly isReadonly: boolean;
readonly document: TextEditorDocument;
readonly onDocumentContentChanged: Event;
diff --git a/packages/filesystem/src/browser/file-resource.ts b/packages/filesystem/src/browser/file-resource.ts
index b38fda0fb8e55..2db117ac45113 100644
--- a/packages/filesystem/src/browser/file-resource.ts
+++ b/packages/filesystem/src/browser/file-resource.ts
@@ -40,6 +40,7 @@ export namespace FileResourceVersion {
}
export interface FileResourceOptions {
+ isReadonly: boolean
shouldOverwrite: () => Promise
shouldOpenAsText: (error: string) => Promise
}
@@ -60,6 +61,9 @@ export class FileResource implements Resource {
get encoding(): string | undefined {
return this._version?.encoding;
}
+ get isReadonly(): boolean {
+ return this.options.isReadonly || this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Readonly);
+ }
constructor(
readonly uri: URI,
@@ -184,15 +188,7 @@ export class FileResource implements Resource {
}
}
- saveContents(content: string, options?: ResourceSaveOptions): Promise {
- return this.doWrite(content, options);
- }
-
- saveStream(content: Readable, options?: ResourceSaveOptions): Promise {
- return this.doWrite(content, options);
- }
-
- protected async doWrite(content: string | Readable, options?: ResourceSaveOptions): Promise {
+ protected doWrite = async (content: string | Readable, options?: ResourceSaveOptions): Promise => {
const version = options?.version || this._version;
const current = FileResourceVersion.is(version) ? version : undefined;
const etag = current?.etag;
@@ -218,14 +214,22 @@ export class FileResource implements Resource {
}
throw e;
}
- }
+ };
+ saveStream?: Resource['saveStream'];
+ saveContents?: Resource['saveContents'];
saveContentChanges?: Resource['saveContentChanges'];
protected updateSavingContentChanges(): void {
- if (this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Update)) {
- this.saveContentChanges = this.doSaveContentChanges;
- } else {
+ if (this.isReadonly) {
delete this.saveContentChanges;
+ delete this.saveContents;
+ delete this.saveStream;
+ } else {
+ this.saveContents = this.doWrite;
+ this.saveStream = this.doWrite;
+ if (this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Update)) {
+ this.saveContentChanges = this.doSaveContentChanges;
+ }
}
}
protected doSaveContentChanges: Resource['saveContentChanges'] = async (changes, options) => {
@@ -317,6 +321,7 @@ export class FileResourceResolver implements ResourceResolver {
throw new Error('The given uri is a directory: ' + this.labelProvider.getLongName(uri));
}
return new FileResource(uri, this.fileService, {
+ isReadonly: stat?.isReadonly ?? false,
shouldOverwrite: () => this.shouldOverwrite(uri),
shouldOpenAsText: error => this.shouldOpenAsText(uri, error)
});
diff --git a/packages/filesystem/src/browser/file-service.ts b/packages/filesystem/src/browser/file-service.ts
index ffc0100cdf93c..c9c59cbfc78b1 100644
--- a/packages/filesystem/src/browser/file-service.ts
+++ b/packages/filesystem/src/browser/file-service.ts
@@ -426,6 +426,16 @@ export class FileService {
return !!(provider && (provider.capabilities & capability));
}
+ /**
+ * List the schemes and capabilities for registered file system providers
+ */
+ listCapabilities(): { scheme: string; capabilities: FileSystemProviderCapabilities }[] {
+ return Array.from(this.providers.entries()).map(([scheme, provider]) => ({
+ scheme,
+ capabilities: provider.capabilities
+ }));
+ }
+
protected async withProvider(resource: URI): Promise {
// Assert path is absolute
if (!resource.path.isAbsolute) {
diff --git a/packages/filesystem/src/common/files.ts b/packages/filesystem/src/common/files.ts
index 2dfed8108bb9c..94d1dc781ead0 100644
--- a/packages/filesystem/src/common/files.ts
+++ b/packages/filesystem/src/common/files.ts
@@ -234,6 +234,11 @@ export interface FileStat extends BaseStat {
*/
isSymbolicLink: boolean;
+ /**
+ * The resource is read only.
+ */
+ isReadonly: boolean;
+
/**
* The children of the file stat or undefined if none.
*/
@@ -277,6 +282,7 @@ export namespace FileStat {
isFile: (stat.type & FileType.File) !== 0,
isDirectory: (stat.type & FileType.Directory) !== 0,
isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0,
+ isReadonly: !!stat.permissions && (stat.permissions & FilePermission.Readonly) !== 0,
mtime: stat.mtime,
ctime: stat.ctime,
size: stat.size,
@@ -485,6 +491,14 @@ export enum FileType {
SymbolicLink = 64
}
+export enum FilePermission {
+
+ /**
+ * File is readonly.
+ */
+ Readonly = 1
+}
+
export interface Stat {
type: FileType;
@@ -499,6 +513,8 @@ export interface Stat {
ctime: number;
size: number;
+
+ permissions?: FilePermission;
}
export interface WatchOptions {
diff --git a/packages/monaco/src/browser/monaco-editor.ts b/packages/monaco/src/browser/monaco-editor.ts
index 5c28658e28b70..96a7d0a5f53c6 100644
--- a/packages/monaco/src/browser/monaco-editor.ts
+++ b/packages/monaco/src/browser/monaco-editor.ts
@@ -220,6 +220,10 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor {
return this.onDocumentContentChangedEmitter.event;
}
+ get isReadonly(): boolean {
+ return this.document.isReadonly();
+ }
+
get cursor(): Position {
const { lineNumber, column } = this.editor.getPosition()!;
return this.m2p.asPosition(lineNumber, column);
diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts
index d35b53d04758f..6f8ec9fd68506 100644
--- a/packages/plugin-ext/src/common/plugin-api-rpc.ts
+++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts
@@ -1835,6 +1835,7 @@ export interface DebugMain {
}
export interface FileSystemExt {
+ $acceptProviderInfos(scheme: string, capabilities?: files.FileSystemProviderCapabilities): void;
$stat(handle: number, resource: UriComponents): Promise;
$readdir(handle: number, resource: UriComponents): Promise<[string, files.FileType][]>;
$readFile(handle: number, resource: UriComponents): Promise;
diff --git a/packages/plugin-ext/src/main/browser/file-system-main-impl.ts b/packages/plugin-ext/src/main/browser/file-system-main-impl.ts
index cebfd8d7e95f9..126cb3f8d06bc 100644
--- a/packages/plugin-ext/src/main/browser/file-system-main-impl.ts
+++ b/packages/plugin-ext/src/main/browser/file-system-main-impl.ts
@@ -27,7 +27,7 @@ import { URI } from '@theia/core/shared/vscode-uri';
import { interfaces } from '@theia/core/shared/inversify';
import CoreURI from '@theia/core/lib/common/uri';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
-import { Disposable } from '@theia/core/lib/common/disposable';
+import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { MAIN_RPC_CONTEXT, FileSystemMain, FileSystemExt, IFileChangeDto } from '../../common/plugin-api-rpc';
import { RPCProtocol } from '../../common/rpc-protocol';
@@ -46,15 +46,24 @@ export class FileSystemMainImpl implements FileSystemMain, Disposable {
private readonly _proxy: FileSystemExt;
private readonly _fileProvider = new Map();
private readonly _fileService: FileService;
+ private readonly _disposables = new DisposableCollection();
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this._proxy = rpc.getProxy(MAIN_RPC_CONTEXT.FILE_SYSTEM_EXT);
this._fileService = container.get(FileService);
+
+ for (const { scheme, capabilities } of this._fileService.listCapabilities()) {
+ this._proxy.$acceptProviderInfos(scheme, capabilities);
+ }
+
+ this._disposables.push(this._fileService.onDidChangeFileSystemProviderRegistrations(e => this._proxy.$acceptProviderInfos(e.scheme, e.provider?.capabilities)));
+ this._disposables.push(this._fileService.onDidChangeFileSystemProviderCapabilities(e => this._proxy.$acceptProviderInfos(e.scheme, e.provider.capabilities)));
+ this._disposables.push(Disposable.create(() => this._fileProvider.forEach(value => value.dispose())));
+ this._disposables.push(Disposable.create(() => this._fileProvider.clear()));
}
dispose(): void {
- this._fileProvider.forEach(value => value.dispose());
- this._fileProvider.clear();
+ this._disposables.dispose();
}
$registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities): void {
diff --git a/packages/plugin-ext/src/plugin/file-system-ext-impl.ts b/packages/plugin-ext/src/plugin/file-system-ext-impl.ts
index 9d771c00322a4..a335a1f03e794 100644
--- a/packages/plugin-ext/src/plugin/file-system-ext-impl.ts
+++ b/packages/plugin-ext/src/plugin/file-system-ext-impl.ts
@@ -137,7 +137,7 @@ class FsLinkProvider {
class ConsumerFileSystem implements vscode.FileSystem {
- constructor(private _proxy: FileSystemMain) { }
+ constructor(private _proxy: FileSystemMain, private _capabilities: Map) { }
stat(uri: vscode.Uri): Promise {
return this._proxy.$stat(uri).catch(ConsumerFileSystem._handleError);
@@ -148,7 +148,7 @@ class ConsumerFileSystem implements vscode.FileSystem {
createDirectory(uri: vscode.Uri): Promise {
return this._proxy.$mkdir(uri).catch(ConsumerFileSystem._handleError);
}
- async readFile(uri: vscode.Uri): Promise {
+ readFile(uri: vscode.Uri): Promise {
return this._proxy.$readFile(uri).then(buff => buff.buffer).catch(ConsumerFileSystem._handleError);
}
writeFile(uri: vscode.Uri, content: Uint8Array): Promise {
@@ -163,6 +163,13 @@ class ConsumerFileSystem implements vscode.FileSystem {
copy(source: vscode.Uri, destination: vscode.Uri, options?: { overwrite?: boolean }): Promise {
return this._proxy.$copy(source, destination, { ...{ overwrite: false }, ...options }).catch(ConsumerFileSystem._handleError);
}
+ isWritableFileSystem(scheme: string): boolean | undefined {
+ const capabilities = this._capabilities.get(scheme);
+ if (typeof capabilities === 'number') {
+ return (capabilities & files.FileSystemProviderCapabilities.Readonly) === 0;
+ }
+ return undefined;
+ }
private static _handleError(err: any): never {
// generic error
if (!(err instanceof Error)) {
@@ -193,6 +200,7 @@ export class FileSystemExtImpl implements FileSystemExt {
private readonly _proxy: FileSystemMain;
private readonly _linkProvider = new FsLinkProvider();
private readonly _fsProvider = new Map();
+ private readonly _capabilities = new Map();
private readonly _usedSchemes = new Set();
private readonly _watches = new Map();
@@ -203,7 +211,7 @@ export class FileSystemExtImpl implements FileSystemExt {
constructor(rpc: RPCProtocol, private _extHostLanguageFeatures: LanguagesExtImpl) {
this._proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.FILE_SYSTEM_MAIN);
- this.fileSystem = new ConsumerFileSystem(this._proxy);
+ this.fileSystem = new ConsumerFileSystem(this._proxy, this._capabilities);
// register used schemes
Object.keys(Schemas).forEach(scheme => this._usedSchemes.add(scheme));
@@ -295,8 +303,16 @@ export class FileSystemExtImpl implements FileSystemExt {
}
private static _asIStat(stat: vscode.FileStat): files.Stat {
- const { type, ctime, mtime, size } = stat;
- return { type, ctime, mtime, size };
+ const { type, ctime, mtime, size, permissions } = stat;
+ return { type, ctime, mtime, size, permissions };
+ }
+
+ $acceptProviderInfos(scheme: string, capabilities?: files.FileSystemProviderCapabilities): void {
+ if (typeof capabilities === 'number') {
+ this._capabilities.set(scheme, capabilities);
+ } else {
+ this._capabilities.delete(scheme);
+ }
}
$stat(handle: number, resource: UriComponents): Promise {
diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts
index 0fee9f59bec5f..69432c6f3f47d 100644
--- a/packages/plugin-ext/src/plugin/plugin-context.ts
+++ b/packages/plugin-ext/src/plugin/plugin-context.ts
@@ -204,6 +204,7 @@ import { CustomEditorsExtImpl } from './custom-editors';
import { WebviewViewsExtImpl } from './webview-views';
import { PluginPackage } from '../common';
import { Endpoint } from '@theia/core/lib/browser/endpoint';
+import { FilePermission } from '@theia/filesystem/lib/common/files';
export function createAPIFactory(
rpc: RPCProtocol,
@@ -602,8 +603,8 @@ export function createAPIFactory(
registerTextDocumentContentProvider(scheme: string, provider: theia.TextDocumentContentProvider): theia.Disposable {
return workspaceExt.registerTextDocumentContentProvider(scheme, provider);
},
- registerFileSystemProvider(scheme: string, provider: theia.FileSystemProvider): theia.Disposable {
- return fileSystemExt.registerFileSystemProvider(scheme, provider);
+ registerFileSystemProvider(scheme: string, provider: theia.FileSystemProvider, options?: { isCaseSensitive?: boolean, isReadonly?: boolean }): theia.Disposable {
+ return fileSystemExt.registerFileSystemProvider(scheme, provider, options);
},
getWorkspaceFolder(uri: theia.Uri): theia.WorkspaceFolder | undefined {
return workspaceExt.getWorkspaceFolder(uri);
@@ -1071,6 +1072,7 @@ export function createAPIFactory(
WorkspaceEdit,
SymbolInformation,
FileType,
+ FilePermission,
FileChangeType,
ShellQuoting,
ShellExecution,
diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts
index 5d92ddc800d8f..0422b0d005d7f 100644
--- a/packages/plugin/src/theia.d.ts
+++ b/packages/plugin/src/theia.d.ts
@@ -5962,6 +5962,18 @@ export module '@theia/plugin' {
SymbolicLink = 64
}
+ export enum FilePermission {
+ /**
+ * The file is readonly.
+ *
+ * *Note:* All `FileStat` from a `FileSystemProvider` that is registered with
+ * the option `isReadonly: true` will be implicitly handled as if `FilePermission.Readonly`
+ * is set. As a consequence, it is not possible to have a readonly file system provider
+ * registered where some `FileStat` are not readonly.
+ */
+ Readonly = 1
+ }
+
/**
* The `FileStat`-type represents metadata about a file
*/
@@ -5993,6 +6005,12 @@ export module '@theia/plugin' {
* example.
*/
size: number;
+ /**
+ * The permissions of the file, e.g. whether the file is readonly.
+ *
+ * *Note:* This value might be a bitmask, e.g. `FilePermission.Readonly | FilePermission.Other`.
+ */
+ permissions?: FilePermission;
}
/**
@@ -6302,6 +6320,21 @@ export module '@theia/plugin' {
* @param options Defines if existing files should be overwritten.
*/
copy(source: Uri, target: Uri, options?: { overwrite?: boolean }): Thenable;
+
+ /**
+ * Check if a given file system supports writing files.
+ *
+ * Keep in mind that just because a file system supports writing, that does
+ * not mean that writes will always succeed. There may be permissions issues
+ * or other errors that prevent writing a file.
+ *
+ * @param scheme The scheme of the filesystem, for example `file` or `git`.
+ *
+ * @return `true` if the file system supports writing, `false` if it does not
+ * support writing (i.e. it is readonly), and `undefined` if the editor does not
+ * know about the filesystem.
+ */
+ isWritableFileSystem(scheme: string): boolean | undefined;
}
/**
diff --git a/packages/property-view/src/browser/resource-property-view/resource-property-data-service.spec.ts b/packages/property-view/src/browser/resource-property-view/resource-property-data-service.spec.ts
index 8d8f64700cd9a..bd38dcf1c6ec6 100644
--- a/packages/property-view/src/browser/resource-property-view/resource-property-data-service.spec.ts
+++ b/packages/property-view/src/browser/resource-property-view/resource-property-data-service.spec.ts
@@ -39,6 +39,7 @@ const mockFileStat: FileStat = {
isFile: false,
isDirectory: true,
isSymbolicLink: false,
+ isReadonly: false,
resource: new URI('resource'),
name: 'name'
};
diff --git a/packages/workspace/src/browser/workspace-commands.spec.ts b/packages/workspace/src/browser/workspace-commands.spec.ts
index a41e0a0ea4d93..68a369b0bd8bb 100644
--- a/packages/workspace/src/browser/workspace-commands.spec.ts
+++ b/packages/workspace/src/browser/workspace-commands.spec.ts
@@ -48,6 +48,7 @@ describe('workspace-commands', () => {
isFile: true,
isDirectory: false,
isSymbolicLink: false,
+ isReadonly: false,
resource: new URI('foo/bar'),
name: 'bar',
};
@@ -56,6 +57,7 @@ describe('workspace-commands', () => {
isFile: false,
isDirectory: true,
isSymbolicLink: false,
+ isReadonly: false,
resource: new URI('foo'),
name: 'foo',
children: [