Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973 (#6381)

* Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973

* disable strict null check
This commit is contained in:
Anthony Dresser
2019-07-15 22:35:46 -07:00
committed by GitHub
parent f720ec642f
commit 0b7e7ddbf9
2406 changed files with 59140 additions and 35464 deletions

View File

@@ -6,6 +6,8 @@
import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService';
import { ITextFileService, IResourceEncodings, IResourceEncoding } from 'vs/workbench/services/textfile/common/textfiles';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
export class BrowserTextFileService extends TextFileService {
@@ -14,6 +16,42 @@ export class BrowserTextFileService extends TextFileService {
return { encoding: 'utf8', hasBOM: false };
}
};
protected beforeShutdown(reason: ShutdownReason): boolean {
// Web: we cannot perform long running in the shutdown phase
// As such we need to check sync if there are any dirty files
// that have not been backed up yet and then prevent the shutdown
// if that is the case.
return this.doBeforeShutdownSync(reason);
}
private doBeforeShutdownSync(reason: ShutdownReason): boolean {
const dirtyResources = this.getDirty();
if (!dirtyResources.length) {
return false; // no dirty: no veto
}
if (!this.isHotExitEnabled) {
return true; // dirty without backup: veto
}
for (const dirtyResource of dirtyResources) {
let hasBackup = false;
if (this.fileService.canHandleResource(dirtyResource)) {
const model = this.models.get(dirtyResource);
hasBackup = !!(model && model.hasBackup());
} else if (dirtyResource.scheme === Schemas.untitled) {
hasBackup = this.untitledEditorService.hasBackup(dirtyResource);
}
if (!hasBackup) {
return true; // dirty without backup: veto
}
}
return false; // dirty with backups: no veto
}
}
registerSingleton(ITextFileService, BrowserTextFileService);

View File

