Skip to content

Commit 45b57b3

Browse files
authored
Merge pull request microsoft#181708 from microsoft/ben/readonly-files
Allow to mark files as readonly (microsoft#4873)
2 parents 5820f8e + 0cf09a1 commit 45b57b3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+673
-143
lines changed

.vscode/settings.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
},
1818
"search.exclude": {
1919
"**/node_modules": true,
20-
"**/bower_components": true,
2120
"cli/target/**": true,
2221
".build/**": true,
2322
"out/**": true,
@@ -34,6 +33,18 @@
3433
"src/vs/workbench/api/test/browser/extHostDocumentData.test.perf-data.ts": true,
3534
"src/vs/editor/test/node/diffing/fixtures/**": true,
3635
},
36+
"files.readonlyInclude": {
37+
"**/node_modules/**": true,
38+
"out/**": true,
39+
"out-build/**": true,
40+
"out-vscode/**": true,
41+
"out-vscode-reh/**": true,
42+
"extensions/**/dist/**": true,
43+
"extensions/**/out/**": true,
44+
"test/smoke/out/**": true,
45+
"test/automation/out/**": true,
46+
"test/integration/browser/out/**": true,
47+
},
3748
"lcov.path": [
3849
"./.build/coverage/lcov.info",
3950
"./.build/coverage-single/lcov.info"

src/vs/base/test/common/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri';
1010

1111
export type ValueCallback<T = any> = (value: T | Promise<T>) => void;
1212

13-
export function toResource(this: any, path: string) {
13+
export function toResource(this: any, path: string): URI {
1414
if (isWindows) {
1515
return URI.file(join('C:\\', btoa(this.test.fullTitle()), path));
1616
}

src/vs/platform/files/common/fileService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ export class FileService extends Disposable implements IFileService {
249249
ctime: stat.ctime,
250250
size: stat.size,
251251
readonly: Boolean((stat.permissions ?? 0) & FilePermission.Readonly) || Boolean(provider.capabilities & FileSystemProviderCapabilities.Readonly),
252+
locked: Boolean((stat.permissions ?? 0) & FilePermission.Locked),
252253
etag: etag({ mtime: stat.mtime, size: stat.size }),
253254
children: undefined
254255
};

src/vs/platform/files/common/files.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,9 +394,17 @@ export enum FileType {
394394
export enum FilePermission {
395395

396396
/**
397-
* File is readonly.
397+
* File is readonly. Components like editors should not
398+
* offer to edit the contents.
398399
*/
399-
Readonly = 1
400+
Readonly = 1,
401+
402+
/**
403+
* File is locked. Components like editors should offer
404+
* to edit the contents and ask the user upon saving to
405+
* remove the lock.
406+
*/
407+
Locked = 2
400408
}
401409

402410
export interface IStat {
@@ -1008,9 +1016,17 @@ interface IBaseFileStat {
10081016
readonly etag?: string;
10091017

10101018
/**
1011-
* The file is read-only.
1019+
* File is readonly. Components like editors should not
1020+
* offer to edit the contents.
10121021
*/
10131022
readonly readonly?: boolean;
1023+
1024+
/**
1025+
* File is locked. Components like editors should offer
1026+
* to edit the contents and ask the user upon saving to
1027+
* remove the lock.
1028+
*/
1029+
readonly locked?: boolean;
10141030
}
10151031

10161032
export interface IBaseFileStatWithMetadata extends Required<IBaseFileStat> { }
@@ -1050,6 +1066,7 @@ export interface IFileStatWithMetadata extends IFileStat, IBaseFileStatWithMetad
10501066
readonly etag: string;
10511067
readonly size: number;
10521068
readonly readonly: boolean;
1069+
readonly locked: boolean;
10531070
readonly children: IFileStatWithMetadata[] | undefined;
10541071
}
10551072

@@ -1229,12 +1246,19 @@ export const HotExitConfiguration = {
12291246

12301247
export const FILES_ASSOCIATIONS_CONFIG = 'files.associations';
12311248
export const FILES_EXCLUDE_CONFIG = 'files.exclude';
1249+
export const FILES_READONLY_INCLUDE_CONFIG = 'files.readonlyInclude';
1250+
export const FILES_READONLY_EXCLUDE_CONFIG = 'files.readonlyExclude';
1251+
export const FILES_READONLY_FROM_PERMISSIONS_CONFIG = 'files.readonlyFromPermissions';
1252+
1253+
export interface IGlobPatterns {
1254+
[filepattern: string]: boolean;
1255+
}
12321256

12331257
export interface IFilesConfiguration {
12341258
files: {
12351259
associations: { [filepattern: string]: string };
12361260
exclude: IExpression;
1237-
watcherExclude: { [filepattern: string]: boolean };
1261+
watcherExclude: IGlobPatterns;
12381262
watcherInclude: string[];
12391263
encoding: string;
12401264
autoGuessEncoding: boolean;
@@ -1246,6 +1270,9 @@ export interface IFilesConfiguration {
12461270
enableTrash: boolean;
12471271
hotExit: string;
12481272
saveConflictResolution: 'askUser' | 'overwriteFileOnDisk';
1273+
readonlyInclude: IGlobPatterns;
1274+
readonlyExclude: IGlobPatterns;
1275+
readonlyFromPermissions: boolean;
12491276
};
12501277
}
12511278

src/vs/platform/files/node/diskFileSystemProvider.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream'
1919
import { URI } from 'vs/base/common/uri';
2020
import { IDirent, Promises, RimRafMode, SymlinkSupport } from 'vs/base/node/pfs';
2121
import { localize } from 'vs/nls';
22-
import { createFileSystemProviderError, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat } from 'vs/platform/files/common/files';
22+
import { createFileSystemProviderError, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat, FilePermission } from 'vs/platform/files/common/files';
2323
import { readFileIntoStream } from 'vs/platform/files/common/io';
2424
import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, IDiskFileChange, ILogMessage } from 'vs/platform/files/common/watcher';
2525
import { ILogService } from 'vs/platform/log/common/log';
@@ -93,7 +93,8 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
9393
type: this.toType(stat, symbolicLink),
9494
ctime: stat.birthtime.getTime(), // intentionally not using ctime here, we want the creation time
9595
mtime: stat.mtime.getTime(),
96-
size: stat.size
96+
size: stat.size,
97+
permissions: (stat.mode & 0o200) === 0 ? FilePermission.Locked : undefined
9798
};
9899
} catch (error) {
99100
throw this.toFileSystemProviderError(error);

src/vs/platform/files/test/node/diskFileService.integrationTest.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2195,11 +2195,15 @@ flakySuite('Disk File Service', function () {
21952195
async function testLockedFiles(expectError: boolean) {
21962196
const lockedFile = URI.file(join(testDir, 'my-locked-file'));
21972197

2198-
await service.writeFile(lockedFile, VSBuffer.fromString('Locked File'));
2198+
const content = await service.writeFile(lockedFile, VSBuffer.fromString('Locked File'));
2199+
assert.strictEqual(content.locked, false);
21992200

22002201
const stats = await Promises.stat(lockedFile.fsPath);
22012202
await Promises.chmod(lockedFile.fsPath, stats.mode & ~0o200);
22022203

2204+
let stat = await service.stat(lockedFile);
2205+
assert.strictEqual(stat.locked, true);
2206+
22032207
let error;
22042208
const newContent = 'Updates to locked file';
22052209
try {
@@ -2222,6 +2226,9 @@ flakySuite('Disk File Service', function () {
22222226
} else {
22232227
await service.writeFile(lockedFile, VSBuffer.fromString(newContent), { unlock: true });
22242228
assert.strictEqual(readFileSync(lockedFile.fsPath).toString(), newContent);
2229+
2230+
stat = await service.stat(lockedFile);
2231+
assert.strictEqual(stat.locked, false);
22252232
}
22262233
}
22272234

src/vs/workbench/api/common/extHostFileSystemConsumer.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,23 @@ export class ExtHostConsumerFileSystem {
3232
this.value = Object.freeze({
3333
async stat(uri: vscode.Uri): Promise<vscode.FileStat> {
3434
try {
35+
let stat;
36+
3537
const provider = that._fileSystemProvider.get(uri.scheme);
36-
if (!provider) {
37-
return await that._proxy.$stat(uri);
38+
if (provider) {
39+
// use shortcut
40+
await that._proxy.$ensureActivation(uri.scheme);
41+
stat = await provider.stat(uri);
42+
} else {
43+
stat = await that._proxy.$stat(uri);
3844
}
39-
// use shortcut
40-
await that._proxy.$ensureActivation(uri.scheme);
41-
const stat = await provider.stat(uri);
42-
return <vscode.FileStat>{
45+
46+
return {
4347
type: stat.type,
4448
ctime: stat.ctime,
4549
mtime: stat.mtime,
4650
size: stat.size,
47-
permissions: stat.permissions
51+
permissions: stat.permissions === files.FilePermission.Readonly ? 1 : undefined
4852
};
4953
} catch (err) {
5054
ExtHostConsumerFileSystem._handleError(err);

src/vs/workbench/api/node/extHostDiskFileSystemProvider.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileS
88
import { Schemas } from 'vs/base/common/network';
99
import { ILogService } from 'vs/platform/log/common/log';
1010
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
11+
import { FilePermission } from 'vs/platform/files/common/files';
1112

1213
export class ExtHostDiskFileSystemProvider {
1314

@@ -29,8 +30,16 @@ class DiskFileSystemProviderAdapter implements vscode.FileSystemProvider {
2930

3031
constructor(private readonly logService: ILogService) { }
3132

32-
stat(uri: vscode.Uri): Promise<vscode.FileStat> {
33-
return this.impl.stat(uri);
33+
async stat(uri: vscode.Uri): Promise<vscode.FileStat> {
34+
const stat = await this.impl.stat(uri);
35+
36+
return {
37+
type: stat.type,
38+
ctime: stat.ctime,
39+
mtime: stat.mtime,
40+
size: stat.size,
41+
permissions: stat.permissions === FilePermission.Readonly ? 1 : undefined
42+
};
3443
}
3544

3645
readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> {

src/vs/workbench/browser/contextkeys.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { Event } from 'vs/base/common/event';
77
import { Disposable } from 'vs/base/common/lifecycle';
88
import { IContextKeyService, IContextKey, setConstant as setConstantContextKey } from 'vs/platform/contextkey/common/contextkey';
99
import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext, ProductQualityContext, IsMobileContext } from 'vs/platform/contextkey/common/contextkeys';
10-
import { SplitEditorsVertically, InEditorZenModeContext, ActiveEditorCanRevertContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, EditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext } from 'vs/workbench/common/contextkeys';
11-
import { TEXT_DIFF_EDITOR_ID, EditorInputCapabilities, SIDE_BY_SIDE_EDITOR_ID, DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor';
10+
import { SplitEditorsVertically, InEditorZenModeContext, ActiveEditorCanRevertContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, EditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, ActiveEditorCanToggleReadonlyContext } from 'vs/workbench/common/contextkeys';
11+
import { TEXT_DIFF_EDITOR_ID, EditorInputCapabilities, SIDE_BY_SIDE_EDITOR_ID, DEFAULT_EDITOR_ASSOCIATION, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor';
1212
import { trackFocus, addDisposableListener, EventType } from 'vs/base/browser/dom';
1313
import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
1414
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -25,18 +25,21 @@ import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/b
2525
import { Schemas } from 'vs/base/common/network';
2626
import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess';
2727
import { IProductService } from 'vs/platform/product/common/productService';
28+
import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files';
2829

2930
export class WorkbenchContextKeysHandler extends Disposable {
3031
private inputFocusedContext: IContextKey<boolean>;
3132

3233
private dirtyWorkingCopiesContext: IContextKey<boolean>;
3334

3435
private activeEditorContext: IContextKey<string | null>;
35-
private activeEditorIsReadonly: IContextKey<boolean>;
3636
private activeEditorCanRevert: IContextKey<boolean>;
3737
private activeEditorCanSplitInGroup: IContextKey<boolean>;
3838
private activeEditorAvailableEditorIds: IContextKey<string>;
3939

40+
private activeEditorIsReadonly: IContextKey<boolean>;
41+
private activeEditorCanToggleReadonly: IContextKey<boolean>;
42+
4043
private activeEditorGroupEmpty: IContextKey<boolean>;
4144
private activeEditorGroupIndex: IContextKey<number>;
4245
private activeEditorGroupLast: IContextKey<boolean>;
@@ -84,7 +87,8 @@ export class WorkbenchContextKeysHandler extends Disposable {
8487
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
8588
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
8689
@IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService,
87-
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService
90+
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
91+
@IFileService private readonly fileService: IFileService
8892
) {
8993
super();
9094

@@ -119,6 +123,7 @@ export class WorkbenchContextKeysHandler extends Disposable {
119123
// Editors
120124
this.activeEditorContext = ActiveEditorContext.bindTo(this.contextKeyService);
121125
this.activeEditorIsReadonly = ActiveEditorReadonlyContext.bindTo(this.contextKeyService);
126+
this.activeEditorCanToggleReadonly = ActiveEditorCanToggleReadonlyContext.bindTo(this.contextKeyService);
122127
this.activeEditorCanRevert = ActiveEditorCanRevertContext.bindTo(this.contextKeyService);
123128
this.activeEditorCanSplitInGroup = ActiveEditorCanSplitInGroupContext.bindTo(this.contextKeyService);
124129
this.activeEditorAvailableEditorIds = ActiveEditorAvailableEditorIdsContext.bindTo(this.contextKeyService);
@@ -289,22 +294,26 @@ export class WorkbenchContextKeysHandler extends Disposable {
289294

290295
if (activeEditorPane) {
291296
this.activeEditorContext.set(activeEditorPane.getId());
292-
this.activeEditorIsReadonly.set(activeEditorPane.input.hasCapability(EditorInputCapabilities.Readonly));
293297
this.activeEditorCanRevert.set(!activeEditorPane.input.hasCapability(EditorInputCapabilities.Untitled));
294298
this.activeEditorCanSplitInGroup.set(activeEditorPane.input.hasCapability(EditorInputCapabilities.CanSplitInGroup));
295299

296300
const activeEditorResource = activeEditorPane.input.resource;
297301
const editors = activeEditorResource ? this.editorResolverService.getEditors(activeEditorResource).map(editor => editor.id) : [];
298-
// Non text editor untitled files cannot be easily serialized between extensions
299-
// so instead we disable this context key to prevent common commands that act on the active editor
300302
if (activeEditorResource?.scheme === Schemas.untitled && activeEditorPane.input.editorId !== DEFAULT_EDITOR_ASSOCIATION.id) {
303+
// Non text editor untitled files cannot be easily serialized between extensions
304+
// so instead we disable this context key to prevent common commands that act on the active editor
301305
this.activeEditorAvailableEditorIds.set('');
302306
} else {
303307
this.activeEditorAvailableEditorIds.set(editors.join(','));
304308
}
309+
310+
this.activeEditorIsReadonly.set(activeEditorPane.input.hasCapability(EditorInputCapabilities.Readonly));
311+
const primaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.PRIMARY });
312+
this.activeEditorCanToggleReadonly.set(!!primaryEditorResource && this.fileService.hasProvider(primaryEditorResource) && !this.fileService.hasCapability(primaryEditorResource, FileSystemProviderCapabilities.Readonly));
305313
} else {
306314
this.activeEditorContext.reset();
307315
this.activeEditorIsReadonly.reset();
316+
this.activeEditorCanToggleReadonly.reset();
308317
this.activeEditorCanRevert.reset();
309318
this.activeEditorCanSplitInGroup.reset();
310319
this.activeEditorAvailableEditorIds.reset();

src/vs/workbench/browser/workbench.contribution.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,9 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
317317
},
318318
'workbench.localHistory.exclude': {
319319
'type': 'object',
320+
'patternProperties': {
321+
'.*': { 'type': 'boolean' }
322+
},
320323
'markdownDescription': localize('exclude', "Configure [glob patterns](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options) for excluding files from the local file history. Changing this setting has no effect on existing local file history entries."),
321324
'scope': ConfigurationScope.RESOURCE
322325
},

0 commit comments

Comments
 (0)