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

Add a workbench.editorLargeFileConfirmation setting #169811

Merged
merged 18 commits into from Dec 27, 2022
Merged
2 changes: 0 additions & 2 deletions src/vs/base/browser/ui/button/button.ts
Expand Up @@ -401,7 +401,5 @@ export class ButtonBar extends Disposable {
}

}));

}

}
29 changes: 19 additions & 10 deletions src/vs/platform/files/common/fileService.ts
Expand Up @@ -18,7 +18,7 @@ import { extUri, extUriIgnorePathCase, IExtUri, isAbsolutePath } from 'vs/base/c
import { consumeStream, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, transform } from 'vs/base/common/stream';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability } from 'vs/platform/files/common/files';
import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability, TooLargeFileOperationError } from 'vs/platform/files/common/files';
import { readFileIntoStream } from 'vs/platform/files/common/io';
import { ILogService } from 'vs/platform/log/common/log';

Expand Down Expand Up @@ -566,21 +566,30 @@ export class FileService extends Disposable implements IFileService {

// Re-throw errors as file operation errors but preserve
// specific errors (such as not modified since)
const message = localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString());
if (error instanceof NotModifiedSinceFileOperationError) {
throw new NotModifiedSinceFileOperationError(message, error.stat, options);
} else {
throw new FileOperationError(message, toFileOperationResult(error), options);
}
throw this.restoreReadError(error, resource, options);
}
}

private restoreReadError(error: Error, resource: URI, options?: IReadFileStreamOptions): FileOperationError {
const message = localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString());

if (error instanceof NotModifiedSinceFileOperationError) {
return new NotModifiedSinceFileOperationError(message, error.stat, options);
}

if (error instanceof TooLargeFileOperationError) {
return new TooLargeFileOperationError(message, error.fileOperationResult, error.size, error.options);
}

return new FileOperationError(message, toFileOperationResult(error), options);
}

private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
const fileStream = provider.readFileStream(resource, options, token);

return transform(fileStream, {
data: data => data instanceof VSBuffer ? data : VSBuffer.wrap(data),
error: error => new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options)
error: error => this.restoreReadError(error, resource, options)
}, data => VSBuffer.concat(data));
}

Expand All @@ -590,7 +599,7 @@ export class FileService extends Disposable implements IFileService {
readFileIntoStream(provider, resource, stream, data => data, {
...options,
bufferSize: this.BUFFER_SIZE,
errorTransformer: error => new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options)
errorTransformer: error => this.restoreReadError(error, resource, options)
}, token);

return stream;
Expand Down Expand Up @@ -666,7 +675,7 @@ export class FileService extends Disposable implements IFileService {
}

if (typeof tooLargeErrorResult === 'number') {
throw new FileOperationError(localize('fileTooLargeError', "Unable to read file '{0}' that is too large to open", this.resourceForError(resource)), tooLargeErrorResult);
throw new TooLargeFileOperationError(localize('fileTooLargeError', "Unable to read file '{0}' that is too large to open", this.resourceForError(resource)), tooLargeErrorResult, size, options);
}
}
}
Expand Down
34 changes: 29 additions & 5 deletions src/vs/platform/files/common/files.ts
Expand Up @@ -284,6 +284,21 @@ export interface IFileAtomicReadOptions {
readonly atomic: true;
}

export interface IFileReadLimits {

/**
* If the file exceeds the given size, an error of kind
* `FILE_TOO_LARGE` will be thrown.
*/
size?: number;

/**
* If the file exceeds the given size, an error of kind
* `FILE_EXCEEDS_MEMORY_LIMIT` will be thrown.
*/
memory?: number;
}

export interface IFileReadStreamOptions {

/**
Expand All @@ -299,12 +314,10 @@ export interface IFileReadStreamOptions {
readonly length?: number;

/**
* If provided, the size of the file will be checked against the limits.
* If provided, the size of the file will be checked against the limits
* and an error will be thrown if any limit is exceeded.
*/
limits?: {
readonly size?: number;
readonly memory?: number;
};
readonly limits?: IFileReadLimits;
}