@@ -3,7 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { join } from 'vs/base/common/path';
import * as nls from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { guessMimeTypes } from 'vs/base/common/mime';
@@ -25,10 +24,9 @@ import { RunOnceScheduler, timeout } from 'vs/base/common/async';
import { ITextBufferFactory } from 'vs/editor/common/model';
import { hash } from 'vs/base/common/hash';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { isLinux } from 'vs/base/common/platform';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { toDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { isEqual, isEqualOrParent, extname, basename } from 'vs/base/common/resources';
import { isEqual, isEqualOrParent, extname, basename, joinPath } from 'vs/base/common/resources';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Schemas } from 'vs/base/common/network';
@@ -39,6 +37,22 @@ export interface IBackupMetaData {
orphaned: boolean;
}
type FileTelemetryDataFragment = {
mimeType: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
ext: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
path: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
reason?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
whitelistedjson?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
};
type TelemetryData = {
mimeType: string;
ext: string;
path: number;
reason?: number;
whitelistedjson?: string;
};
/**
* The text file editor model listens to changes to its underlying code editor model and saves these changes through the file service back to the disk.
*/
@@ -56,10 +70,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
static setSaveParticipant(handler: ISaveParticipant | null): void { TextFileEditorModel.saveParticipant = handler; }
private readonly _onDidContentChange: Emitter<StateChange> = this._register(new Emitter<StateChange>());
get onDidContentChange(): Event<StateChange> { return this._onDidContentChange.event; }
readonly onDidContentChange: Event<StateChange> = this._onDidContentChange.event;
private readonly _onDidStateChange: Emitter<StateChange> = this._register(new Emitter<StateChange>());
get onDidStateChange(): Event<StateChange> { return this._onDidStateChange.event; }
readonly onDidStateChange: Event<StateChange> = this._onDidStateChange.event;
private resource: URI;
@@ -76,7 +90,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
private autoSaveAfterMillies?: number;
private autoSaveAfterMilliesEnabled: boolean;
private autoSaveDisposable?: IDisposable;
private readonly autoSaveDisposable = this._register(new MutableDisposable());
private saveSequentializer: SaveSequentializer;
private lastSaveAttemptTime: number;
@@ -144,7 +158,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
}
private onFileChanges(e: FileChangesEvent): void {
private async onFileChanges(e: FileChangesEvent): Promise<void> {
let fileEventImpactsModel = false;
let newInOrphanModeGuess: boolean | undefined;
@@ -167,28 +181,25 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
if (fileEventImpactsModel && this.inOrphanMode !== newInOrphanModeGuess) {
let checkOrphanedPromise: Promise<boolean>;
let newInOrphanModeValidated: boolean = false;
if (newInOrphanModeGuess) {
// We have received reports of users seeing delete events even though the file still
// exists (network shares issue: https://github.com/Microsoft/vscode/issues/13665).
// Since we do not want to mark the model as orphaned, we have to check if the
// file is really gone and not just a faulty file event.
checkOrphanedPromise = timeout(100).then(() => {
if (this.disposed) {
return true;
}
await timeout(100);
return this.fileService.exists(this.resource).then(exists => !exists);
});
} else {
checkOrphanedPromise = Promise.resolve(false);
if (this.disposed) {
newInOrphanModeValidated = true;
} else {
const exists = await this.fileService.exists(this.resource);
newInOrphanModeValidated = !exists;
}
}
checkOrphanedPromise.then(newInOrphanModeValidated => {
if (this.inOrphanMode !== newInOrphanModeValidated && !this.disposed) {
this.setOrphaned(newInOrphanModeValidated);
}
});
if (this.inOrphanMode !== newInOrphanModeValidated && !this.disposed) {
this.setOrphaned(newInOrphanModeValidated);
}
}
}
@@ -239,40 +250,38 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return this.backupFileService.backupResource<IBackupMetaData>(target, this.createSnapshot(), this.versionId, meta);
}
}
return Promise.resolve();
hasBackup(): boolean {
return this.backupFileService.hasBackupSync(this.resource, this.versionId);
}
async revert(soft?: boolean): Promise<void> {
if (!this.isResolved()) {
return Promise.resolve(undefined);
return;
}
// Cancel any running auto-save
this.cancelPendingAutoSave();
this.autoSaveDisposable.clear();
// Unset flags
const undo = this.setDirty(false);
let loadPromise: Promise<unknown>;
if (soft) {
loadPromise = Promise.resolve();
} else {
loadPromise = this.load({ forceReadFromDisk: true });
// Force read from disk unless reverting soft
if (!soft) {
try {
await this.load({ forceReadFromDisk: true });
} catch (error) {
// Set flags back to previous values, we are still dirty if revert failed
undo();
throw error;
}
}
try {
await loadPromise;
// Emit file change event
this._onDidStateChange.fire(StateChange.REVERTED);
} catch (error) {
// Set flags back to previous values, we are still dirty if revert failed
undo();
return Promise.reject(error);
}
// Emit file change event
this._onDidStateChange.fire(StateChange.REVERTED);
}
async load(options?: ILoadOptions): Promise<ITextFileEditorModel> {
@@ -437,21 +446,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Telemetry: We log the fileGet telemetry event after the model has been loaded to ensure a good mimetype
const settingsType = this.getTypeIfSettings();
if (settingsType) {
/* __GDPR__
"settingsRead" : {
"settingsType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data
type SettingsReadClassification = {
settingsType: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
};
this.telemetryService.publicLog2<{ settingsType: string }, SettingsReadClassification>('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data
} else {
/* __GDPR__
"fileGet" : {
"${include}": [
"${FileTelemetryData}"
]
}
*/
this.telemetryService.publicLog('fileGet', this.getTelemetryData(options && options.reason ? options.reason : LoadReason.OTHER));
type FileGetClassification = {} & FileTelemetryDataFragment;
this.telemetryService.publicLog2<TelemetryData, FileGetClassification>('fileGet', this.getTelemetryData(options && options.reason ? options.reason : LoadReason.OTHER));
}
return this;
@@ -577,7 +580,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.logService.trace(`doAutoSave() - enter for versionId ${versionId}`, this.resource);
// Cancel any currently running auto saves to make this the one that succeeds
this.cancelPendingAutoSave();
this.autoSaveDisposable.clear();
// Create new save timer and store it for disposal as needed
const handle = setTimeout(() => {
@@ -588,25 +591,18 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
}, this.autoSaveAfterMillies);
this.autoSaveDisposable = toDisposable(() => clearTimeout(handle));
this.autoSaveDisposable.value = toDisposable(() => clearTimeout(handle));
}
private cancelPendingAutoSave(): void {
if (this.autoSaveDisposable) {
this.autoSaveDisposable.dispose();
this.autoSaveDisposable = undefined;
}
}
save(options: ISaveOptions = Object.create(null)): Promise<void> {
async save(options: ISaveOptions = Object.create(null)): Promise<void> {
if (!this.isResolved()) {
return Promise.resolve(undefined);
return;
}
this.logService.trace('save() - enter', this.resource);
// Cancel any currently running auto saves to make this the one that succeeds
this.cancelPendingAutoSave();
this.autoSaveDisposable.clear();
return this.doSave(this.versionId, options);
}
@@ -626,7 +622,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
if (this.saveSequentializer.hasPendingSave(versionId)) {
this.logService.trace(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource);
return this.saveSequentializer.pendingSave || Promise.resolve(undefined);
return this.saveSequentializer.pendingSave || Promise.resolve();
}
// Return early if not dirty (unless forced) or version changed meanwhile
@@ -639,7 +635,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
if ((!options.force && !this.dirty) || versionId !== this.versionId) {
this.logService.trace(`doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource);
return Promise.resolve(undefined);
return Promise.resolve();
}
// Return if currently saving by storing this save request as the next save that should happen.
@@ -750,21 +746,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Telemetry
const settingsType = this.getTypeIfSettings();
if (settingsType) {
/* __GDPR__
"settingsWritten" : {
"settingsType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data
type SettingsWrittenClassification = {
settingsType: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
};
this.telemetryService.publicLog2<{ settingsType: string }, SettingsWrittenClassification>('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data
} else {
/* __GDPR__
"filePUT" : {
"${include}": [
"${FileTelemetryData}"
]
}
*/
this.telemetryService.publicLog('filePUT', this.getTelemetryData(options.reason));
type FilePutClassfication = {} & FileTelemetryDataFragment;
this.telemetryService.publicLog2<TelemetryData, FilePutClassfication>('filePUT', this.getTelemetryData(options.reason));
}
}, error => {
this.logService.error(`doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource);
@@ -792,22 +780,22 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
// Check for global settings file
if (isEqual(this.resource, URI.file(this.environmentService.appSettingsPath), !isLinux)) {
if (isEqual(this.resource, this.environmentService.settingsResource)) {
return 'global-settings';
}
// Check for keybindings file
if (isEqual(this.resource, URI.file(this.environmentService.appKeybindingsPath), !isLinux)) {
if (isEqual(this.resource, this.environmentService.keybindingsResource)) {
return 'keybindings';
}
// Check for locale file
if (isEqual(this.resource, URI.file(join(this.environmentService.appSettingsHome, 'locale.json')), !isLinux)) {
if (isEqual(this.resource, joinPath(this.environmentService.userRoamingDataHome, 'locale.json'))) {
return 'locale';
}
// Check for snippets
if (isEqualOrParent(this.resource, URI.file(join(this.environmentService.appSettingsHome, 'snippets')))) {
if (isEqualOrParent(this.resource, joinPath(this.environmentService.userRoamingDataHome, 'snippets'))) {
return 'snippets';
}
@@ -827,30 +815,22 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return '';
}
private getTelemetryData(reason: number | undefined): object {
private getTelemetryData(reason: number | undefined): TelemetryData {
const ext = extname(this.resource);
const fileName = basename(this.resource);
const path = this.resource.scheme === Schemas.file ? this.resource.fsPath : this.resource.path;
const telemetryData = {
mimeType: guessMimeTypes(path).join(', '),
mimeType: guessMimeTypes(this.resource).join(', '),
ext,
path: hash(path),
reason
reason,
whitelistedjson: undefined as string | undefined
};
if (ext === '.json' && TextFileEditorModel.WHITELIST_JSON.indexOf(fileName) > -1) {
telemetryData['whitelistedjson'] = fileName;
}
/* __GDPR__FRAGMENT__
"FileTelemetryData" : {
"mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"whitelistedjson": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
return telemetryData;
}
@@ -1050,8 +1030,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.inOrphanMode = false;
this.inErrorMode = false;
this.cancelPendingAutoSave();
super.dispose();
}
}

View File

@@ -15,28 +15,28 @@ import { ResourceMap } from 'vs/base/common/map';
export class TextFileEditorModelManager extends Disposable implements ITextFileEditorModelManager {
private readonly _onModelDisposed: Emitter<URI> = this._register(new Emitter<URI>());
get onModelDisposed(): Event<URI> { return this._onModelDisposed.event; }
readonly onModelDisposed: Event<URI> = this._onModelDisposed.event;
private readonly _onModelContentChanged: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
get onModelContentChanged(): Event<TextFileModelChangeEvent> { return this._onModelContentChanged.event; }
readonly onModelContentChanged: Event<TextFileModelChangeEvent> = this._onModelContentChanged.event;
private readonly _onModelDirty: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
get onModelDirty(): Event<TextFileModelChangeEvent> { return this._onModelDirty.event; }
readonly onModelDirty: Event<TextFileModelChangeEvent> = this._onModelDirty.event;
private readonly _onModelSaveError: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
get onModelSaveError(): Event<TextFileModelChangeEvent> { return this._onModelSaveError.event; }
readonly onModelSaveError: Event<TextFileModelChangeEvent> = this._onModelSaveError.event;
private readonly _onModelSaved: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
get onModelSaved(): Event<TextFileModelChangeEvent> { return this._onModelSaved.event; }
readonly onModelSaved: Event<TextFileModelChangeEvent> = this._onModelSaved.event;
private readonly _onModelReverted: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
get onModelReverted(): Event<TextFileModelChangeEvent> { return this._onModelReverted.event; }
readonly onModelReverted: Event<TextFileModelChangeEvent> = this._onModelReverted.event;
private readonly _onModelEncodingChanged: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
get onModelEncodingChanged(): Event<TextFileModelChangeEvent> { return this._onModelEncodingChanged.event; }
readonly onModelEncodingChanged: Event<TextFileModelChangeEvent> = this._onModelEncodingChanged.event;
private readonly _onModelOrphanedChanged: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
get onModelOrphanedChanged(): Event<TextFileModelChangeEvent> { return this._onModelOrphanedChanged.event; }
readonly onModelOrphanedChanged: Event<TextFileModelChangeEvent> = this._onModelOrphanedChanged.event;
private _onModelsDirtyEvent: Event<TextFileModelChangeEvent[]>;
private _onModelsSaveError: Event<TextFileModelChangeEvent[]>;

View File

@@ -31,7 +31,6 @@ import { createTextBufferFactoryFromSnapshot, createTextBufferFactoryFromStream
import { IModelService } from 'vs/editor/common/services/modelService';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { isEqualOrParent, isEqual, joinPath, dirname, extname, basename, toLocalResource } from 'vs/base/common/resources';
import { posix } from 'vs/base/common/path';
import { getConfirmMessage, IDialogService, IFileDialogService, ISaveDialogOptions, IConfirmation } from 'vs/platform/dialogs/common/dialogs';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
@@ -50,13 +49,13 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
_serviceBrand: ServiceIdentifier<any>;
private readonly _onAutoSaveConfigurationChange: Emitter<IAutoSaveConfiguration> = this._register(new Emitter<IAutoSaveConfiguration>());
get onAutoSaveConfigurationChange(): Event<IAutoSaveConfiguration> { return this._onAutoSaveConfigurationChange.event; }
readonly onAutoSaveConfigurationChange: Event<IAutoSaveConfiguration> = this._onAutoSaveConfigurationChange.event;
private readonly _onFilesAssociationChange: Emitter<void> = this._register(new Emitter<void>());
get onFilesAssociationChange(): Event<void> { return this._onFilesAssociationChange.event; }
readonly onFilesAssociationChange: Event<void> = this._onFilesAssociationChange.event;
private readonly _onWillMove = this._register(new Emitter<IWillMoveEvent>());
get onWillMove(): Event<IWillMoveEvent> { return this._onWillMove.event; }
readonly onWillMove: Event<IWillMoveEvent> = this._onWillMove.event;
private _models: TextFileEditorModelManager;
get models(): ITextFileEditorModelManager { return this._models; }
@@ -73,7 +72,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
constructor(
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IFileService protected readonly fileService: IFileService,
@IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
@IUntitledEditorService protected readonly untitledEditorService: IUntitledEditorService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@IInstantiationService protected instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@@ -119,7 +118,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
}));
}
private beforeShutdown(reason: ShutdownReason): boolean | Promise<boolean> {
protected beforeShutdown(reason: ShutdownReason): boolean | Promise<boolean> {
// Dirty files need treatment on shutdown
const dirty = this.getDirty();
@@ -152,7 +151,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
// If hot exit is enabled, backup dirty files and allow to exit without confirmation
if (this.isHotExitEnabled) {
return this.backupBeforeShutdown(dirty, this.models, reason).then(didBackup => {
return this.backupBeforeShutdown(dirty, reason).then(didBackup => {
if (didBackup) {
return this.noVeto({ cleanUpBackups: false }); // no veto and no backup cleanup (since backup was successful)
}
@@ -171,7 +170,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
return this.confirmBeforeShutdown();
}
private async backupBeforeShutdown(dirtyToBackup: URI[], textFileEditorModelManager: ITextFileEditorModelManager, reason: ShutdownReason): Promise<boolean> {
private async backupBeforeShutdown(dirtyToBackup: URI[], reason: ShutdownReason): Promise<boolean> {
const windowCount = await this.windowsService.getWindowCount();
// When quit is requested skip the confirm callback and attempt to backup all workspaces.
@@ -212,24 +211,24 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
return false;
}
await this.backupAll(dirtyToBackup, textFileEditorModelManager);
await this.backupAll(dirtyToBackup);
return true;
}
private backupAll(dirtyToBackup: URI[], textFileEditorModelManager: ITextFileEditorModelManager): Promise<void> {
private backupAll(dirtyToBackup: URI[]): Promise<void> {
// split up between files and untitled
const filesToBackup: ITextFileEditorModel[] = [];
const untitledToBackup: URI[] = [];
dirtyToBackup.forEach(s => {
if (this.fileService.canHandleResource(s)) {
const model = textFileEditorModelManager.get(s);
dirtyToBackup.forEach(dirty => {
if (this.fileService.canHandleResource(dirty)) {
const model = this.models.get(dirty);
if (model) {
filesToBackup.push(model);
}
} else if (s.scheme === Schemas.untitled) {
untitledToBackup.push(s);
} else if (dirty.scheme === Schemas.untitled) {
untitledToBackup.push(dirty);
}
});
@@ -436,14 +435,14 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
}
async delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void> {
const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource, !platform.isLinux /* ignorecase */));
const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource));
await this.revertAll(dirtyFiles, { soft: true });
return this.fileService.del(resource, options);
}
async move(source: URI, target: URI, overwrite?: boolean): Promise<void> {
async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
const waitForPromises: Promise<unknown>[] = [];
// Event
@@ -467,7 +466,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
}
// Handle dirty source models if existing (if source URI is a folder, this can be multiple)
const dirtySourceModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), source, !platform.isLinux /* ignorecase */));
const dirtySourceModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), source));
const dirtyTargetModelUris: URI[] = [];
if (dirtySourceModels.length) {
await Promise.all(dirtySourceModels.map(async sourceModel => {
@@ -475,7 +474,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
let targetModelResource: URI;
// If the source is the actual model, just use target as new resource
if (isEqual(sourceModelResource, source, !platform.isLinux /* ignorecase */)) {
if (isEqual(sourceModelResource, source)) {
targetModelResource = target;
}
@@ -498,10 +497,12 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
// Rename to target
try {
await this.fileService.move(source, target, overwrite);
const stat = await this.fileService.move(source, target, overwrite);
// Load models that were dirty before
await Promise.all(dirtyTargetModelUris.map(dirtyTargetModel => this.models.loadOrCreate(dirtyTargetModel)));
return stat;
} catch (error) {
// In case of an error, discard any dirty target backups that were made
@@ -536,7 +537,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
async confirmSave(resources?: URI[]): Promise<ConfirmResult> {
if (this.environmentService.isExtensionDevelopment) {
return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assum we run interactive (e.g. tests)
return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assume we run interactive (e.g. tests)
}
const resourcesToConfirm = this.getDirty(resources);
@@ -591,11 +592,11 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
// split up between files and untitled
const filesToSave: URI[] = [];
const untitledToSave: URI[] = [];
toSave.forEach(s => {
if ((Array.isArray(arg1) || arg1 === true /* includeUntitled */) && s.scheme === Schemas.untitled) {
untitledToSave.push(s);
toSave.forEach(resourceToSave => {
if ((Array.isArray(arg1) || arg1 === true /* includeUntitled */) && resourceToSave.scheme === Schemas.untitled) {
untitledToSave.push(resourceToSave);
} else {
filesToSave.push(s);
filesToSave.push(resourceToSave);
}
});
@@ -646,18 +647,19 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
return result;
}
protected async promptForPath(resource: URI, defaultUri: URI): Promise<URI | undefined> {
protected async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined> {
// Help user to find a name for the file by opening it first
await this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true, } });
return this.fileDialogService.showSaveDialog(this.getSaveDialogOptions(defaultUri));
return this.fileDialogService.pickFileToSave(this.getSaveDialogOptions(defaultUri, availableFileSystems));
}
private getSaveDialogOptions(defaultUri: URI): ISaveDialogOptions {
private getSaveDialogOptions(defaultUri: URI, availableFileSystems?: string[]): ISaveDialogOptions {
const options: ISaveDialogOptions = {
defaultUri,
title: nls.localize('saveAsTitle', "Save As")
title: nls.localize('saveAsTitle', "Save As"),
availableFileSystems,
};
// Filters are only enabled on Windows where they work properly
@@ -763,7 +765,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
dialogPath = this.suggestFileName(resource);
}
targetResource = await this.promptForPath(resource, dialogPath);
targetResource = await this.promptForPath(resource, dialogPath, options ? options.availableFileSystems : undefined);
}
if (!targetResource) {
@@ -854,14 +856,15 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
return false;
}
// take over encoding, mode and model value from source model
// take over encoding, mode (only if more specific) and model value from source model
targetModel.updatePreferredEncoding(sourceModel.getEncoding());
if (sourceModel.isResolved() && targetModel.isResolved()) {
this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot()));
const mode = sourceModel.textEditorModel.getLanguageIdentifier();
if (mode.language !== PLAINTEXT_MODE_ID) {
targetModel.textEditorModel.setMode(mode); // only use if more specific than plain/text
const sourceMode = sourceModel.textEditorModel.getLanguageIdentifier();
const targetMode = targetModel.textEditorModel.getLanguageIdentifier();
if (sourceMode.language !== PLAINTEXT_MODE_ID && targetMode.language === PLAINTEXT_MODE_ID) {
targetModel.textEditorModel.setMode(sourceMode); // only use if more specific than plain/text
}
}
@@ -901,7 +904,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
return joinPath(lastActiveFolder, untitledFileName);
}
return schemeFilter === Schemas.file ? URI.file(untitledFileName) : URI.from({ scheme: schemeFilter, authority: remoteAuthority, path: posix.sep + untitledFileName });
return untitledResource.with({ path: untitledFileName });
}
async revert(resource: URI, options?: IRevertOptions): Promise<boolean> {

View File

@@ -125,7 +125,7 @@ export interface ITextFileService extends IDisposable {
/**
* Move a file. If the file is dirty, its contents will be preserved and restored.
*/
move(source: URI, target: URI, overwrite?: boolean): Promise<void>;
move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata>;
/**
* Brings up the confirm dialog to either save, don't save or cancel.
@@ -136,12 +136,12 @@ export interface ITextFileService extends IDisposable {
confirmSave(resources?: URI[]): Promise<ConfirmResult>;
/**
* Convinient fast access to the current auto save mode.
* Convenient fast access to the current auto save mode.
*/
getAutoSaveMode(): AutoSaveMode;
/**
* Convinient fast access to the raw configured auto save settings.
* Convenient fast access to the raw configured auto save settings.
*/
getAutoSaveConfiguration(): IAutoSaveConfiguration;
}
@@ -428,6 +428,7 @@ export interface ISaveOptions {
overwriteEncoding?: boolean;
skipSaveParticipants?: boolean;
writeElevated?: boolean;
availableFileSystems?: string[];
}
export interface ILoadOptions {
@@ -467,6 +468,8 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport
backup(target?: URI): Promise<void>;
hasBackup(): boolean;
isDirty(): boolean;
isResolved(): this is IResolvedTextFileEditorModel;

View File

@@ -13,7 +13,7 @@ import { IFileStatWithMetadata, ICreateFileOptions, FileOperationError, FileOper
import { Schemas } from 'vs/base/common/network';
import { exists, stat, chmod, rimraf, MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/base/node/pfs';
import { join, dirname } from 'vs/base/common/path';
import { isMacintosh, isLinux } from 'vs/base/common/platform';
import { isMacintosh } from 'vs/base/common/platform';
import product from 'vs/platform/product/node/product';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
@@ -390,7 +390,7 @@ export class EncodingOracle extends Disposable implements IResourceEncodings {
const defaultEncodingOverrides: IEncodingOverride[] = [];
// Global settings
defaultEncodingOverrides.push({ parent: URI.file(this.environmentService.appSettingsHome), encoding: UTF8 });
defaultEncodingOverrides.push({ parent: this.environmentService.userRoamingDataHome, encoding: UTF8 });
// Workspace files
defaultEncodingOverrides.push({ extension: WORKSPACE_EXTENSION, encoding: UTF8 });
@@ -490,7 +490,7 @@ export class EncodingOracle extends Disposable implements IResourceEncodings {
for (const override of this.encodingOverrides) {
// check if the resource is child of encoding override path
if (override.parent && isEqualOrParent(resource, override.parent, !isLinux /* ignorecase */)) {
if (override.parent && isEqualOrParent(resource, override.parent)) {
return override.encoding;
}

View File

@@ -17,12 +17,12 @@ import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl';
import { Schemas } from 'vs/base/common/network';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { rimraf, RimRafMode, copy, readFile, exists } from 'vs/base/node/pfs';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { FileService } from 'vs/workbench/services/files/common/fileService';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { FileService } from 'vs/platform/files/common/fileService';
import { NullLogService } from 'vs/platform/log/common/log';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { tmpdir } from 'os';
import { DiskFileSystemProvider } from 'vs/workbench/services/files/node/diskFileSystemProvider';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { generateUuid } from 'vs/base/common/uuid';
import { join, basename } from 'vs/base/common/path';
import { getPathFromAmdModule } from 'vs/base/common/amd';
@@ -76,7 +76,7 @@ suite('Files - TextFileService i/o', () => {
const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'textfileservice');
let accessor: ServiceAccessor;
let disposables: IDisposable[] = [];
const disposables = new DisposableStore();
let service: ITextFileService;
let testDir: string;
@@ -88,8 +88,8 @@ suite('Files - TextFileService i/o', () => {
const fileService = new FileService(logService);
const fileProvider = new DiskFileSystemProvider(logService);
disposables.push(fileService.registerProvider(Schemas.file, fileProvider));
disposables.push(fileProvider);
disposables.add(fileService.registerProvider(Schemas.file, fileProvider));
disposables.add(fileProvider);
const collection = new ServiceCollection();
collection.set(IFileService, fileService);
@@ -108,7 +108,7 @@ suite('Files - TextFileService i/o', () => {
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
accessor.untitledEditorService.revertAll();
disposables = dispose(disposables);
disposables.clear();
await rimraf(parentDir, RimRafMode.MOVE);
});
@@ -247,7 +247,10 @@ suite('Files - TextFileService i/o', () => {
}
test('write - use encoding (cp1252)', async () => {
await testEncodingKeepsData(URI.file(join(testDir, 'some_cp1252.txt')), 'cp1252', ['ObjectCount = LoadObjects("Öffentlicher Ordner");', '', 'Private = "Persönliche Information"', ''].join(isWindows ? '\r\n' : '\n'));
const filePath = join(testDir, 'some_cp1252.txt');
const contents = await readFile(filePath, 'utf8');
const eol = /\r\n/.test(contents) ? '\r\n' : '\n';
await testEncodingKeepsData(URI.file(filePath), 'cp1252', ['ObjectCount = LoadObjects("Öffentlicher Ordner");', '', 'Private = "Persönliche Information"', ''].join(eol));
});
test('write - use encoding (shiftjis)', async () => {