Skip to content

Commit

Permalink
Add a workbench.editorLargeFileConfirmation setting (#169811)
Browse files Browse the repository at this point in the history
* files - introduce a `workbench.editorLargeFileConfirmation` setting and apply it for opening text files

* allow to override limits

* comments 💄

* editor place holder - use buttons over links

* files - allow to resolve with limits

* wire things in

* 💄

* polish

* 💄

* more polish

* better file is folder handling

* 💄

* tweak wording

* support diff editor too

* adopt

* 💄
  • Loading branch information
bpasero authored Dec 27, 2022
1 parent 18918f8 commit 229c95c
Show file tree
Hide file tree
Showing 26 changed files with 460 additions and 203 deletions.
2 changes: 0 additions & 2 deletions src/vs/base/browser/ui/button/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,5 @@ export class ButtonBar extends Disposable {
}

}));

}

}
29 changes: 19 additions & 10 deletions src/vs/platform/files/common/fileService.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit 229c95c

Please sign in to comment.