export interface IFileWriteOptions extends IFileOverwriteOptions, IFileUnlockOptions {
Expand Down Expand Up @@ -1158,6 +1171,17 @@ export class FileOperationError extends Error {
}
}

export class TooLargeFileOperationError extends FileOperationError {
constructor(
message: string,
override readonly fileOperationResult: FileOperationResult.FILE_TOO_LARGE | FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT,
readonly size: number,
options?: IReadFileOptions
) {
super(message, fileOperationResult, options);
}
}

export class NotModifiedSinceFileOperationError extends FileOperationError {

constructor(
Expand Down
23 changes: 15 additions & 8 deletions src/vs/platform/files/test/node/diskFileService.test.ts
Expand Up @@ -16,7 +16,7 @@ import { joinPath } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { Promises } from 'vs/base/node/pfs';
import { flakySuite, getPathFromAmdModule, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { etag, IFileAtomicReadOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, hasFileAtomicReadCapability, hasOpenReadWriteCloseCapability, IFileStat, IFileStatWithMetadata, IReadFileOptions, IStat, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files';
import { etag, IFileAtomicReadOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, hasFileAtomicReadCapability, hasOpenReadWriteCloseCapability, IFileStat, IFileStatWithMetadata, IReadFileOptions, IStat, NotModifiedSinceFileOperationError, TooLargeFileOperationError } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { NullLogService } from 'vs/platform/log/common/log';
Expand Down Expand Up @@ -1643,14 +1643,14 @@ flakySuite('Disk File Service', function () {
});

async function testFileExceedsMemoryLimit() {
await doTestFileExceedsMemoryLimit();
await doTestFileExceedsMemoryLimit(false);

// Also test when the stat size is wrong
fileProvider.setSmallStatSize(true);
return doTestFileExceedsMemoryLimit();
return doTestFileExceedsMemoryLimit(true);
}

async function doTestFileExceedsMemoryLimit() {
async function doTestFileExceedsMemoryLimit(statSizeWrong: boolean) {
const resource = URI.file(join(testDir, 'index.html'));

let error: FileOperationError | undefined = undefined;
Expand All @@ -1661,6 +1661,10 @@ flakySuite('Disk File Service', function () {
}

assert.ok(error);
if (!statSizeWrong) {
assert.ok(error instanceof TooLargeFileOperationError);
assert.ok(typeof error.size === 'number');
}
assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT);
}

Expand All @@ -1687,14 +1691,14 @@ flakySuite('Disk File Service', function () {
});

async function testFileTooLarge() {
await doTestFileTooLarge();
await doTestFileTooLarge(false);

// Also test when the stat size is wrong
fileProvider.setSmallStatSize(true);
return doTestFileTooLarge();
return doTestFileTooLarge(true);
}

async function doTestFileTooLarge() {
async function doTestFileTooLarge(statSizeWrong: boolean) {
const resource = URI.file(join(testDir, 'index.html'));

let error: FileOperationError | undefined = undefined;
Expand All @@ -1704,7 +1708,10 @@ flakySuite('Disk File Service', function () {
error = err;
}

assert.ok(error);
if (!statSizeWrong) {
assert.ok(error instanceof TooLargeFileOperationError);
assert.ok(typeof error.size === 'number');
}
assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_TOO_LARGE);
}

Expand Down
10 changes: 4 additions & 6 deletions src/vs/workbench/browser/parts/editor/binaryEditor.ts
Expand Up @@ -12,7 +12,6 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ByteSize } from 'vs/platform/files/common/files';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { EditorPlaceholder, IEditorPlaceholderContents } from 'vs/workbench/browser/parts/editor/editorPlaceholder';

export interface IOpenCallbacks {
Expand All @@ -37,18 +36,17 @@ export abstract class BaseBinaryResourceEditor extends EditorPlaceholder {
private readonly callbacks: IOpenCallbacks,
telemetryService: ITelemetryService,
themeService: IThemeService,
@IStorageService storageService: IStorageService,
@IInstantiationService instantiationService: IInstantiationService
@IStorageService storageService: IStorageService
) {
super(id, telemetryService, themeService, storageService, instantiationService);
super(id, telemetryService, themeService, storageService);
}

override getTitle(): string {
return this.input ? this.input.getName() : localize('binaryEditor', "Binary Viewer");
}

protected async getContents(input: EditorInput, options: IEditorOptions): Promise<IEditorPlaceholderContents> {
const model = await input.resolve();
const model = await input.resolve(options);

// Assert Model instance
if (!(model instanceof BinaryEditorModel)) {
Expand All @@ -61,7 +59,7 @@ export abstract class BaseBinaryResourceEditor extends EditorPlaceholder {

return {
icon: '$(warning)',
label: localize('binaryError', "The file is not displayed in the editor because it is either binary or uses an unsupported text encoding."),
label: localize('binaryError', "The file is not displayed in the text editor because it is either binary or uses an unsupported text encoding."),
actions: [
{
label: localize('openAnyway', "Open Anyway"),
Expand Down
27 changes: 24 additions & 3 deletions src/vs/workbench/browser/parts/editor/editorConfiguration.ts
Expand Up @@ -7,13 +7,15 @@ import { localize } from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { Disposable } from 'vs/base/common/lifecycle';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationNode, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration';
import { IEditorResolverService, RegisteredEditorInfo, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService';
import { IJSONSchemaMap } from 'vs/base/common/jsonSchema';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { coalesce } from 'vs/base/common/arrays';
import { Event } from 'vs/base/common/event';
import { isWeb } from 'vs/base/common/platform';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';

export class DynamicEditorConfigurations extends Disposable implements IWorkbenchContribution {

Expand Down Expand Up @@ -51,10 +53,12 @@ export class DynamicEditorConfigurations extends Disposable implements IWorkbenc
private autoLockConfigurationNode: IConfigurationNode | undefined;
private defaultBinaryEditorConfigurationNode: IConfigurationNode | undefined;
private editorAssociationsConfigurationNode: IConfigurationNode | undefined;
private editorLargeFileConfirmationConfigurationNode: IConfigurationNode | undefined;

constructor(
@IEditorResolverService private readonly editorResolverService: IEditorResolverService,
@IExtensionService extensionService: IExtensionService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService
) {
super();

Expand Down Expand Up @@ -144,16 +148,33 @@ export class DynamicEditorConfigurations extends Disposable implements IWorkbenc
}
};

// Registers setting for large file confirmation based on environment
const oldEditorLargeFileConfirmationConfigurationNode = this.editorLargeFileConfirmationConfigurationNode;
this.editorLargeFileConfirmationConfigurationNode = {
...workbenchConfigurationNodeBase,
properties: {
'workbench.editorLargeFileConfirmation': {
type: 'number',
default: isWeb ? 10 : this.environmentService.remoteAuthority ? 50 : 1024,
minimum: 1,
scope: ConfigurationScope.RESOURCE,
markdownDescription: localize('editorLargeFileSizeConfirmation', "Controls the minimum size of a file in MB before asking for confirmation when opening in the editor."),
}
}
};

this.configurationRegistry.updateConfigurations({
add: [
this.autoLockConfigurationNode,
this.defaultBinaryEditorConfigurationNode,
this.editorAssociationsConfigurationNode
this.editorAssociationsConfigurationNode,
this.editorLargeFileConfirmationConfigurationNode
],
remove: coalesce([
oldAutoLockConfigurationNode,
oldDefaultBinaryEditorConfigurationNode,
oldEditorAssociationsConfigurationNode
oldEditorAssociationsConfigurationNode,
oldEditorLargeFileConfirmationConfigurationNode
])
});
}
Expand Down