Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService';
|
||||
import { ITextFileService, TextFileEditorModelState } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
|
||||
export class BrowserTextFileService extends AbstractTextFileService {
|
||||
|
||||
protected registerListeners(): void {
|
||||
super.registerListeners();
|
||||
|
||||
// Lifecycle
|
||||
this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason)));
|
||||
}
|
||||
|
||||
protected onBeforeShutdown(reason: ShutdownReason): boolean {
|
||||
if (this.files.models.some(model => model.hasState(TextFileEditorModelState.PENDING_SAVE))) {
|
||||
this.logService.warn('Unload veto: pending file saves');
|
||||
|
||||
return true; // files are pending to be saved: veto
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ITextFileService, BrowserTextFileService);
|
||||
@@ -0,0 +1,667 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ITextFileService, ITextFileStreamContent, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, ITextFileSaveOptions, ITextFileEditorModelManager, IResourceEncoding, stringToSnapshot, ITextFileSaveAsOptions } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IRevertOptions, IEncodingSupport } from 'vs/workbench/common/editor';
|
||||
import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
import { IFileService, FileOperationError, FileOperationResult, IFileStatWithMetadata, ICreateFileOptions, IFileStreamContent } from 'vs/platform/files/common/files';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IUntitledTextEditorService, IUntitledTextEditorModelManager } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
|
||||
import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel';
|
||||
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { createTextBufferFactoryFromSnapshot, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { joinPath, dirname, basename, toLocalResource, extname, isEqual } from 'vs/base/common/resources';
|
||||
import { IDialogService, IFileDialogService, IConfirmation } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { VSBuffer, VSBufferReadable, bufferToStream } from 'vs/base/common/buffer';
|
||||
import { ITextSnapshot, ITextModel } from 'vs/editor/common/model';
|
||||
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
|
||||
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
|
||||
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
|
||||
import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
|
||||
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { IPathService } from 'vs/workbench/services/path/common/pathService';
|
||||
import { isValidBasename } from 'vs/base/common/extpath';
|
||||
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { UTF8, UTF8_with_bom, UTF16be, UTF16le, encodingExists, UTF8_BOM, detectEncodingByBOMFromBuffer, toEncodeReadable, toDecodeStream, IDecodeStreamResult } from 'vs/workbench/services/textfile/common/encoding';
|
||||
import { consumeStream } from 'vs/base/common/stream';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
/**
|
||||
* The workbench file service implementation implements the raw file service spec and adds additional methods on top.
|
||||
*/
|
||||
export abstract class AbstractTextFileService extends Disposable implements ITextFileService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
readonly files: ITextFileEditorModelManager = this._register(this.instantiationService.createInstance(TextFileEditorModelManager));
|
||||
|
||||
readonly untitled: IUntitledTextEditorModelManager = this.untitledTextEditorService;
|
||||
|
||||
constructor(
|
||||
@IFileService protected readonly fileService: IFileService,
|
||||
@IUntitledTextEditorService private untitledTextEditorService: IUntitledTextEditorService,
|
||||
@ILifecycleService protected readonly lifecycleService: ILifecycleService,
|
||||
@IInstantiationService protected readonly instantiationService: IInstantiationService,
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
@IFileDialogService private readonly fileDialogService: IFileDialogService,
|
||||
@ITextResourceConfigurationService protected readonly textResourceConfigurationService: ITextResourceConfigurationService,
|
||||
@IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService,
|
||||
@ITextModelService private readonly textModelService: ITextModelService,
|
||||
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
|
||||
@IPathService private readonly pathService: IPathService,
|
||||
@IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService,
|
||||
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
|
||||
@IModeService private readonly modeService: IModeService,
|
||||
@ILogService protected readonly logService: ILogService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
protected registerListeners(): void {
|
||||
|
||||
// Lifecycle
|
||||
this.lifecycleService.onShutdown(this.dispose, this);
|
||||
}
|
||||
|
||||
//#region text file read / write / create
|
||||
|
||||
private _encoding: EncodingOracle | undefined;
|
||||
|
||||
get encoding(): EncodingOracle {
|
||||
if (!this._encoding) {
|
||||
this._encoding = this._register(this.instantiationService.createInstance(EncodingOracle));
|
||||
}
|
||||
|
||||
return this._encoding;
|
||||
}
|
||||
|
||||
async read(resource: URI, options?: IReadTextFileOptions): Promise<ITextFileContent> {
|
||||
const [bufferStream, decoder] = await this.doRead(resource, {
|
||||
...options,
|
||||
// optimization: since we know that the caller does not
|
||||
// care about buffering, we indicate this to the reader.
|
||||
// this reduces all the overhead the buffered reading
|
||||
// has (open, read, close) if the provider supports
|
||||
// unbuffered reading.
|
||||
preferUnbuffered: true
|
||||
});
|
||||
|
||||
return {
|
||||
...bufferStream,
|
||||
encoding: decoder.detected.encoding || UTF8,
|
||||
value: await consumeStream(decoder.stream, strings => strings.join(''))
|
||||
};
|
||||
}
|
||||
|
||||
async readStream(resource: URI, options?: IReadTextFileOptions): Promise<ITextFileStreamContent> {
|
||||
const [bufferStream, decoder] = await this.doRead(resource, options);
|
||||
|
||||
return {
|
||||
...bufferStream,
|
||||
encoding: decoder.detected.encoding || UTF8,
|
||||
value: await createTextBufferFactoryFromStream(decoder.stream)
|
||||
};
|
||||
}
|
||||
|
||||
private async doRead(resource: URI, options?: IReadTextFileOptions & { preferUnbuffered?: boolean }): Promise<[IFileStreamContent, IDecodeStreamResult]> {
|
||||
|
||||
// read stream raw (either buffered or unbuffered)
|
||||
let bufferStream: IFileStreamContent;
|
||||
if (options?.preferUnbuffered) {
|
||||
const content = await this.fileService.readFile(resource, options);
|
||||
bufferStream = {
|
||||
...content,
|
||||
value: bufferToStream(content.value)
|
||||
};
|
||||
} else {
|
||||
bufferStream = await this.fileService.readFileStream(resource, options);
|
||||
}
|
||||
|
||||
// read through encoding library
|
||||
const decoder = await toDecodeStream(bufferStream.value, {
|
||||
guessEncoding: options?.autoGuessEncoding || this.textResourceConfigurationService.getValue(resource, 'files.autoGuessEncoding'),
|
||||
overwriteEncoding: detectedEncoding => this.encoding.getReadEncoding(resource, options, detectedEncoding)
|
||||
});
|
||||
|
||||
// validate binary
|
||||
if (options?.acceptTextOnly && decoder.detected.seemsBinary) {
|
||||
throw new TextFileOperationError(nls.localize('fileBinaryError', "File seems to be binary and cannot be opened as text"), TextFileOperationResult.FILE_IS_BINARY, options);
|
||||
}
|
||||
|
||||
return [bufferStream, decoder];
|
||||
}
|
||||
|
||||
async create(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
|
||||
const readable = await this.getEncodedReadable(resource, value);
|
||||
|
||||
return this.workingCopyFileService.create(resource, readable, options);
|
||||
}
|
||||
|
||||
async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {
|
||||
const readable = await this.getEncodedReadable(resource, value, options);
|
||||
|
||||
return this.fileService.writeFile(resource, readable, options);
|
||||
}
|
||||
|
||||
private async getEncodedReadable(resource: URI, value?: string | ITextSnapshot): Promise<VSBuffer | VSBufferReadable | undefined>;
|
||||
private async getEncodedReadable(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<VSBuffer | VSBufferReadable>;
|
||||
private async getEncodedReadable(resource: URI, value?: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<VSBuffer | VSBufferReadable | undefined> {
|
||||
|
||||
// check for encoding
|
||||
const { encoding, addBOM } = await this.encoding.getWriteEncoding(resource, options);
|
||||
|
||||
// when encoding is standard skip encoding step
|
||||
if (encoding === UTF8 && !addBOM) {
|
||||
return typeof value === 'undefined'
|
||||
? undefined
|
||||
: toBufferOrReadable(value);
|
||||
}
|
||||
|
||||
// otherwise create encoded readable
|
||||
value = value || '';
|
||||
const snapshot = typeof value === 'string' ? stringToSnapshot(value) : value;
|
||||
return toEncodeReadable(snapshot, encoding, { addBOM });
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region save
|
||||
|
||||
async save(resource: URI, options?: ITextFileSaveOptions): Promise<URI | undefined> {
|
||||
|
||||
// Untitled
|
||||
if (resource.scheme === Schemas.untitled) {
|
||||
const model = this.untitled.get(resource);
|
||||
if (model) {
|
||||
let targetUri: URI | undefined;
|
||||
|
||||
// Untitled with associated file path don't need to prompt
|
||||
if (model.hasAssociatedFilePath) {
|
||||
targetUri = await this.suggestSavePath(resource);
|
||||
}
|
||||
|
||||
// Otherwise ask user
|
||||
else {
|
||||
targetUri = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(resource), options?.availableFileSystems);
|
||||
}
|
||||
|
||||
// Save as if target provided
|
||||
if (targetUri) {
|
||||
return this.saveAs(resource, targetUri, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// File
|
||||
else {
|
||||
const model = this.files.get(resource);
|
||||
if (model) {
|
||||
return await model.save(options) ? resource : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async saveAs(source: URI, target?: URI, options?: ITextFileSaveAsOptions): Promise<URI | undefined> {
|
||||
|
||||
// Get to target resource
|
||||
if (!target) {
|
||||
target = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(options?.suggestedTarget ?? source), options?.availableFileSystems);
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
return; // user canceled
|
||||
}
|
||||
|
||||
// Just save if target is same as models own resource
|
||||
if (isEqual(source, target)) {
|
||||
return this.save(source, { ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ });
|
||||
}
|
||||
|
||||
// If the target is different but of same identity, we
|
||||
// move the source to the target, knowing that the
|
||||
// underlying file system cannot have both and then save.
|
||||
// However, this will only work if the source exists
|
||||
// and is not orphaned, so we need to check that too.
|
||||
if (this.fileService.canHandleResource(source) && this.uriIdentityService.extUri.isEqual(source, target) && (await this.fileService.exists(source))) {
|
||||
await this.workingCopyFileService.move([{ source, target }]);
|
||||
|
||||
return this.save(target, options);
|
||||
}
|
||||
|
||||
// Do it
|
||||
return this.doSaveAs(source, target, options);
|
||||
}
|
||||
|
||||
private async doSaveAs(source: URI, target: URI, options?: ITextFileSaveOptions): Promise<URI> {
|
||||
let success = false;
|
||||
|
||||
// If the source is an existing text file model, we can directly
|
||||
// use that model to copy the contents to the target destination
|
||||
const textFileModel = this.files.get(source);
|
||||
if (textFileModel && textFileModel.isResolved()) {
|
||||
success = await this.doSaveAsTextFile(textFileModel, source, target, options);
|
||||
}
|
||||
|
||||
// Otherwise if the source can be handled by the file service
|
||||
// we can simply invoke the copy() function to save as
|
||||
else if (this.fileService.canHandleResource(source)) {
|
||||
await this.fileService.copy(source, target);
|
||||
|
||||
success = true;
|
||||
}
|
||||
|
||||
// Next, if the source does not seem to be a file, we try to
|
||||
// resolve a text model from the resource to get at the
|
||||
// contents and additional meta data (e.g. encoding).
|
||||
else if (this.textModelService.canHandleResource(source)) {
|
||||
const modelReference = await this.textModelService.createModelReference(source);
|
||||
try {
|
||||
success = await this.doSaveAsTextFile(modelReference.object, source, target, options);
|
||||
} finally {
|
||||
modelReference.dispose(); // free up our use of the reference
|
||||
}
|
||||
}
|
||||
|
||||
// Finally we simply check if we can find a editor model that
|
||||
// would give us access to the contents.
|
||||
else {
|
||||
const textModel = this.modelService.getModel(source);
|
||||
if (textModel) {
|
||||
success = await this.doSaveAsTextFile(textModel, source, target, options);
|
||||
}
|
||||
}
|
||||
|
||||
// Revert the source if result is success
|
||||
if (success) {
|
||||
await this.revert(source);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
private async doSaveAsTextFile(sourceModel: IResolvedTextEditorModel | ITextModel, source: URI, target: URI, options?: ITextFileSaveOptions): Promise<boolean> {
|
||||
|
||||
// Find source encoding if any
|
||||
let sourceModelEncoding: string | undefined = undefined;
|
||||
const sourceModelWithEncodingSupport = (sourceModel as unknown as IEncodingSupport);
|
||||
if (typeof sourceModelWithEncodingSupport.getEncoding === 'function') {
|
||||
sourceModelEncoding = sourceModelWithEncodingSupport.getEncoding();
|
||||
}
|
||||
|
||||
// Prefer an existing model if it is already loaded for the given target resource
|
||||
let targetExists: boolean = false;
|
||||
let targetModel = this.files.get(target);
|
||||
if (targetModel?.isResolved()) {
|
||||
targetExists = true;
|
||||
}
|
||||
|
||||
// Otherwise create the target file empty if it does not exist already and resolve it from there
|
||||
else {
|
||||
targetExists = await this.fileService.exists(target);
|
||||
|
||||
// create target file adhoc if it does not exist yet
|
||||
if (!targetExists) {
|
||||
await this.create(target, '');
|
||||
}
|
||||
|
||||
try {
|
||||
targetModel = await this.files.resolve(target, { encoding: sourceModelEncoding });
|
||||
} catch (error) {
|
||||
// if the target already exists and was not created by us, it is possible
|
||||
// that we cannot load the target as text model if it is binary or too
|
||||
// large. in that case we have to delete the target file first and then
|
||||
// re-run the operation.
|
||||
if (targetExists) {
|
||||
if (
|
||||
(<TextFileOperationError>error).textFileOperationResult === TextFileOperationResult.FILE_IS_BINARY ||
|
||||
(<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE
|
||||
) {
|
||||
await this.fileService.del(target);
|
||||
|
||||
return this.doSaveAsTextFile(sourceModel, source, target, options);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm to overwrite if we have an untitled file with associated file where
|
||||
// the file actually exists on disk and we are instructed to save to that file
|
||||
// path. This can happen if the file was created after the untitled file was opened.
|
||||
// See https://github.com/microsoft/vscode/issues/67946
|
||||
let write: boolean;
|
||||
if (sourceModel instanceof UntitledTextEditorModel && sourceModel.hasAssociatedFilePath && targetExists && this.uriIdentityService.extUri.isEqual(target, toLocalResource(sourceModel.resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme))) {
|
||||
write = await this.confirmOverwrite(target);
|
||||
} else {
|
||||
write = true;
|
||||
}
|
||||
|
||||
if (!write) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let sourceTextModel: ITextModel | undefined = undefined;
|
||||
if (sourceModel instanceof BaseTextEditorModel) {
|
||||
if (sourceModel.isResolved()) {
|
||||
sourceTextModel = sourceModel.textEditorModel;
|
||||
}
|
||||
} else {
|
||||
sourceTextModel = sourceModel as ITextModel;
|
||||
}
|
||||
|
||||
let targetTextModel: ITextModel | undefined = undefined;
|
||||
if (targetModel.isResolved()) {
|
||||
targetTextModel = targetModel.textEditorModel;
|
||||
}
|
||||
|
||||
// take over model value, encoding and mode (only if more specific) from source model
|
||||
if (sourceTextModel && targetTextModel) {
|
||||
|
||||
// encoding
|
||||
targetModel.updatePreferredEncoding(sourceModelEncoding);
|
||||
|
||||
// content
|
||||
this.modelService.updateModel(targetTextModel, createTextBufferFactoryFromSnapshot(sourceTextModel.createSnapshot()));
|
||||
|
||||
// mode
|
||||
const sourceMode = sourceTextModel.getLanguageIdentifier();
|
||||
const targetMode = targetTextModel.getLanguageIdentifier();
|
||||
if (sourceMode.language !== PLAINTEXT_MODE_ID && targetMode.language === PLAINTEXT_MODE_ID) {
|
||||
targetTextModel.setMode(sourceMode); // only use if more specific than plain/text
|
||||
}
|
||||
|
||||
// transient properties
|
||||
const sourceTransientProperties = this.codeEditorService.getTransientModelProperties(sourceTextModel);
|
||||
if (sourceTransientProperties) {
|
||||
for (const [key, value] of sourceTransientProperties) {
|
||||
this.codeEditorService.setTransientModelProperty(targetTextModel, key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// save model
|
||||
return await targetModel.save(options);
|
||||
}
|
||||
|
||||
private async confirmOverwrite(resource: URI): Promise<boolean> {
|
||||
const confirm: IConfirmation = {
|
||||
message: nls.localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)),
|
||||
detail: nls.localize('irreversible', "A file or folder with the name '{0}' already exists in the folder '{1}'. Replacing it will overwrite its current contents.", basename(resource), basename(dirname(resource))),
|
||||
primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
|
||||
type: 'warning'
|
||||
};
|
||||
|
||||
return (await this.dialogService.confirm(confirm)).confirmed;
|
||||
}
|
||||
|
||||
private async suggestSavePath(resource: URI): Promise<URI> {
|
||||
|
||||
// Just take the resource as is if the file service can handle it
|
||||
if (this.fileService.canHandleResource(resource)) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
const remoteAuthority = this.environmentService.remoteAuthority;
|
||||
|
||||
// Otherwise try to suggest a path that can be saved
|
||||
let suggestedFilename: string | undefined = undefined;
|
||||
if (resource.scheme === Schemas.untitled) {
|
||||
const model = this.untitledTextEditorService.get(resource);
|
||||
if (model) {
|
||||
|
||||
// Untitled with associated file path
|
||||
if (model.hasAssociatedFilePath) {
|
||||
return toLocalResource(resource, remoteAuthority, this.pathService.defaultUriScheme);
|
||||
}
|
||||
|
||||
// Untitled without associated file path: use name
|
||||
// of untitled model if it is a valid path name
|
||||
let untitledName = model.name;
|
||||
if (!isValidBasename(untitledName)) {
|
||||
untitledName = basename(resource);
|
||||
}
|
||||
|
||||
// Add mode file extension if specified
|
||||
const mode = model.getMode();
|
||||
if (mode && mode !== PLAINTEXT_MODE_ID) {
|
||||
suggestedFilename = this.suggestFilename(mode, untitledName);
|
||||
} else {
|
||||
suggestedFilename = untitledName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to basename of resource
|
||||
if (!suggestedFilename) {
|
||||
suggestedFilename = basename(resource);
|
||||
}
|
||||
|
||||
// Try to place where last active file was if any
|
||||
// Otherwise fallback to user home
|
||||
return joinPath(this.fileDialogService.defaultFilePath() || (await this.pathService.userHome()), suggestedFilename);
|
||||
}
|
||||
|
||||
suggestFilename(mode: string, untitledName: string) {
|
||||
const languageName = this.modeService.getLanguageName(mode);
|
||||
if (!languageName) {
|
||||
return untitledName;
|
||||
}
|
||||
const extension = this.modeService.getExtensions(languageName)[0];
|
||||
if (extension) {
|
||||
if (!untitledName.endsWith(extension)) {
|
||||
return untitledName + extension;
|
||||
}
|
||||
}
|
||||
const filename = this.modeService.getFilenames(languageName)[0];
|
||||
return filename || untitledName;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region revert
|
||||
|
||||
async revert(resource: URI, options?: IRevertOptions): Promise<void> {
|
||||
|
||||
// Untitled
|
||||
if (resource.scheme === Schemas.untitled) {
|
||||
const model = this.untitled.get(resource);
|
||||
if (model) {
|
||||
return model.revert(options);
|
||||
}
|
||||
}
|
||||
|
||||
// File
|
||||
else {
|
||||
const model = this.files.get(resource);
|
||||
if (model && (model.isDirty() || options?.force)) {
|
||||
return model.revert(options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region dirty
|
||||
|
||||
isDirty(resource: URI): boolean {
|
||||
const model = resource.scheme === Schemas.untitled ? this.untitled.get(resource) : this.files.get(resource);
|
||||
if (model) {
|
||||
return model.isDirty();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
export interface IEncodingOverride {
|
||||
parent?: URI;
|
||||
extension?: string;
|
||||
encoding: string;
|
||||
}
|
||||
|
||||
export class EncodingOracle extends Disposable implements IResourceEncodings {
|
||||
|
||||
private _encodingOverrides: IEncodingOverride[];
|
||||
protected get encodingOverrides(): IEncodingOverride[] { return this._encodingOverrides; }
|
||||
protected set encodingOverrides(value: IEncodingOverride[]) { this._encodingOverrides = value; }
|
||||
|
||||
constructor(
|
||||
@ITextResourceConfigurationService private textResourceConfigurationService: ITextResourceConfigurationService,
|
||||
@IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService,
|
||||
@IWorkspaceContextService private contextService: IWorkspaceContextService,
|
||||
@IFileService private fileService: IFileService,
|
||||
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._encodingOverrides = this.getDefaultEncodingOverrides();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
||||
// Workspace Folder Change
|
||||
this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.encodingOverrides = this.getDefaultEncodingOverrides()));
|
||||
}
|
||||
|
||||
private getDefaultEncodingOverrides(): IEncodingOverride[] {
|
||||
const defaultEncodingOverrides: IEncodingOverride[] = [];
|
||||
|
||||
// Global settings
|
||||
defaultEncodingOverrides.push({ parent: this.environmentService.userRoamingDataHome, encoding: UTF8 });
|
||||
|
||||
// Workspace files (via extension and via untitled workspaces location)
|
||||
defaultEncodingOverrides.push({ extension: WORKSPACE_EXTENSION, encoding: UTF8 });
|
||||
defaultEncodingOverrides.push({ parent: this.environmentService.untitledWorkspacesHome, encoding: UTF8 });
|
||||
|
||||
// Folder Settings
|
||||
this.contextService.getWorkspace().folders.forEach(folder => {
|
||||
defaultEncodingOverrides.push({ parent: joinPath(folder.uri, '.vscode'), encoding: UTF8 });
|
||||
});
|
||||
|
||||
return defaultEncodingOverrides;
|
||||
}
|
||||
|
||||
async getWriteEncoding(resource: URI, options?: IWriteTextFileOptions): Promise<{ encoding: string, addBOM: boolean }> {
|
||||
const { encoding, hasBOM } = await this.getPreferredWriteEncoding(resource, options ? options.encoding : undefined);
|
||||
|
||||
// Some encodings come with a BOM automatically
|
||||
if (hasBOM) {
|
||||
return { encoding, addBOM: true };
|
||||
}
|
||||
|
||||
// Ensure that we preserve an existing BOM if found for UTF8
|
||||
// unless we are instructed to overwrite the encoding
|
||||
const overwriteEncoding = options?.overwriteEncoding;
|
||||
if (!overwriteEncoding && encoding === UTF8) {
|
||||
try {
|
||||
const buffer = (await this.fileService.readFile(resource, { length: UTF8_BOM.length })).value;
|
||||
if (detectEncodingByBOMFromBuffer(buffer, buffer.byteLength) === UTF8_with_bom) {
|
||||
return { encoding, addBOM: true };
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore - file might not exist
|
||||
}
|
||||
}
|
||||
|
||||
return { encoding, addBOM: false };
|
||||
}
|
||||
|
||||
async getPreferredWriteEncoding(resource: URI, preferredEncoding?: string): Promise<IResourceEncoding> {
|
||||
const resourceEncoding = await this.getEncodingForResource(resource, preferredEncoding);
|
||||
|
||||
return {
|
||||
encoding: resourceEncoding,
|
||||
hasBOM: resourceEncoding === UTF16be || resourceEncoding === UTF16le || resourceEncoding === UTF8_with_bom // enforce BOM for certain encodings
|
||||
};
|
||||
}
|
||||
|
||||
getReadEncoding(resource: URI, options: IReadTextFileOptions | undefined, detectedEncoding: string | null): Promise<string> {
|
||||
let preferredEncoding: string | undefined;
|
||||
|
||||
// Encoding passed in as option
|
||||
if (options?.encoding) {
|
||||
if (detectedEncoding === UTF8_with_bom && options.encoding === UTF8) {
|
||||
preferredEncoding = UTF8_with_bom; // indicate the file has BOM if we are to resolve with UTF 8
|
||||
} else {
|
||||
preferredEncoding = options.encoding; // give passed in encoding highest priority
|
||||
}
|
||||
}
|
||||
|
||||
// Encoding detected
|
||||
else if (detectedEncoding) {
|
||||
preferredEncoding = detectedEncoding;
|
||||
}
|
||||
|
||||
// Encoding configured
|
||||
else if (this.textResourceConfigurationService.getValue(resource, 'files.encoding') === UTF8_with_bom) {
|
||||
preferredEncoding = UTF8; // if we did not detect UTF 8 BOM before, this can only be UTF 8 then
|
||||
}
|
||||
|
||||
return this.getEncodingForResource(resource, preferredEncoding);
|
||||
}
|
||||
|
||||
private async getEncodingForResource(resource: URI, preferredEncoding?: string): Promise<string> {
|
||||
let fileEncoding: string;
|
||||
|
||||
const override = this.getEncodingOverride(resource);
|
||||
if (override) {
|
||||
fileEncoding = override; // encoding override always wins
|
||||
} else if (preferredEncoding) {
|
||||
fileEncoding = preferredEncoding; // preferred encoding comes second
|
||||
} else {
|
||||
fileEncoding = this.textResourceConfigurationService.getValue(resource, 'files.encoding'); // and last we check for settings
|
||||
}
|
||||
|
||||
if (fileEncoding !== UTF8) {
|
||||
if (!fileEncoding || !(await encodingExists(fileEncoding))) {
|
||||
fileEncoding = UTF8; // the default is UTF-8
|
||||
}
|
||||
}
|
||||
|
||||
return fileEncoding;
|
||||
}
|
||||
|
||||
private getEncodingOverride(resource: URI): string | undefined {
|
||||
if (this.encodingOverrides && this.encodingOverrides.length) {
|
||||
for (const override of this.encodingOverrides) {
|
||||
|
||||
// check if the resource is child of encoding override path
|
||||
if (override.parent && this.uriIdentityService.extUri.isEqualOrParent(resource, override.parent)) {
|
||||
return override.encoding;
|
||||
}
|
||||
|
||||
// check if the resource extension is equal to encoding override
|
||||
if (override.extension && extname(resource) === `.${override.extension}`) {
|
||||
return override.encoding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user