Vscode merge (#4582)

* Merge from vscode 37cb23d3dd4f9433d56d4ba5ea3203580719a0bd

* fix issues with merges

* bump node version in azpipe

* replace license headers

* remove duplicate launch task

* fix build errors

* fix build errors

* fix tslint issues

* working through package and linux build issues

* more work

* wip

* fix packaged builds

* working through linux build errors

* wip

* wip

* wip

* fix mac and linux file limits

* iterate linux pipeline

* disable editor typing

* revert series to parallel

* remove optimize vscode from linux

* fix linting issues

* revert testing change

* add work round for new node

* readd packaging for extensions

* fix issue with angular not resolving decorator dependencies
This commit is contained in:
Anthony Dresser
2019-03-19 17:44:35 -07:00
committed by GitHub
parent 833d197412
commit 87765e8673
1879 changed files with 54505 additions and 38058 deletions

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/paths';
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';
@@ -12,7 +12,7 @@ import { URI } from 'vs/base/common/uri';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, IRawTextContent, ILoadOptions, LoadReason } from 'vs/workbench/services/textfile/common/textfiles';
import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, IRawTextContent, ILoadOptions, LoadReason, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
import { EncodingMode } from 'vs/workbench/common/editor';
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
@@ -29,7 +29,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { isLinux } from 'vs/base/common/platform';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { isEqual, isEqualOrParent } from 'vs/base/common/resources';
import { isEqual, isEqualOrParent, extname, basename } from 'vs/base/common/resources';
import { onUnexpectedError } from 'vs/base/common/errors';
/**
@@ -45,8 +45,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
private static saveErrorHandler: ISaveErrorHandler;
static setSaveErrorHandler(handler: ISaveErrorHandler): void { TextFileEditorModel.saveErrorHandler = handler; }
private static saveParticipant: ISaveParticipant;
static setSaveParticipant(handler: ISaveParticipant): void { TextFileEditorModel.saveParticipant = handler; }
private static saveParticipant: ISaveParticipant | null;
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; }
@@ -62,15 +62,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
private bufferSavedVersionId: number;
private lastResolvedDiskStat: IFileStat;
private blockModelContentChange: boolean;
private autoSaveAfterMillies: number;
private autoSaveAfterMillies?: number;
private autoSaveAfterMilliesEnabled: boolean;
private autoSaveDisposable: IDisposable;
private autoSaveDisposable?: IDisposable;
private contentChangeEventScheduler: RunOnceScheduler;
private orphanedChangeEventScheduler: RunOnceScheduler;
private saveSequentializer: SaveSequentializer;
private disposed: boolean;
private lastSaveAttemptTime: number;
private createTextEditorModelPromise: Promise<TextFileEditorModel>;
private createTextEditorModelPromise: Promise<TextFileEditorModel> | null;
private inConflictMode: boolean;
private inOrphanMode: boolean;
private inErrorMode: boolean;
@@ -129,7 +129,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
private onFileChanges(e: FileChangesEvent): void {
let fileEventImpactsModel = false;
let newInOrphanModeGuess: boolean;
let newInOrphanModeGuess: boolean | undefined;
// If we are currently orphaned, we check if the model file was added back
if (this.inOrphanMode) {
@@ -215,7 +215,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Unset flags
const undo = this.setDirty(false);
let loadPromise: Promise<TextFileEditorModel>;
let loadPromise: Promise<any>;
if (soft) {
loadPromise = Promise.resolve();
} else {
@@ -235,7 +235,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
});
}
load(options?: ILoadOptions): Promise<TextFileEditorModel> {
load(options?: ILoadOptions): Promise<ITextFileEditorModel> {
this.logService.trace('load() - enter', this.resource);
// It is very important to not reload the model when the model is dirty.
@@ -268,11 +268,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
if (!!backup) {
const content: IRawTextContent = {
resource: this.resource,
name: path.basename(this.resource.fsPath),
name: basename(this.resource),
mtime: Date.now(),
etag: undefined,
value: createTextBufferFactory(''), /* will be filled later from backup */
encoding: this.fileService.encoding.getWriteEncoding(this.resource, this.preferredEncoding),
encoding: this.fileService.encoding.getWriteEncoding(this.resource, this.preferredEncoding).encoding,
isReadonly: false
};
@@ -289,7 +289,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
const allowBinary = this.isResolved() /* always allow if we resolved previously */ || (options && options.allowBinary);
// Decide on etag
let etag: string;
let etag: string | undefined;
if (forceReadFromDisk) {
etag = undefined; // reset ETag if we enforce to read from disk
} else if (this.lastResolvedDiskStat) {
@@ -435,7 +435,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.updateSavedVersionId();
}
private doCreateTextModel(resource: URI, value: ITextBufferFactory, backup: URI): Promise<TextFileEditorModel> {
private doCreateTextModel(resource: URI, value: ITextBufferFactory, backup: URI | undefined): Promise<TextFileEditorModel> {
this.logService.trace('load() - created text editor model', this.resource);
this.createTextEditorModelPromise = this.doLoadBackup(backup).then(backupContent => {
@@ -443,7 +443,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Create model
const hasBackupContent = !!backupContent;
this.createTextEditorModel(hasBackupContent ? backupContent : value, resource);
this.createTextEditorModel(backupContent ? backupContent : value, resource);
// We restored a backup so we have to set the model as being dirty
// We also want to trigger auto save if it is enabled to simulate the exact same behaviour
@@ -480,18 +480,20 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// where `value` was captured in the content change listener closure scope.
// Content Change
this._register(this.textEditorModel.onDidChangeContent(() => this.onModelContentChanged()));
if (this.textEditorModel) {
this._register(this.textEditorModel.onDidChangeContent(() => this.onModelContentChanged()));
}
}
private doLoadBackup(backup: URI): Promise<ITextBufferFactory | null> {
private doLoadBackup(backup: URI | undefined): Promise<ITextBufferFactory | null> {
if (!backup) {
return Promise.resolve(null);
}
return this.backupFileService.resolveBackupContent(backup).then(backupContent => backupContent, error => null /* ignore errors */);
return this.backupFileService.resolveBackupContent(backup).then(backupContent => backupContent || null, error => null /* ignore errors */);
}
protected getOrCreateMode(modeService: IModeService, preferredModeIds: string, firstLineText?: string): ILanguageSelection {
protected getOrCreateMode(modeService: IModeService, preferredModeIds: string | undefined, firstLineText?: string): ILanguageSelection {
return modeService.createByFilepathOrFirstLine(this.resource.fsPath, firstLineText);
}
@@ -511,7 +513,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// In this case we clear the dirty flag and emit a SAVED event to indicate this state.
// Note: we currently only do this check when auto-save is turned off because there you see
// a dirty indicator that you want to get rid of when undoing to the saved version.
if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
this.logService.trace('onModelContentChanged() - model content changed back to last saved version', this.resource);
// Clear flags
@@ -609,7 +611,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;
return this.saveSequentializer.pendingSave || Promise.resolve(undefined);
}
// Return early if not dirty (unless forced) or version changed meanwhile
@@ -642,7 +644,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Push all edit operations to the undo stack so that the user has a chance to
// Ctrl+Z back to the saved version. We only do this when auto-save is turned off
if (!this.autoSaveAfterMilliesEnabled) {
if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel) {
this.textEditorModel.pushStackElement();
}
@@ -659,7 +661,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
};
this.blockModelContentChange = true;
saveParticipantPromise = TextFileEditorModel.saveParticipant.participate(this, { reason: options.reason }).then(onCompleteOrError, onCompleteOrError);
saveParticipantPromise = TextFileEditorModel.saveParticipant.participate(this as IResolvedTextFileEditorModel, { reason: options.reason }).then(onCompleteOrError, onCompleteOrError);
}
// mark the save participant as current pending save operation
@@ -698,7 +700,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Save to Disk
// mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering)
this.logService.trace(`doSave(${versionId}) - before updateContent()`, this.resource);
return this.saveSequentializer.setPending(newVersionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, this.createSnapshot(), {
const snapshot = this.createSnapshot();
if (!snapshot) {
throw new Error('Invalid snapshot');
}
return this.saveSequentializer.setPending(newVersionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, snapshot, {
overwriteReadonly: options.overwriteReadonly,
overwriteEncoding: options.overwriteEncoding,
mtime: this.lastResolvedDiskStat.mtime,
@@ -708,26 +714,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}).then(stat => {
this.logService.trace(`doSave(${versionId}) - after updateContent()`, this.resource);
// 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
} else {
/* __GDPR__
"filePUT" : {
"${include}": [
"${FileTelemetryData}"
]
}
*/
this.telemetryService.publicLog('filePUT', this.getTelemetryData(options.reason));
}
// Update dirty state unless model has changed meanwhile
if (versionId === this.versionId) {
this.logService.trace(`doSave(${versionId}) - setting dirty to false because versionId did not change`, this.resource);
@@ -744,6 +730,33 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Emit File Saved Event
this._onDidStateChange.fire(StateChange.SAVED);
// Telemetry
let telemetryPromise: Thenable<void>;
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
telemetryPromise = Promise.resolve();
} else {
telemetryPromise = this.getTelemetryData(options.reason).then(data => {
/* __GDPR__
"filePUT" : {
"${include}": [
"${FileTelemetryData}"
]
}
*/
this.telemetryService.publicLog('filePUT', data);
});
}
return telemetryPromise;
}, error => {
if (!error) {
error = new Error('Unknown Save Error'); // TODO@remote we should never get null as error (https://github.com/Microsoft/vscode/issues/55051)
@@ -769,7 +782,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
private getTypeIfSettings(): string {
if (path.extname(this.resource.fsPath) !== '.json') {
if (extname(this.resource) !== '.json') {
return '';
}
@@ -784,12 +797,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
// Check for locale file
if (isEqual(this.resource, URI.file(path.join(this.environmentService.appSettingsHome, 'locale.json')), !isLinux)) {
if (isEqual(this.resource, URI.file(join(this.environmentService.appSettingsHome, 'locale.json')), !isLinux)) {
return 'locale';
}
// Check for snippets
if (isEqualOrParent(this.resource, URI.file(path.join(this.environmentService.appSettingsHome, 'snippets')))) {
if (isEqualOrParent(this.resource, URI.file(join(this.environmentService.appSettingsHome, 'snippets')))) {
return 'snippets';
}
@@ -798,7 +811,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
for (const folder of folders) {
// {{SQL CARBON EDIT}}
if (isEqualOrParent(this.resource, folder.toResource('.azuredatastudio'))) {
const filename = path.basename(this.resource.fsPath);
const filename = basename(this.resource);
if (TextFileEditorModel.WHITELIST_WORKSPACE_JSON.indexOf(filename) > -1) {
// {{SQL CARBON EDIT}}
return `.azuredatastudio/${filename}`;
@@ -809,34 +822,40 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return '';
}
private getTelemetryData(reason: number): Object {
const ext = path.extname(this.resource.fsPath);
const fileName = path.basename(this.resource.fsPath);
const telemetryData = {
mimeType: guessMimeTypes(this.resource.fsPath).join(', '),
ext,
path: this.hashService.createSHA1(this.resource.fsPath),
reason
};
private getTelemetryData(reason: number | undefined): Thenable<object> {
return this.hashService.createSHA1(this.resource.fsPath).then(hashedPath => {
const ext = extname(this.resource);
const fileName = basename(this.resource);
const telemetryData = {
mimeType: guessMimeTypes(this.resource.fsPath).join(', '),
ext,
path: hashedPath,
reason
};
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" }
if (ext === '.json' && TextFileEditorModel.WHITELIST_JSON.indexOf(fileName) > -1) {
telemetryData['whitelistedjson'] = fileName;
}
*/
return telemetryData;
/* __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;
});
}
private doTouch(versionId: number): Promise<void> {
return this.saveSequentializer.setPending(versionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, this.createSnapshot(), {
const snapshot = this.createSnapshot();
if (!snapshot) {
throw new Error('invalid snapshot');
}
return this.saveSequentializer.setPending(versionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, snapshot, {
mtime: this.lastResolvedDiskStat.mtime,
encoding: this.getEncoding(),
etag: this.lastResolvedDiskStat.etag
@@ -916,8 +935,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return this.lastSaveAttemptTime;
}
getETag(): string {
return this.lastResolvedDiskStat ? this.lastResolvedDiskStat.etag : null;
getETag(): string | null {
return this.lastResolvedDiskStat ? this.lastResolvedDiskStat.etag || null : null;
}
hasState(state: ModelState): boolean {
@@ -1006,7 +1025,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
isReadonly(): boolean {
return this.lastResolvedDiskStat && this.lastResolvedDiskStat.isReadonly;
return !!(this.lastResolvedDiskStat && this.lastResolvedDiskStat.isReadonly);
}
isDisposed(): boolean {
@@ -1048,8 +1067,8 @@ interface ISaveOperation {
}
export class SaveSequentializer {
private _pendingSave: IPendingSave;
private _nextSave: ISaveOperation;
private _pendingSave?: IPendingSave;
private _nextSave?: ISaveOperation;
hasPendingSave(versionId?: number): boolean {
if (!this._pendingSave) {
@@ -1063,7 +1082,7 @@ export class SaveSequentializer {
return !!this._pendingSave;
}
get pendingSave(): Promise<void> {
get pendingSave(): Promise<void> | undefined {
return this._pendingSave ? this._pendingSave.promise : undefined;
}
@@ -1112,8 +1131,8 @@ export class SaveSequentializer {
this._nextSave = {
run,
promise,
promiseResolve: promiseResolve,
promiseReject: promiseReject
promiseResolve: promiseResolve!,
promiseReject: promiseReject!
};
}
@@ -1131,6 +1150,6 @@ class DefaultSaveErrorHandler implements ISaveErrorHandler {
constructor(@INotificationService private readonly notificationService: INotificationService) { }
onSaveError(error: any, model: TextFileEditorModel): void {
this.notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", path.basename(model.getResource().fsPath), toErrorMessage(error, false)));
this.notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", basename(model.getResource()), toErrorMessage(error, false)));
}
}

View File

@@ -117,7 +117,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
return 250;
}
get(resource: URI): ITextFileEditorModel {
get(resource: URI): ITextFileEditorModel | undefined {
return this.mapResourceToModel.get(resource);
}
@@ -153,12 +153,12 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
// Model does not exist
else {
model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : undefined);
const newModel = model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : undefined);
modelPromise = model.load(options);
// Install state change listener
this.mapResourceToStateChangeListener.set(resource, model.onDidStateChange(state => {
const event = new TextFileModelChangeEvent(model, state);
const event = new TextFileModelChangeEvent(newModel, state);
switch (state) {
case StateChange.DIRTY:
this._onModelDirty.fire(event);
@@ -183,7 +183,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
// Install model content change listener
this.mapResourceToModelContentChangeListener.set(resource, model.onDidContentChange(e => {
this._onModelContentChanged.fire(new TextFileModelChangeEvent(model, e));
this._onModelContentChanged.fire(new TextFileModelChangeEvent(newModel, e));
}));
}
@@ -207,7 +207,9 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
}, error => {
// Free resources of this invalid model
model.dispose();
if (model) {
model.dispose();
}
// Remove from pending loads
this.mapResourceToPendingModelLoaders.delete(resource);

View File

@@ -5,7 +5,6 @@
import * as nls from 'vs/nls';
import { URI } from 'vs/base/common/uri';
import * as paths from 'vs/base/common/paths';
import * as errors from 'vs/base/common/errors';
import * as objects from 'vs/base/common/objects';
import { Event, Emitter } from 'vs/base/common/event';
@@ -28,10 +27,17 @@ import { ResourceMap } from 'vs/base/common/map';
import { Schemas } from 'vs/base/common/network';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
import { createTextBufferFactoryFromSnapshot, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel';
import { IModelService } from 'vs/editor/common/services/modelService';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { isEqualOrParent, isEqual, joinPath, dirname } from 'vs/base/common/resources';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { isEqualOrParent, isEqual, joinPath, dirname, extname, basename } from 'vs/base/common/resources';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
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';
import { coalesce } from 'vs/base/common/arrays';
import { trim } from 'vs/base/common/strings';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export interface IBackupResult {
didBackup: boolean;
@@ -42,7 +48,7 @@ export interface IBackupResult {
*
* It also adds diagnostics and logging around file system operations.
*/
export abstract class TextFileService extends Disposable implements ITextFileService {
export class TextFileService extends Disposable implements ITextFileService {
_serviceBrand: any;
@@ -57,34 +63,38 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
private _models: TextFileEditorModelManager;
private currentFilesAssociationConfig: { [key: string]: string; };
private configuredAutoSaveDelay: number;
private configuredAutoSaveDelay?: number;
private configuredAutoSaveOnFocusChange: boolean;
private configuredAutoSaveOnWindowChange: boolean;
private configuredHotExit: string;
private autoSaveContext: IContextKey<string>;
constructor(
private lifecycleService: ILifecycleService,
private contextService: IWorkspaceContextService,
private configurationService: IConfigurationService,
protected fileService: IFileService,
private untitledEditorService: IUntitledEditorService,
private instantiationService: IInstantiationService,
private notificationService: INotificationService,
protected environmentService: IEnvironmentService,
private backupFileService: IBackupFileService,
private windowsService: IWindowsService,
protected windowService: IWindowService,
private historyService: IHistoryService,
contextKeyService: IContextKeyService,
private modelService: IModelService
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IFileService protected readonly fileService: IFileService,
@IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IModeService private readonly modeService: IModeService,
@IModelService private readonly modelService: IModelService,
@IWindowService private readonly windowService: IWindowService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@INotificationService private readonly notificationService: INotificationService,
@IBackupFileService private readonly backupFileService: IBackupFileService,
@IWindowsService private readonly windowsService: IWindowsService,
@IHistoryService private readonly historyService: IHistoryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IDialogService private readonly dialogService: IDialogService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@IEditorService private readonly editorService: IEditorService
) {
super();
this._models = this.instantiationService.createInstance(TextFileEditorModelManager);
this._models = instantiationService.createInstance(TextFileEditorModelManager);
this.autoSaveContext = AutoSaveContext.bindTo(contextKeyService);
const configuration = this.configurationService.getValue<IFilesConfiguration>();
const configuration = configurationService.getValue<IFilesConfiguration>();
this.currentFilesAssociationConfig = configuration && configuration.files && configuration.files.associations;
this.onFilesConfigurationChange(configuration);
@@ -96,11 +106,124 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
return this._models;
}
abstract resolveTextContent(resource: URI, options?: IResolveContentOptions): Promise<IRawTextContent>;
resolveTextContent(resource: URI, options?: IResolveContentOptions): Promise<IRawTextContent> {
return this.fileService.resolveStreamContent(resource, options).then(streamContent => {
return createTextBufferFactoryFromStream(streamContent.value).then(res => {
const r: IRawTextContent = {
resource: streamContent.resource,
name: streamContent.name,
mtime: streamContent.mtime,
etag: streamContent.etag,
encoding: streamContent.encoding,
isReadonly: streamContent.isReadonly,
value: res
};
return r;
});
});
}
abstract promptForPath(resource: URI, defaultPath: URI): Promise<URI>;
promptForPath(resource: URI, defaultUri: URI): Promise<URI | undefined> {
abstract confirmSave(resources?: URI[]): Promise<ConfirmResult>;
// Help user to find a name for the file by opening it first
return this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true, } }).then(() => {
return this.fileDialogService.showSaveDialog(this.getSaveDialogOptions(defaultUri));
});
}
private getSaveDialogOptions(defaultUri: URI): ISaveDialogOptions {
const options: ISaveDialogOptions = {
defaultUri,
title: nls.localize('saveAsTitle', "Save As")
};
// Filters are only enabled on Windows where they work properly
if (!platform.isWindows) {
return options;
}
interface IFilter { name: string; extensions: string[]; }
// Build the file filter by using our known languages
const ext: string | undefined = defaultUri ? extname(defaultUri) : undefined;
let matchingFilter: IFilter | undefined;
const filters: IFilter[] = coalesce(this.modeService.getRegisteredLanguageNames().map(languageName => {
const extensions = this.modeService.getExtensions(languageName);
if (!extensions || !extensions.length) {
return null;
}
const filter: IFilter = { name: languageName, extensions: extensions.slice(0, 10).map(e => trim(e, '.')) };
if (ext && extensions.indexOf(ext) >= 0) {
matchingFilter = filter;
return null; // matching filter will be added last to the top
}
return filter;
}));
// Filters are a bit weird on Windows, based on having a match or not:
// Match: we put the matching filter first so that it shows up selected and the all files last
// No match: we put the all files filter first
const allFilesFilter = { name: nls.localize('allFiles', "All Files"), extensions: ['*'] };
if (matchingFilter) {
filters.unshift(matchingFilter);
filters.unshift(allFilesFilter);
} else {
filters.unshift(allFilesFilter);
}
// Allow to save file without extension
filters.push({ name: nls.localize('noExt', "No Extension"), extensions: [''] });
options.filters = filters;
return options;
}
confirmSave(resources?: URI[]): Promise<ConfirmResult> {
if (this.environmentService.isExtensionDevelopment) {
return Promise.resolve(ConfirmResult.DONT_SAVE); // no veto when we are in extension dev mode because we cannot assum we run interactive (e.g. tests)
}
const resourcesToConfirm = this.getDirty(resources);
if (resourcesToConfirm.length === 0) {
return Promise.resolve(ConfirmResult.DONT_SAVE);
}
const message = resourcesToConfirm.length === 1 ? nls.localize('saveChangesMessage', "Do you want to save the changes you made to {0}?", basename(resourcesToConfirm[0]))
: getConfirmMessage(nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", resourcesToConfirm.length), resourcesToConfirm);
const buttons: string[] = [
resourcesToConfirm.length > 1 ? nls.localize({ key: 'saveAll', comment: ['&& denotes a mnemonic'] }, "&&Save All") : nls.localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save"),
nls.localize({ key: 'dontSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save"),
nls.localize('cancel', "Cancel")
];
return this.dialogService.show(Severity.Warning, message, buttons, {
cancelId: 2,
detail: nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them.")
}).then(index => {
switch (index) {
case 0: return ConfirmResult.SAVE;
case 1: return ConfirmResult.DONT_SAVE;
default: return ConfirmResult.CANCEL;
}
});
}
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 same name already exists in the folder {0}. Replacing it will overwrite its current contents.", basename(dirname(resource))),
primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
type: 'warning'
};
return this.dialogService.confirm(confirm).then(result => result.confirmed);
}
private registerListeners(): void {
@@ -133,7 +256,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
return this.handleDirtyBeforeShutdown(remainingDirty, reason);
}
return undefined;
return false;
});
}
@@ -176,7 +299,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
// closed is the only VS Code window open, except for on Mac where hot exit is only
// ever activated when quit is requested.
let doBackup: boolean;
let doBackup: boolean | undefined;
switch (reason) {
case ShutdownReason.CLOSE:
if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.configuredHotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
@@ -221,7 +344,10 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
const untitledToBackup: URI[] = [];
dirtyToBackup.forEach(s => {
if (this.fileService.canHandleResource(s)) {
filesToBackup.push(textFileEditorModelManager.get(s));
const model = textFileEditorModelManager.get(s);
if (model) {
filesToBackup.push(model);
}
} else if (s.scheme === Schemas.untitled) {
untitledToBackup.push(s);
}
@@ -231,9 +357,16 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
}
private doBackupAll(dirtyFileModels: ITextFileEditorModel[], untitledResources: URI[]): Promise<void> {
const promises = dirtyFileModels.map(model => {
const snapshot = model.createSnapshot();
if (snapshot) {
return this.backupFileService.backupResource(model.getResource(), snapshot, model.getVersionId());
}
return Promise.resolve();
});
// Handle file resources first
return Promise.all(dirtyFileModels.map(model => this.backupFileService.backupResource(model.getResource(), model.createSnapshot(), model.getVersionId()))).then(results => {
return Promise.all(promises).then(results => {
// Handle untitled resources
const untitledModelPromises = untitledResources
@@ -242,7 +375,11 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
return Promise.all(untitledModelPromises).then(untitledModels => {
const untitledBackupPromises = untitledModels.map(model => {
return this.backupFileService.backupResource(model.getResource(), model.createSnapshot(), model.getVersionId());
const snapshot = model.createSnapshot();
if (snapshot) {
return this.backupFileService.backupResource(model.getResource(), snapshot, model.getVersionId());
}
return Promise.resolve();
});
return Promise.all(untitledBackupPromises).then(() => undefined);
@@ -279,7 +416,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
return true; // veto
}
return undefined;
return false;
});
}
@@ -392,7 +529,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
}
}
return this.saveAll([resource], options).then(result => result.results.length === 1 && result.results[0].success);
return this.saveAll([resource], options).then(result => result.results.length === 1 && !!result.results[0].success);
}
saveAll(includeUntitled?: boolean, options?: ISaveOptions): Promise<ITextFileOperationResult>;
@@ -434,7 +571,7 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
// Untitled with associated file path don't need to prompt
if (this.untitledEditorService.hasAssociatedFilePath(untitled)) {
targetUri = untitled.with({ scheme: Schemas.file });
targetUri = this.untitledToAssociatedFileResource(untitled);
}
// Otherwise ask user
@@ -471,6 +608,12 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
});
}
private untitledToAssociatedFileResource(untitled: URI): URI {
const authority = this.windowService.getConfiguration().remoteAuthority;
return authority ? untitled.with({ scheme: REMOTE_HOST_SCHEME, authority }) : untitled.with({ scheme: Schemas.file });
}
private doSaveAllFiles(resources?: URI[], options: ISaveOptions = Object.create(null)): Promise<ITextFileOperationResult> {
const dirtyFileModels = this.getDirtyFileModels(Array.isArray(resources) ? resources : undefined /* Save All */)
.filter(model => {
@@ -491,7 +634,10 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
return Promise.all(dirtyFileModels.map(model => {
return model.save(options).then(() => {
if (!model.isDirty()) {
mapResourceToResult.get(model.getResource()).success = true;
const result = mapResourceToResult.get(model.getResource());
if (result) {
result.success = true;
}
}
});
})).then(r => ({ results: mapResourceToResult.values() }));
@@ -518,10 +664,10 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
return this.getFileModels(arg1).filter(model => model.isDirty());
}
saveAs(resource: URI, target?: URI, options?: ISaveOptions): Promise<URI> {
saveAs(resource: URI, target?: URI, options?: ISaveOptions): Promise<URI | undefined> {
// Get to target resource
let targetPromise: Promise<URI>;
let targetPromise: Promise<URI | undefined>;
if (target) {
targetPromise = Promise.resolve(target);
} else {
@@ -533,9 +679,9 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
targetPromise = this.promptForPath(resource, dialogPath);
}
return targetPromise.then(target => {
return targetPromise.then<URI | undefined>(target => {
if (!target) {
return null; // user canceled
return undefined; // user canceled
}
// Just save if target is same as models own resource
@@ -548,17 +694,17 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
});
}
private doSaveAs(resource: URI, target?: URI, options?: ISaveOptions): Promise<URI> {
private doSaveAs(resource: URI, target: URI, options?: ISaveOptions): Promise<URI> {
// Retrieve text model from provided resource if any
let modelPromise: Promise<ITextFileEditorModel | UntitledEditorModel> = Promise.resolve(null);
let modelPromise: Promise<ITextFileEditorModel | UntitledEditorModel | undefined> = Promise.resolve(undefined);
if (this.fileService.canHandleResource(resource)) {
modelPromise = Promise.resolve(this._models.get(resource));
} else if (resource.scheme === Schemas.untitled && this.untitledEditorService.exists(resource)) {
modelPromise = this.untitledEditorService.loadOrCreate({ resource });
}
return modelPromise.then<any>(model => {
return modelPromise.then(model => {
// We have a model: Use it (can be null e.g. if this file is binary and not a text file or was never opened before)
if (model) {
@@ -566,8 +712,13 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
}
// Otherwise we can only copy
return this.fileService.copyFile(resource, target);
}).then(() => {
return this.fileService.copyFile(resource, target).then(() => true);
}).then(result => {
// Return early if the operation was not running
if (!result) {
return target;
}
// Revert the source
return this.revert(resource).then(() => {
@@ -578,30 +729,61 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
});
}
private doSaveTextFileAs(sourceModel: ITextFileEditorModel | UntitledEditorModel, resource: URI, target: URI, options?: ISaveOptions): Promise<void> {
private doSaveTextFileAs(sourceModel: ITextFileEditorModel | UntitledEditorModel, resource: URI, target: URI, options?: ISaveOptions): Promise<boolean> {
let targetModelResolver: Promise<ITextFileEditorModel>;
let targetExists: boolean = false;
// Prefer an existing model if it is already loaded for the given target resource
const targetModel = this.models.get(target);
if (targetModel && targetModel.isResolved()) {
targetModelResolver = Promise.resolve(targetModel);
targetExists = true;
}
// Otherwise create the target file empty if it does not exist already and resolve it from there
else {
targetModelResolver = this.fileService.resolveFile(target).then(stat => stat, () => null).then(stat => stat || this.fileService.updateContent(target, '')).then(stat => {
return this.models.loadOrCreate(target);
});
targetModelResolver = this.fileService.existsFile(target).then<any>(exists => {
targetExists = exists;
// create target model adhoc if file does not exist yet
if (!targetExists) {
return this.fileService.updateContent(target, '');
}
return undefined;
}).then(() => this.models.loadOrCreate(target));
}
return targetModelResolver.then(targetModel => {
// take over encoding and model value from source model
targetModel.updatePreferredEncoding(sourceModel.getEncoding());
this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot()));
// 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 confirmWrite: Promise<boolean>;
if (sourceModel instanceof UntitledEditorModel && sourceModel.hasAssociatedFilePath && targetExists && isEqual(target, this.untitledToAssociatedFileResource(sourceModel.getResource()))) {
confirmWrite = this.confirmOverwrite(target);
} else {
confirmWrite = Promise.resolve(true);
}
// save model
return targetModel.save(options);
return confirmWrite.then(write => {
if (!write) {
return false;
}
// take over encoding and model value from source model
targetModel.updatePreferredEncoding(sourceModel.getEncoding());
if (targetModel.textEditorModel) {
const snapshot = sourceModel.createSnapshot();
if (snapshot) {
this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(snapshot));
}
}
// save model
return targetModel.save(options).then(() => true);
});
}, error => {
// binary model: delete the file and run the operation again
@@ -615,8 +797,8 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
private suggestFileName(untitledResource: URI): URI {
const untitledFileName = this.untitledEditorService.suggestFileName(untitledResource);
const schemeFilter = Schemas.file;
const remoteAuthority = this.windowService.getConfiguration().remoteAuthority;
const schemeFilter = remoteAuthority ? REMOTE_HOST_SCHEME : Schemas.file;
const lastActiveFile = this.historyService.getLastActiveFile(schemeFilter);
if (lastActiveFile) {
@@ -629,11 +811,11 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
return joinPath(lastActiveFolder, untitledFileName);
}
return URI.file(untitledFileName);
return schemeFilter === Schemas.file ? URI.file(untitledFileName) : URI.from({ scheme: schemeFilter, authority: remoteAuthority, path: '/' + untitledFileName });
}
revert(resource: URI, options?: IRevertOptions): Promise<boolean> {
return this.revertAll([resource], options).then(result => result.results.length === 1 && result.results[0].success);
return this.revertAll([resource], options).then(result => result.results.length === 1 && !!result.results[0].success);
}
revertAll(resources?: URI[], options?: IRevertOptions): Promise<ITextFileOperationResult> {
@@ -662,13 +844,19 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
return Promise.all(fileModels.map(model => {
return model.revert(options && options.soft).then(() => {
if (!model.isDirty()) {
mapResourceToResult.get(model.getResource()).success = true;
const result = mapResourceToResult.get(model.getResource());
if (result) {
result.success = true;
}
}
}, error => {
// FileNotFound means the file got deleted meanwhile, so still record as successful revert
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
mapResourceToResult.get(model.getResource()).success = true;
const result = mapResourceToResult.get(model.getResource());
if (result) {
result.success = true;
}
}
// Otherwise bubble up the error
@@ -747,14 +935,18 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
// Otherwise a parent folder of the source is being moved, so we need
// to compute the target resource based on that
else {
targetModelResource = sourceModelResource.with({ path: paths.join(target.path, sourceModelResource.path.substr(source.path.length + 1)) });
targetModelResource = sourceModelResource.with({ path: joinPath(target, sourceModelResource.path.substr(source.path.length + 1)).path });
}
// Remember as dirty target model to load after the operation
dirtyTargetModels.push(targetModelResource);
// Backup dirty source model to the target resource it will become later
return this.backupFileService.backupResource(targetModelResource, sourceModel.createSnapshot(), sourceModel.getVersionId());
const snapshot = sourceModel.createSnapshot();
if (snapshot) {
return this.backupFileService.backupResource(targetModelResource, snapshot, sourceModel.getVersionId());
}
return Promise.resolve();
}));
} else {
handleDirtySourceModels = Promise.resolve();
@@ -818,3 +1010,5 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
super.dispose();
}
}
registerSingleton(ITextFileService, TextFileService);

View File

@@ -10,7 +10,7 @@ import { IEncodingSupport, ConfirmResult, IRevertOptions } from 'vs/workbench/co
import { IBaseStat, IResolveContentOptions, ITextSnapshot } from 'vs/platform/files/common/files';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { ITextBufferFactory } from 'vs/editor/common/model';
import { ITextBufferFactory, ITextModel } from 'vs/editor/common/model';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
/**
@@ -29,7 +29,7 @@ export interface ISaveParticipant {
/**
* Participate in a save of a model. Allows to change the model before it is being saved to disk.
*/
participate(model: ITextFileEditorModel, env: { reason: SaveReason }): Promise<void>;
participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason }): Promise<void>;
}
/**
@@ -101,7 +101,7 @@ export interface IResult {
}
export interface IAutoSaveConfiguration {
autoSaveDelay: number;
autoSaveDelay?: number;
autoSaveFocusChange: boolean;
autoSaveApplicationChange: boolean;
}
@@ -189,7 +189,7 @@ export interface ITextFileEditorModelManager {
onModelsSaved: Event<TextFileModelChangeEvent[]>;
onModelsReverted: Event<TextFileModelChangeEvent[]>;
get(resource: URI): ITextFileEditorModel;
get(resource: URI): ITextFileEditorModel | undefined;
getAll(resource?: URI): ITextFileEditorModel[];
@@ -236,7 +236,7 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport
hasState(state: ModelState): boolean;
getETag(): string;
getETag(): string | null;
updatePreferredEncoding(encoding: string): void;
@@ -246,7 +246,7 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport
revert(soft?: boolean): Promise<void>;
createSnapshot(): ITextSnapshot;
createSnapshot(): ITextSnapshot | null;
isDirty(): boolean;
@@ -255,6 +255,12 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport
isDisposed(): boolean;
}
export interface IResolvedTextFileEditorModel extends ITextFileEditorModel {
readonly textEditorModel: ITextModel;
createSnapshot(): ITextSnapshot;
}
export interface IWillMoveEvent {
oldResource: URI;
@@ -313,9 +319,9 @@ export interface ITextFileService extends IDisposable {
* @param resource the resource to save as.
* @param targetResource the optional target to save to.
* @param options optional save options
* @return true if the file was saved.
* @return Path of the saved resource.
*/
saveAs(resource: URI, targetResource?: URI, options?: ISaveOptions): Promise<URI>;
saveAs(resource: URI, targetResource?: URI, options?: ISaveOptions): Promise<URI | undefined>;
/**
* Saves the set of resources and returns a promise with the operation result.

View File

@@ -1,165 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as paths from 'vs/base/common/paths';
import * as strings from 'vs/base/common/strings';
import { isWindows } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { ConfirmResult } from 'vs/workbench/common/editor';
import { TextFileService as AbstractTextFileService } from 'vs/workbench/services/textfile/common/textFileService';
import { IRawTextContent } from 'vs/workbench/services/textfile/common/textfiles';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IFileService, IResolveContentOptions } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IModeService } from 'vs/editor/common/services/modeService';
import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IModelService } from 'vs/editor/common/services/modelService';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { getConfirmMessage, IDialogService, ISaveDialogOptions, IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { coalesce } from 'vs/base/common/arrays';
export class TextFileService extends AbstractTextFileService {
constructor(
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IFileService fileService: IFileService,
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
@ILifecycleService lifecycleService: ILifecycleService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@IModeService private readonly modeService: IModeService,
@IModelService modelService: IModelService,
@IWindowService windowService: IWindowService,
@IEnvironmentService environmentService: IEnvironmentService,
@INotificationService notificationService: INotificationService,
@IBackupFileService backupFileService: IBackupFileService,
@IWindowsService windowsService: IWindowsService,
@IHistoryService historyService: IHistoryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IDialogService private readonly dialogService: IDialogService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@IEditorService private readonly editorService: IEditorService
) {
super(lifecycleService, contextService, configurationService, fileService, untitledEditorService, instantiationService, notificationService, environmentService, backupFileService, windowsService, windowService, historyService, contextKeyService, modelService);
}
resolveTextContent(resource: URI, options?: IResolveContentOptions): Promise<IRawTextContent> {
return this.fileService.resolveStreamContent(resource, options).then(streamContent => {
return createTextBufferFactoryFromStream(streamContent.value).then(res => {
const r: IRawTextContent = {
resource: streamContent.resource,
name: streamContent.name,
mtime: streamContent.mtime,
etag: streamContent.etag,
encoding: streamContent.encoding,
isReadonly: streamContent.isReadonly,
value: res
};
return r;
});
});
}
confirmSave(resources?: URI[]): Promise<ConfirmResult> {
if (this.environmentService.isExtensionDevelopment) {
return Promise.resolve(ConfirmResult.DONT_SAVE); // no veto when we are in extension dev mode because we cannot assum we run interactive (e.g. tests)
}
const resourcesToConfirm = this.getDirty(resources);
if (resourcesToConfirm.length === 0) {
return Promise.resolve(ConfirmResult.DONT_SAVE);
}
const message = resourcesToConfirm.length === 1 ? nls.localize('saveChangesMessage', "Do you want to save the changes you made to {0}?", paths.basename(resourcesToConfirm[0].fsPath))
: getConfirmMessage(nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", resourcesToConfirm.length), resourcesToConfirm);
const buttons: string[] = [
resourcesToConfirm.length > 1 ? nls.localize({ key: 'saveAll', comment: ['&& denotes a mnemonic'] }, "&&Save All") : nls.localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save"),
nls.localize({ key: 'dontSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save"),
nls.localize('cancel', "Cancel")
];
return this.dialogService.show(Severity.Warning, message, buttons, {
cancelId: 2,
detail: nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them.")
}).then(index => {
switch (index) {
case 0: return ConfirmResult.SAVE;
case 1: return ConfirmResult.DONT_SAVE;
default: return ConfirmResult.CANCEL;
}
});
}
promptForPath(resource: URI, defaultUri: URI): Promise<URI> {
// Help user to find a name for the file by opening it first
return this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true, } }).then(() => {
return this.fileDialogService.showSaveDialog(this.getSaveDialogOptions(defaultUri));
});
}
private getSaveDialogOptions(defaultUri: URI): ISaveDialogOptions {
const options: ISaveDialogOptions = {
defaultUri,
title: nls.localize('saveAsTitle', "Save As")
};
// Filters are only enabled on Windows where they work properly
if (!isWindows) {
return options;
}
interface IFilter { name: string; extensions: string[]; }
// Build the file filter by using our known languages
const ext: string = defaultUri ? paths.extname(defaultUri.path) : undefined;
let matchingFilter: IFilter;
const filters: IFilter[] = coalesce(this.modeService.getRegisteredLanguageNames().map(languageName => {
const extensions = this.modeService.getExtensions(languageName);
if (!extensions || !extensions.length) {
return null;
}
const filter: IFilter = { name: languageName, extensions: extensions.slice(0, 10).map(e => strings.trim(e, '.')) };
if (ext && extensions.indexOf(ext) >= 0) {
matchingFilter = filter;
return null; // matching filter will be added last to the top
}
return filter;
}));
// Filters are a bit weird on Windows, based on having a match or not:
// Match: we put the matching filter first so that it shows up selected and the all files last
// No match: we put the all files filter first
const allFilesFilter = { name: nls.localize('allFiles', "All Files"), extensions: ['*'] };
if (matchingFilter) {
filters.unshift(matchingFilter);
filters.unshift(allFilesFilter);
} else {
filters.unshift(allFilesFilter);
}
// Allow to save file without extension
filters.push({ name: nls.localize('noExt', "No Extension"), extensions: [''] });
options.filters = filters;
return options;
}
}

View File

@@ -7,10 +7,12 @@ import { URI } from 'vs/base/common/uri';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
import { OperatingSystem, OS } from 'vs/base/common/platform';
import { IRemoteAgentService, IRemoteAgentEnvironment } from 'vs/workbench/services/remote/node/remoteAgentService';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { Schemas } from 'vs/base/common/network';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
export class TextResourcePropertiesService implements ITextResourcePropertiesService {
@@ -45,11 +47,12 @@ export class TextResourcePropertiesService implements ITextResourcePropertiesSer
if (remoteAuthority) {
if (resource.scheme !== Schemas.file) {
const osCacheKey = `resource.authority.os.${remoteAuthority}`;
os = this.remoteEnvironment ? this.remoteEnvironment.os : /* Get it from cache */ this.storageService.getInteger(osCacheKey, StorageScope.WORKSPACE, OS);
os = this.remoteEnvironment ? this.remoteEnvironment.os : /* Get it from cache */ this.storageService.getNumber(osCacheKey, StorageScope.WORKSPACE, OS);
this.storageService.store(osCacheKey, os, StorageScope.WORKSPACE);
}
}
return os;
}
}
}
registerSingleton(ITextResourcePropertiesService, TextResourcePropertiesService, true);

View File

@@ -48,7 +48,7 @@ suite('Files - TextFileEditorModel', () => {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
return model.load().then(() => {
model.textEditorModel.setValue('bar');
model.textEditorModel!.setValue('bar');
assert.ok(getLastModifiedTime(model) <= Date.now());
return model.save().then(() => {
@@ -90,7 +90,7 @@ suite('Files - TextFileEditorModel', () => {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
return model.load().then(() => {
model.textEditorModel.dispose();
model.textEditorModel!.dispose();
assert.ok(model.isDisposed());
});
@@ -117,7 +117,7 @@ suite('Files - TextFileEditorModel', () => {
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model.textEditorModel!.setValue('foo');
assert.ok(model.isDirty());
assert.ok(model.hasState(ModelState.DIRTY));
@@ -141,13 +141,13 @@ suite('Files - TextFileEditorModel', () => {
});
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model.textEditorModel!.setValue('foo');
assert.ok(model.isDirty());
return model.revert().then(() => {
assert.ok(!model.isDirty());
assert.equal(model.textEditorModel.getValue(), 'Hello Html');
assert.equal(model.textEditorModel!.getValue(), 'Hello Html');
assert.equal(eventCounter, 1);
model.dispose();
@@ -167,13 +167,13 @@ suite('Files - TextFileEditorModel', () => {
});
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model.textEditorModel!.setValue('foo');
assert.ok(model.isDirty());
return model.revert(true /* soft revert */).then(() => {
assert.ok(!model.isDirty());
assert.equal(model.textEditorModel.getValue(), 'foo');
assert.equal(model.textEditorModel!.getValue(), 'foo');
assert.equal(eventCounter, 1);
model.dispose();
@@ -186,7 +186,7 @@ suite('Files - TextFileEditorModel', () => {
return model.load().then(() => {
accessor.fileService.setContent('Hello Change');
return model.load().then(() => {
model.textEditorModel.undo();
model.textEditorModel!.undo();
assert.ok(model.isDirty());
});
@@ -227,7 +227,7 @@ suite('Files - TextFileEditorModel', () => {
return input1.resolve().then((model1: TextFileEditorModel) => {
return input2.resolve().then((model2: TextFileEditorModel) => {
model1.textEditorModel.setValue('foo');
model1.textEditorModel!.setValue('foo');
const m1Mtime = model1.getStat().mtime;
const m2Mtime = model2.getStat().mtime;
@@ -238,7 +238,7 @@ suite('Files - TextFileEditorModel', () => {
assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt')));
assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt')));
model2.textEditorModel.setValue('foo');
model2.textEditorModel!.setValue('foo');
assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt')));
return timeout(10).then(() => {
@@ -264,7 +264,7 @@ suite('Files - TextFileEditorModel', () => {
model.onDidStateChange(e => {
if (e === StateChange.SAVED) {
assert.equal(snapshotToString(model.createSnapshot()), 'bar');
assert.equal(snapshotToString(model.createSnapshot()!), 'bar');
assert.ok(!model.isDirty());
eventCounter++;
}
@@ -281,7 +281,7 @@ suite('Files - TextFileEditorModel', () => {
});
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model.textEditorModel!.setValue('foo');
return model.save().then(() => {
model.dispose();
@@ -302,7 +302,7 @@ suite('Files - TextFileEditorModel', () => {
});
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model.textEditorModel!.setValue('foo');
const now = Date.now();
return model.save().then(() => {
@@ -322,7 +322,7 @@ suite('Files - TextFileEditorModel', () => {
});
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model.textEditorModel!.setValue('foo');
return model.save().then(() => {
model.dispose();
});
@@ -337,7 +337,7 @@ suite('Files - TextFileEditorModel', () => {
assert.ok(!sequentializer.pendingSave);
// pending removes itself after done
return sequentializer.setPending(1, Promise.resolve(null)).then(() => {
return sequentializer.setPending(1, Promise.resolve()).then(() => {
assert.ok(!sequentializer.hasPendingSave());
assert.ok(!sequentializer.hasPendingSave(1));
assert.ok(!sequentializer.pendingSave);
@@ -361,11 +361,11 @@ suite('Files - TextFileEditorModel', () => {
const sequentializer = new SaveSequentializer();
let pendingDone = false;
sequentializer.setPending(1, timeout(1).then(() => { pendingDone = true; return null; }));
sequentializer.setPending(1, timeout(1).then(() => { pendingDone = true; return; }));
// next finishes instantly
let nextDone = false;
const res = sequentializer.setNext(() => Promise.resolve(null).then(() => { nextDone = true; return null; }));
const res = sequentializer.setNext(() => Promise.resolve(null).then(() => { nextDone = true; return; }));
return res.then(() => {
assert.ok(pendingDone);
@@ -377,11 +377,11 @@ suite('Files - TextFileEditorModel', () => {
const sequentializer = new SaveSequentializer();
let pendingDone = false;
sequentializer.setPending(1, timeout(1).then(() => { pendingDone = true; return null; }));
sequentializer.setPending(1, timeout(1).then(() => { pendingDone = true; return; }));
// next finishes after timeout
let nextDone = false;
const res = sequentializer.setNext(() => timeout(1).then(() => { nextDone = true; return null; }));
const res = sequentializer.setNext(() => timeout(1).then(() => { nextDone = true; return; }));
return res.then(() => {
assert.ok(pendingDone);
@@ -393,17 +393,17 @@ suite('Files - TextFileEditorModel', () => {
const sequentializer = new SaveSequentializer();
let pendingDone = false;
sequentializer.setPending(1, timeout(1).then(() => { pendingDone = true; return null; }));
sequentializer.setPending(1, timeout(1).then(() => { pendingDone = true; return; }));
// next finishes after timeout
let firstDone = false;
let firstRes = sequentializer.setNext(() => timeout(2).then(() => { firstDone = true; return null; }));
let firstRes = sequentializer.setNext(() => timeout(2).then(() => { firstDone = true; return; }));
let secondDone = false;
let secondRes = sequentializer.setNext(() => timeout(3).then(() => { secondDone = true; return null; }));
let secondRes = sequentializer.setNext(() => timeout(3).then(() => { secondDone = true; return; }));
let thirdDone = false;
let thirdRes = sequentializer.setNext(() => timeout(4).then(() => { thirdDone = true; return null; }));
let thirdRes = sequentializer.setNext(() => timeout(4).then(() => { thirdDone = true; return; }));
return Promise.all([firstRes, secondRes, thirdRes]).then(() => {
assert.ok(pendingDone);

View File

@@ -7,12 +7,12 @@ import * as assert from 'assert';
import { URI } from 'vs/base/common/uri';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
import { join } from 'vs/base/common/paths';
import { workbenchInstantiationService, TestFileService } from 'vs/workbench/test/workbenchTestServices';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { IFileService, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files';
import { IModelService } from 'vs/editor/common/services/modelService';
import { timeout } from 'vs/base/common/async';
import { toResource } from 'vs/base/test/common/utils';
export class TestTextFileEditorModelManager extends TextFileEditorModelManager {
@@ -29,10 +29,6 @@ class ServiceAccessor {
}
}
function toResource(path: string): URI {
return URI.file(join('C:\\', path));
}
suite('Files - TextFileEditorModelManager', () => {
let instantiationService: IInstantiationService;
@@ -46,9 +42,9 @@ suite('Files - TextFileEditorModelManager', () => {
test('add, remove, clear, get, getAll', function () {
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource('/path/random1.txt'), 'utf8');
const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource('/path/random2.txt'), 'utf8');
const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource('/path/random3.txt'), 'utf8');
const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8');
const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8');
const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8');
manager.add(URI.file('/test.html'), model1);
manager.add(URI.file('/some/other.html'), model2);
@@ -126,9 +122,9 @@ suite('Files - TextFileEditorModelManager', () => {
test('removed from cache when model disposed', function () {
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource('/path/random1.txt'), 'utf8');
const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource('/path/random2.txt'), 'utf8');
const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource('/path/random3.txt'), 'utf8');
const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8');
const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8');
const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8');
manager.add(URI.file('/test.html'), model1);
manager.add(URI.file('/some/other.html'), model2);
@@ -143,14 +139,14 @@ suite('Files - TextFileEditorModelManager', () => {
model3.dispose();
});
test('events', () => {
test('events', function () {
TextFileEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = 0;
TextFileEditorModel.DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 0;
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
const resource1 = toResource('/path/index.txt');
const resource2 = toResource('/path/other.txt');
const resource1 = toResource.call(this, '/path/index.txt');
const resource2 = toResource.call(this, '/path/other.txt');
let dirtyCounter = 0;
let revertedCounter = 0;
@@ -198,11 +194,11 @@ suite('Files - TextFileEditorModelManager', () => {
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.ADDED }]));
return manager.loadOrCreate(resource2, { encoding: 'utf8' }).then(model2 => {
model1.textEditorModel.setValue('changed');
model1.textEditorModel!.setValue('changed');
model1.updatePreferredEncoding('utf16');
return model1.revert().then(() => {
model1.textEditorModel.setValue('changed again');
model1.textEditorModel!.setValue('changed again');
return model1.save().then(() => {
model1.dispose();
@@ -235,8 +231,8 @@ suite('Files - TextFileEditorModelManager', () => {
test('events debounced', function () {
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
const resource1 = toResource('/path/index.txt');
const resource2 = toResource('/path/other.txt');
const resource1 = toResource.call(this, '/path/index.txt');
const resource2 = toResource.call(this, '/path/other.txt');
let dirtyCounter = 0;
let revertedCounter = 0;
@@ -261,11 +257,11 @@ suite('Files - TextFileEditorModelManager', () => {
return manager.loadOrCreate(resource1, { encoding: 'utf8' }).then(model1 => {
return manager.loadOrCreate(resource2, { encoding: 'utf8' }).then(model2 => {
model1.textEditorModel.setValue('changed');
model1.textEditorModel!.setValue('changed');
model1.updatePreferredEncoding('utf16');
return model1.revert().then(() => {
model1.textEditorModel.setValue('changed again');
model1.textEditorModel!.setValue('changed again');
return model1.save().then(() => {
model1.dispose();
@@ -293,7 +289,7 @@ suite('Files - TextFileEditorModelManager', () => {
test('disposing model takes it out of the manager', function () {
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
const resource = toResource('/path/index_something.txt');
const resource = toResource.call(this, '/path/index_something.txt');
return manager.loadOrCreate(resource, { encoding: 'utf8' }).then(model => {
model.dispose();
@@ -308,10 +304,10 @@ suite('Files - TextFileEditorModelManager', () => {
test('dispose prevents dirty model from getting disposed', function () {
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
const resource = toResource('/path/index_something.txt');
const resource = toResource.call(this, '/path/index_something.txt');
return manager.loadOrCreate(resource, { encoding: 'utf8' }).then(model => {
model.textEditorModel.setValue('make dirty');
model.textEditorModel!.setValue('make dirty');
manager.disposeModel(model as TextFileEditorModel);
assert.ok(!model.isDisposed());

View File

@@ -58,7 +58,9 @@ suite('Files - TextFileService', () => {
});
teardown(() => {
model.dispose();
if (model) {
model.dispose();
}
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
accessor.untitledEditorService.revertAll();
@@ -89,7 +91,7 @@ suite('Files - TextFileService', () => {
service.setConfirmResult(ConfirmResult.CANCEL);
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model.textEditorModel!.setValue('foo');
assert.equal(service.getDirty().length, 1);
@@ -109,7 +111,7 @@ suite('Files - TextFileService', () => {
service.onFilesConfigurationChange({ files: { hotExit: 'off' } });
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model.textEditorModel!.setValue('foo');
assert.equal(service.getDirty().length, 1);
@@ -140,7 +142,7 @@ suite('Files - TextFileService', () => {
service.onFilesConfigurationChange({ files: { hotExit: 'off' } });
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model.textEditorModel!.setValue('foo');
assert.equal(service.getDirty().length, 1);
@@ -161,7 +163,7 @@ suite('Files - TextFileService', () => {
const service = accessor.textFileService;
return model.load().then(() => {
assert.ok(!service.isDirty(model.getResource()));
model.textEditorModel.setValue('foo');
model.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
assert.equal(service.getDirty().length, 1);
@@ -171,7 +173,7 @@ suite('Files - TextFileService', () => {
return untitled.resolve().then((model: UntitledEditorModel) => {
assert.ok(!service.isDirty(untitled.getResource()));
assert.equal(service.getDirty().length, 1);
model.textEditorModel.setValue('changed');
model.textEditorModel!.setValue('changed');
assert.ok(service.isDirty(untitled.getResource()));
assert.equal(service.getDirty().length, 2);
@@ -187,7 +189,7 @@ suite('Files - TextFileService', () => {
const service = accessor.textFileService;
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
@@ -212,7 +214,7 @@ suite('Files - TextFileService', () => {
sinon.stub(accessor.modelService, 'updateModel', () => { });
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model.textEditorModel!.setValue('foo');
return accessor.textFileService.saveAll(true).then(res => {
assert.ok(loadOrCreateStub.calledOnce);
@@ -233,7 +235,7 @@ suite('Files - TextFileService', () => {
const service = accessor.textFileService;
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
@@ -254,12 +256,12 @@ suite('Files - TextFileService', () => {
service.setPromptPath(model.getResource());
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
return service.saveAs(model.getResource()).then(res => {
assert.equal(res.toString(), model.getResource().toString());
assert.equal(res!.toString(), model.getResource().toString());
assert.ok(!service.isDirty(model.getResource()));
});
});
@@ -273,7 +275,7 @@ suite('Files - TextFileService', () => {
service.setPromptPath(model.getResource());
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model!.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
@@ -291,7 +293,7 @@ suite('Files - TextFileService', () => {
const service = accessor.textFileService;
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model!.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
@@ -310,7 +312,7 @@ suite('Files - TextFileService', () => {
const service = accessor.textFileService;
return sourceModel.load().then(() => {
sourceModel.textEditorModel.setValue('foo');
sourceModel.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(sourceModel.getResource()));
@@ -447,7 +449,7 @@ suite('Files - TextFileService', () => {
service.setConfirmResult(ConfirmResult.CANCEL);
return model.load().then(() => {
model.textEditorModel.setValue('foo');
model.textEditorModel!.setValue('foo');
assert.equal(service.getDirty().length, 1);