mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
SQL Operations Studio Public Preview 1 (0.23) release source code
This commit is contained in:
1076
src/vs/workbench/services/textfile/common/textFileEditorModel.ts
Normal file
1076
src/vs/workbench/services/textfile/common/textFileEditorModel.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,348 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import Event, { Emitter, debounceEvent } from 'vs/base/common/event';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
|
||||
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ITextFileEditorModel, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange, IModelLoadOrCreateOptions } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
|
||||
export class TextFileEditorModelManager implements ITextFileEditorModelManager {
|
||||
private toUnbind: IDisposable[];
|
||||
|
||||
private _onModelDisposed: Emitter<URI>;
|
||||
private _onModelContentChanged: Emitter<TextFileModelChangeEvent>;
|
||||
private _onModelDirty: Emitter<TextFileModelChangeEvent>;
|
||||
private _onModelSaveError: Emitter<TextFileModelChangeEvent>;
|
||||
private _onModelSaved: Emitter<TextFileModelChangeEvent>;
|
||||
private _onModelReverted: Emitter<TextFileModelChangeEvent>;
|
||||
private _onModelEncodingChanged: Emitter<TextFileModelChangeEvent>;
|
||||
private _onModelOrphanedChanged: Emitter<TextFileModelChangeEvent>;
|
||||
|
||||
private _onModelsDirtyEvent: Event<TextFileModelChangeEvent[]>;
|
||||
private _onModelsSaveError: Event<TextFileModelChangeEvent[]>;
|
||||
private _onModelsSaved: Event<TextFileModelChangeEvent[]>;
|
||||
private _onModelsReverted: Event<TextFileModelChangeEvent[]>;
|
||||
|
||||
private mapResourceToDisposeListener: ResourceMap<IDisposable>;
|
||||
private mapResourceToStateChangeListener: ResourceMap<IDisposable>;
|
||||
private mapResourceToModelContentChangeListener: ResourceMap<IDisposable>;
|
||||
private mapResourceToModel: ResourceMap<ITextFileEditorModel>;
|
||||
private mapResourceToPendingModelLoaders: ResourceMap<TPromise<ITextFileEditorModel>>;
|
||||
|
||||
constructor(
|
||||
@ILifecycleService private lifecycleService: ILifecycleService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService
|
||||
) {
|
||||
this.toUnbind = [];
|
||||
|
||||
this._onModelDisposed = new Emitter<URI>();
|
||||
this._onModelContentChanged = new Emitter<TextFileModelChangeEvent>();
|
||||
this._onModelDirty = new Emitter<TextFileModelChangeEvent>();
|
||||
this._onModelSaveError = new Emitter<TextFileModelChangeEvent>();
|
||||
this._onModelSaved = new Emitter<TextFileModelChangeEvent>();
|
||||
this._onModelReverted = new Emitter<TextFileModelChangeEvent>();
|
||||
this._onModelEncodingChanged = new Emitter<TextFileModelChangeEvent>();
|
||||
this._onModelOrphanedChanged = new Emitter<TextFileModelChangeEvent>();
|
||||
|
||||
this.toUnbind.push(this._onModelDisposed);
|
||||
this.toUnbind.push(this._onModelContentChanged);
|
||||
this.toUnbind.push(this._onModelDirty);
|
||||
this.toUnbind.push(this._onModelSaveError);
|
||||
this.toUnbind.push(this._onModelSaved);
|
||||
this.toUnbind.push(this._onModelReverted);
|
||||
this.toUnbind.push(this._onModelEncodingChanged);
|
||||
this.toUnbind.push(this._onModelOrphanedChanged);
|
||||
|
||||
this.mapResourceToModel = new ResourceMap<ITextFileEditorModel>();
|
||||
this.mapResourceToDisposeListener = new ResourceMap<IDisposable>();
|
||||
this.mapResourceToStateChangeListener = new ResourceMap<IDisposable>();
|
||||
this.mapResourceToModelContentChangeListener = new ResourceMap<IDisposable>();
|
||||
this.mapResourceToPendingModelLoaders = new ResourceMap<TPromise<ITextFileEditorModel>>();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
||||
// Lifecycle
|
||||
this.lifecycleService.onShutdown(this.dispose, this);
|
||||
}
|
||||
|
||||
public get onModelDisposed(): Event<URI> {
|
||||
return this._onModelDisposed.event;
|
||||
}
|
||||
|
||||
public get onModelContentChanged(): Event<TextFileModelChangeEvent> {
|
||||
return this._onModelContentChanged.event;
|
||||
}
|
||||
|
||||
public get onModelDirty(): Event<TextFileModelChangeEvent> {
|
||||
return this._onModelDirty.event;
|
||||
}
|
||||
|
||||
public get onModelSaveError(): Event<TextFileModelChangeEvent> {
|
||||
return this._onModelSaveError.event;
|
||||
}
|
||||
|
||||
public get onModelSaved(): Event<TextFileModelChangeEvent> {
|
||||
return this._onModelSaved.event;
|
||||
}
|
||||
|
||||
public get onModelReverted(): Event<TextFileModelChangeEvent> {
|
||||
return this._onModelReverted.event;
|
||||
}
|
||||
|
||||
public get onModelEncodingChanged(): Event<TextFileModelChangeEvent> {
|
||||
return this._onModelEncodingChanged.event;
|
||||
}
|
||||
|
||||
public get onModelOrphanedChanged(): Event<TextFileModelChangeEvent> {
|
||||
return this._onModelOrphanedChanged.event;
|
||||
}
|
||||
|
||||
public get onModelsDirty(): Event<TextFileModelChangeEvent[]> {
|
||||
if (!this._onModelsDirtyEvent) {
|
||||
this._onModelsDirtyEvent = this.debounce(this.onModelDirty);
|
||||
}
|
||||
|
||||
return this._onModelsDirtyEvent;
|
||||
}
|
||||
|
||||
public get onModelsSaveError(): Event<TextFileModelChangeEvent[]> {
|
||||
if (!this._onModelsSaveError) {
|
||||
this._onModelsSaveError = this.debounce(this.onModelSaveError);
|
||||
}
|
||||
|
||||
return this._onModelsSaveError;
|
||||
}
|
||||
|
||||
public get onModelsSaved(): Event<TextFileModelChangeEvent[]> {
|
||||
if (!this._onModelsSaved) {
|
||||
this._onModelsSaved = this.debounce(this.onModelSaved);
|
||||
}
|
||||
|
||||
return this._onModelsSaved;
|
||||
}
|
||||
|
||||
public get onModelsReverted(): Event<TextFileModelChangeEvent[]> {
|
||||
if (!this._onModelsReverted) {
|
||||
this._onModelsReverted = this.debounce(this.onModelReverted);
|
||||
}
|
||||
|
||||
return this._onModelsReverted;
|
||||
}
|
||||
|
||||
private debounce(event: Event<TextFileModelChangeEvent>): Event<TextFileModelChangeEvent[]> {
|
||||
return debounceEvent(event, (prev: TextFileModelChangeEvent[], cur: TextFileModelChangeEvent) => {
|
||||
if (!prev) {
|
||||
prev = [cur];
|
||||
} else {
|
||||
prev.push(cur);
|
||||
}
|
||||
return prev;
|
||||
}, this.debounceDelay());
|
||||
}
|
||||
|
||||
protected debounceDelay(): number {
|
||||
return 250;
|
||||
}
|
||||
|
||||
public get(resource: URI): ITextFileEditorModel {
|
||||
return this.mapResourceToModel.get(resource);
|
||||
}
|
||||
|
||||
public loadOrCreate(resource: URI, options?: IModelLoadOrCreateOptions): TPromise<ITextFileEditorModel> {
|
||||
|
||||
// Return early if model is currently being loaded
|
||||
const pendingLoad = this.mapResourceToPendingModelLoaders.get(resource);
|
||||
if (pendingLoad) {
|
||||
return pendingLoad;
|
||||
}
|
||||
|
||||
let modelPromise: TPromise<ITextFileEditorModel>;
|
||||
|
||||
// Model exists
|
||||
let model = this.get(resource);
|
||||
if (model) {
|
||||
if (!options || !options.reload) {
|
||||
modelPromise = TPromise.as(model);
|
||||
} else {
|
||||
modelPromise = model.load();
|
||||
}
|
||||
}
|
||||
|
||||
// Model does not exist
|
||||
else {
|
||||
model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : void 0);
|
||||
modelPromise = model.load();
|
||||
|
||||
// Install state change listener
|
||||
this.mapResourceToStateChangeListener.set(resource, model.onDidStateChange(state => {
|
||||
const event = new TextFileModelChangeEvent(model, state);
|
||||
switch (state) {
|
||||
case StateChange.DIRTY:
|
||||
this._onModelDirty.fire(event);
|
||||
break;
|
||||
case StateChange.SAVE_ERROR:
|
||||
this._onModelSaveError.fire(event);
|
||||
break;
|
||||
case StateChange.SAVED:
|
||||
this._onModelSaved.fire(event);
|
||||
break;
|
||||
case StateChange.REVERTED:
|
||||
this._onModelReverted.fire(event);
|
||||
break;
|
||||
case StateChange.ENCODING:
|
||||
this._onModelEncodingChanged.fire(event);
|
||||
break;
|
||||
case StateChange.ORPHANED_CHANGE:
|
||||
this._onModelOrphanedChanged.fire(event);
|
||||
break;
|
||||
}
|
||||
}));
|
||||
|
||||
// Install model content change listener
|
||||
this.mapResourceToModelContentChangeListener.set(resource, model.onDidContentChange(e => {
|
||||
this._onModelContentChanged.fire(new TextFileModelChangeEvent(model, e));
|
||||
}));
|
||||
}
|
||||
|
||||
// Store pending loads to avoid race conditions
|
||||
this.mapResourceToPendingModelLoaders.set(resource, modelPromise);
|
||||
|
||||
return modelPromise.then(model => {
|
||||
|
||||
// Make known to manager (if not already known)
|
||||
this.add(resource, model);
|
||||
|
||||
// Model can be dirty if a backup was restored, so we make sure to have this event delivered
|
||||
if (model.isDirty()) {
|
||||
this._onModelDirty.fire(new TextFileModelChangeEvent(model, StateChange.DIRTY));
|
||||
}
|
||||
|
||||
// Remove from pending loads
|
||||
this.mapResourceToPendingModelLoaders.delete(resource);
|
||||
|
||||
return model;
|
||||
}, error => {
|
||||
|
||||
// Free resources of this invalid model
|
||||
model.dispose();
|
||||
|
||||
// Remove from pending loads
|
||||
this.mapResourceToPendingModelLoaders.delete(resource);
|
||||
|
||||
return TPromise.wrapError<ITextFileEditorModel>(error);
|
||||
});
|
||||
}
|
||||
|
||||
public getAll(resource?: URI, filter?: (model: ITextFileEditorModel) => boolean): ITextFileEditorModel[] {
|
||||
if (resource) {
|
||||
const res = this.mapResourceToModel.get(resource);
|
||||
|
||||
return res ? [res] : [];
|
||||
}
|
||||
|
||||
const res: ITextFileEditorModel[] = [];
|
||||
this.mapResourceToModel.forEach(model => {
|
||||
if (!filter || filter(model)) {
|
||||
res.push(model);
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public add(resource: URI, model: ITextFileEditorModel): void {
|
||||
const knownModel = this.mapResourceToModel.get(resource);
|
||||
if (knownModel === model) {
|
||||
return; // already cached
|
||||
}
|
||||
|
||||
// dispose any previously stored dispose listener for this resource
|
||||
const disposeListener = this.mapResourceToDisposeListener.get(resource);
|
||||
if (disposeListener) {
|
||||
disposeListener.dispose();
|
||||
}
|
||||
|
||||
// store in cache but remove when model gets disposed
|
||||
this.mapResourceToModel.set(resource, model);
|
||||
this.mapResourceToDisposeListener.set(resource, model.onDispose(() => {
|
||||
this.remove(resource);
|
||||
this._onModelDisposed.fire(resource);
|
||||
}));
|
||||
}
|
||||
|
||||
public remove(resource: URI): void {
|
||||
this.mapResourceToModel.delete(resource);
|
||||
|
||||
const disposeListener = this.mapResourceToDisposeListener.get(resource);
|
||||
if (disposeListener) {
|
||||
dispose(disposeListener);
|
||||
this.mapResourceToDisposeListener.delete(resource);
|
||||
}
|
||||
|
||||
const stateChangeListener = this.mapResourceToStateChangeListener.get(resource);
|
||||
if (stateChangeListener) {
|
||||
dispose(stateChangeListener);
|
||||
this.mapResourceToStateChangeListener.delete(resource);
|
||||
}
|
||||
|
||||
const modelContentChangeListener = this.mapResourceToModelContentChangeListener.get(resource);
|
||||
if (modelContentChangeListener) {
|
||||
dispose(modelContentChangeListener);
|
||||
this.mapResourceToModelContentChangeListener.delete(resource);
|
||||
}
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
|
||||
// model caches
|
||||
this.mapResourceToModel.clear();
|
||||
this.mapResourceToPendingModelLoaders.clear();
|
||||
|
||||
// dispose dispose listeners
|
||||
this.mapResourceToDisposeListener.forEach(l => l.dispose());
|
||||
this.mapResourceToDisposeListener.clear();
|
||||
|
||||
// dispose state change listeners
|
||||
this.mapResourceToStateChangeListener.forEach(l => l.dispose());
|
||||
this.mapResourceToStateChangeListener.clear();
|
||||
|
||||
// dispose model content change listeners
|
||||
this.mapResourceToModelContentChangeListener.forEach(l => l.dispose());
|
||||
this.mapResourceToModelContentChangeListener.clear();
|
||||
}
|
||||
|
||||
public disposeModel(model: TextFileEditorModel): void {
|
||||
if (!model) {
|
||||
return; // we need data!
|
||||
}
|
||||
|
||||
if (model.isDisposed()) {
|
||||
return; // already disposed
|
||||
}
|
||||
|
||||
if (this.mapResourceToPendingModelLoaders.has(model.getResource())) {
|
||||
return; // not yet loaded
|
||||
}
|
||||
|
||||
if (model.isDirty()) {
|
||||
return; // not saved
|
||||
}
|
||||
|
||||
model.dispose();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.toUnbind = dispose(this.toUnbind);
|
||||
}
|
||||
}
|
||||
717
src/vs/workbench/services/textfile/common/textFileService.ts
Normal file
717
src/vs/workbench/services/textfile/common/textFileService.ts
Normal file
@@ -0,0 +1,717 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import paths = require('vs/base/common/paths');
|
||||
import errors = require('vs/base/common/errors');
|
||||
import objects = require('vs/base/common/objects');
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import platform = require('vs/base/common/platform');
|
||||
import { IWindowsService } from 'vs/platform/windows/common/windows';
|
||||
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
|
||||
import { IRevertOptions, IResult, ITextFileOperationResult, ITextFileService, IRawTextContent, IAutoSaveConfiguration, AutoSaveMode, SaveReason, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ISaveOptions } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ConfirmResult } from 'vs/workbench/common/editor';
|
||||
import { ILifecycleService, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { IFileService, IResolveContentOptions, IFilesConfiguration, FileOperationError, FileOperationResult, AutoSaveConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IUntitledEditorService, UNTITLED_SCHEMA } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
|
||||
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IMessageService, Severity } from 'vs/platform/message/common/message';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IHistoryService } from 'vs/workbench/services/history/common/history';
|
||||
|
||||
export interface IBackupResult {
|
||||
didBackup: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The workbench file service implementation implements the raw file service spec and adds additional methods on top.
|
||||
*
|
||||
* It also adds diagnostics and logging around file system operations.
|
||||
*/
|
||||
export abstract class TextFileService implements ITextFileService {
|
||||
|
||||
public _serviceBrand: any;
|
||||
|
||||
private toUnbind: IDisposable[];
|
||||
private _models: TextFileEditorModelManager;
|
||||
|
||||
private _onFilesAssociationChange: Emitter<void>;
|
||||
private currentFilesAssociationConfig: { [key: string]: string; };
|
||||
|
||||
private _onAutoSaveConfigurationChange: Emitter<IAutoSaveConfiguration>;
|
||||
private configuredAutoSaveDelay: number;
|
||||
private configuredAutoSaveOnFocusChange: boolean;
|
||||
private configuredAutoSaveOnWindowChange: boolean;
|
||||
|
||||
private configuredHotExit: string;
|
||||
|
||||
constructor(
|
||||
private lifecycleService: ILifecycleService,
|
||||
private contextService: IWorkspaceContextService,
|
||||
private configurationService: IConfigurationService,
|
||||
private telemetryService: ITelemetryService,
|
||||
protected fileService: IFileService,
|
||||
private untitledEditorService: IUntitledEditorService,
|
||||
private instantiationService: IInstantiationService,
|
||||
private messageService: IMessageService,
|
||||
protected environmentService: IEnvironmentService,
|
||||
private backupFileService: IBackupFileService,
|
||||
private windowsService: IWindowsService,
|
||||
private historyService: IHistoryService
|
||||
) {
|
||||
this.toUnbind = [];
|
||||
|
||||
this._onAutoSaveConfigurationChange = new Emitter<IAutoSaveConfiguration>();
|
||||
this.toUnbind.push(this._onAutoSaveConfigurationChange);
|
||||
|
||||
this._onFilesAssociationChange = new Emitter<void>();
|
||||
this.toUnbind.push(this._onFilesAssociationChange);
|
||||
|
||||
this._models = this.instantiationService.createInstance(TextFileEditorModelManager);
|
||||
|
||||
const configuration = this.configurationService.getConfiguration<IFilesConfiguration>();
|
||||
this.currentFilesAssociationConfig = configuration && configuration.files && configuration.files.associations;
|
||||
|
||||
this.onConfigurationChange(configuration);
|
||||
|
||||
this.telemetryService.publicLog('autoSave', this.getAutoSaveConfiguration());
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
public get models(): ITextFileEditorModelManager {
|
||||
return this._models;
|
||||
}
|
||||
|
||||
abstract resolveTextContent(resource: URI, options?: IResolveContentOptions): TPromise<IRawTextContent>;
|
||||
|
||||
abstract promptForPath(defaultPath?: string): string;
|
||||
|
||||
abstract confirmSave(resources?: URI[]): ConfirmResult;
|
||||
|
||||
public get onAutoSaveConfigurationChange(): Event<IAutoSaveConfiguration> {
|
||||
return this._onAutoSaveConfigurationChange.event;
|
||||
}
|
||||
|
||||
public get onFilesAssociationChange(): Event<void> {
|
||||
return this._onFilesAssociationChange.event;
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
||||
// Lifecycle
|
||||
this.lifecycleService.onWillShutdown(event => event.veto(this.beforeShutdown(event.reason)));
|
||||
this.lifecycleService.onShutdown(this.dispose, this);
|
||||
|
||||
// Configuration changes
|
||||
this.toUnbind.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationChange(this.configurationService.getConfiguration<IFilesConfiguration>())));
|
||||
}
|
||||
|
||||
private beforeShutdown(reason: ShutdownReason): boolean | TPromise<boolean> {
|
||||
|
||||
// Dirty files need treatment on shutdown
|
||||
const dirty = this.getDirty();
|
||||
if (dirty.length) {
|
||||
|
||||
// If auto save is enabled, save all files and then check again for dirty files
|
||||
let handleAutoSave: TPromise<URI[] /* remaining dirty resources */>;
|
||||
if (this.getAutoSaveMode() !== AutoSaveMode.OFF) {
|
||||
handleAutoSave = this.saveAll(false /* files only */).then(() => this.getDirty());
|
||||
} else {
|
||||
handleAutoSave = TPromise.as(dirty);
|
||||
}
|
||||
|
||||
return handleAutoSave.then(dirty => {
|
||||
|
||||
// If we still have dirty files, we either have untitled ones or files that cannot be saved
|
||||
// or auto save was not enabled and as such we did not save any dirty files to disk automatically
|
||||
if (dirty.length) {
|
||||
|
||||
// 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(result => {
|
||||
if (result.didBackup) {
|
||||
return this.noVeto({ cleanUpBackups: false }); // no veto and no backup cleanup (since backup was successful)
|
||||
}
|
||||
|
||||
// since a backup did not happen, we have to confirm for the dirty files now
|
||||
return this.confirmBeforeShutdown();
|
||||
}, errors => {
|
||||
const firstError = errors[0];
|
||||
this.messageService.show(Severity.Error, nls.localize('files.backup.failSave', "Files could not be backed up (Error: {0}), try saving your files to exit.", firstError.message));
|
||||
|
||||
return true; // veto, the backups failed
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise just confirm from the user what to do with the dirty files
|
||||
return this.confirmBeforeShutdown();
|
||||
}
|
||||
|
||||
return void 0;
|
||||
});
|
||||
}
|
||||
|
||||
// No dirty files: no veto
|
||||
return this.noVeto({ cleanUpBackups: true });
|
||||
}
|
||||
|
||||
private backupBeforeShutdown(dirtyToBackup: URI[], textFileEditorModelManager: ITextFileEditorModelManager, reason: ShutdownReason): TPromise<IBackupResult> {
|
||||
return this.windowsService.getWindowCount().then(windowCount => {
|
||||
|
||||
// When quit is requested skip the confirm callback and attempt to backup all workspaces.
|
||||
// When quit is not requested the confirm callback should be shown when the window being
|
||||
// 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;
|
||||
switch (reason) {
|
||||
case ShutdownReason.CLOSE:
|
||||
if (this.contextService.hasWorkspace() && this.configuredHotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
|
||||
doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured
|
||||
} else if (windowCount > 1 || platform.isMacintosh) {
|
||||
doBackup = false; // do not backup if a window is closed that does not cause quitting of the application
|
||||
} else {
|
||||
doBackup = true; // backup if last window is closed on win/linux where the application quits right after
|
||||
}
|
||||
break;
|
||||
|
||||
case ShutdownReason.QUIT:
|
||||
doBackup = true; // backup because next start we restore all backups
|
||||
break;
|
||||
|
||||
case ShutdownReason.RELOAD:
|
||||
doBackup = true; // backup because after window reload, backups restore
|
||||
break;
|
||||
|
||||
case ShutdownReason.LOAD:
|
||||
if (this.contextService.hasWorkspace() && this.configuredHotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
|
||||
doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured
|
||||
} else {
|
||||
doBackup = false; // do not backup because we are switching contexts
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!doBackup) {
|
||||
return TPromise.as({ didBackup: false });
|
||||
}
|
||||
|
||||
// Telemetry
|
||||
this.telemetryService.publicLog('hotExit:triggered', { reason, windowCount, fileCount: dirtyToBackup.length });
|
||||
|
||||
// Backup
|
||||
return this.backupAll(dirtyToBackup, textFileEditorModelManager).then(() => { return { didBackup: true }; });
|
||||
});
|
||||
}
|
||||
|
||||
private backupAll(dirtyToBackup: URI[], textFileEditorModelManager: ITextFileEditorModelManager): TPromise<void> {
|
||||
|
||||
// split up between files and untitled
|
||||
const filesToBackup: ITextFileEditorModel[] = [];
|
||||
const untitledToBackup: URI[] = [];
|
||||
dirtyToBackup.forEach(s => {
|
||||
if (s.scheme === Schemas.file) {
|
||||
filesToBackup.push(textFileEditorModelManager.get(s));
|
||||
} else if (s.scheme === UNTITLED_SCHEMA) {
|
||||
untitledToBackup.push(s);
|
||||
}
|
||||
});
|
||||
|
||||
return this.doBackupAll(filesToBackup, untitledToBackup);
|
||||
}
|
||||
|
||||
private doBackupAll(dirtyFileModels: ITextFileEditorModel[], untitledResources: URI[]): TPromise<void> {
|
||||
|
||||
// Handle file resources first
|
||||
return TPromise.join(dirtyFileModels.map(model => this.backupFileService.backupResource(model.getResource(), model.getValue(), model.getVersionId()))).then(results => {
|
||||
|
||||
// Handle untitled resources
|
||||
const untitledModelPromises = untitledResources
|
||||
.filter(untitled => this.untitledEditorService.exists(untitled))
|
||||
.map(untitled => this.untitledEditorService.loadOrCreate({ resource: untitled }));
|
||||
|
||||
return TPromise.join(untitledModelPromises).then(untitledModels => {
|
||||
const untitledBackupPromises = untitledModels.map(model => {
|
||||
return this.backupFileService.backupResource(model.getResource(), model.getValue(), model.getVersionId());
|
||||
});
|
||||
|
||||
return TPromise.join(untitledBackupPromises).then(() => void 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private confirmBeforeShutdown(): boolean | TPromise<boolean> {
|
||||
const confirm = this.confirmSave();
|
||||
|
||||
// Save
|
||||
if (confirm === ConfirmResult.SAVE) {
|
||||
return this.saveAll(true /* includeUntitled */).then(result => {
|
||||
if (result.results.some(r => !r.success)) {
|
||||
return true; // veto if some saves failed
|
||||
}
|
||||
|
||||
return this.noVeto({ cleanUpBackups: true });
|
||||
});
|
||||
}
|
||||
|
||||
// Don't Save
|
||||
else if (confirm === ConfirmResult.DONT_SAVE) {
|
||||
|
||||
// Make sure to revert untitled so that they do not restore
|
||||
// see https://github.com/Microsoft/vscode/issues/29572
|
||||
this.untitledEditorService.revertAll();
|
||||
|
||||
return this.noVeto({ cleanUpBackups: true });
|
||||
}
|
||||
|
||||
// Cancel
|
||||
else if (confirm === ConfirmResult.CANCEL) {
|
||||
return true; // veto
|
||||
}
|
||||
|
||||
return void 0;
|
||||
}
|
||||
|
||||
private noVeto(options: { cleanUpBackups: boolean }): boolean | TPromise<boolean> {
|
||||
if (!options.cleanUpBackups) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.cleanupBackupsBeforeShutdown().then(() => false, () => false);
|
||||
}
|
||||
|
||||
protected cleanupBackupsBeforeShutdown(): TPromise<void> {
|
||||
if (this.environmentService.isExtensionDevelopment) {
|
||||
return TPromise.as(void 0);
|
||||
}
|
||||
|
||||
return this.backupFileService.discardAllWorkspaceBackups();
|
||||
}
|
||||
|
||||
protected onConfigurationChange(configuration: IFilesConfiguration): void {
|
||||
const wasAutoSaveEnabled = (this.getAutoSaveMode() !== AutoSaveMode.OFF);
|
||||
|
||||
const autoSaveMode = (configuration && configuration.files && configuration.files.autoSave) || AutoSaveConfiguration.OFF;
|
||||
switch (autoSaveMode) {
|
||||
case AutoSaveConfiguration.AFTER_DELAY:
|
||||
this.configuredAutoSaveDelay = configuration && configuration.files && configuration.files.autoSaveDelay;
|
||||
this.configuredAutoSaveOnFocusChange = false;
|
||||
this.configuredAutoSaveOnWindowChange = false;
|
||||
break;
|
||||
|
||||
case AutoSaveConfiguration.ON_FOCUS_CHANGE:
|
||||
this.configuredAutoSaveDelay = void 0;
|
||||
this.configuredAutoSaveOnFocusChange = true;
|
||||
this.configuredAutoSaveOnWindowChange = false;
|
||||
break;
|
||||
|
||||
case AutoSaveConfiguration.ON_WINDOW_CHANGE:
|
||||
this.configuredAutoSaveDelay = void 0;
|
||||
this.configuredAutoSaveOnFocusChange = false;
|
||||
this.configuredAutoSaveOnWindowChange = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
this.configuredAutoSaveDelay = void 0;
|
||||
this.configuredAutoSaveOnFocusChange = false;
|
||||
this.configuredAutoSaveOnWindowChange = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Emit as event
|
||||
this._onAutoSaveConfigurationChange.fire(this.getAutoSaveConfiguration());
|
||||
|
||||
// save all dirty when enabling auto save
|
||||
if (!wasAutoSaveEnabled && this.getAutoSaveMode() !== AutoSaveMode.OFF) {
|
||||
this.saveAll().done(null, errors.onUnexpectedError);
|
||||
}
|
||||
|
||||
// Check for change in files associations
|
||||
const filesAssociation = configuration && configuration.files && configuration.files.associations;
|
||||
if (!objects.equals(this.currentFilesAssociationConfig, filesAssociation)) {
|
||||
this.currentFilesAssociationConfig = filesAssociation;
|
||||
this._onFilesAssociationChange.fire();
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
// Hot exit
|
||||
//const hotExitMode = configuration && configuration.files ? configuration.files.hotExit : HotExitConfiguration.ON_EXIT;
|
||||
const hotExitMode = HotExitConfiguration.OFF;
|
||||
if (hotExitMode === HotExitConfiguration.OFF || hotExitMode === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
|
||||
this.configuredHotExit = hotExitMode;
|
||||
} else {
|
||||
this.configuredHotExit = HotExitConfiguration.ON_EXIT;
|
||||
}
|
||||
}
|
||||
|
||||
public getDirty(resources?: URI[]): URI[] {
|
||||
|
||||
// Collect files
|
||||
const dirty = this.getDirtyFileModels(resources).map(m => m.getResource());
|
||||
|
||||
// Add untitled ones
|
||||
dirty.push(...this.untitledEditorService.getDirty(resources));
|
||||
|
||||
return dirty;
|
||||
}
|
||||
|
||||
public isDirty(resource?: URI): boolean {
|
||||
|
||||
// Check for dirty file
|
||||
if (this._models.getAll(resource).some(model => model.isDirty())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for dirty untitled
|
||||
return this.untitledEditorService.getDirty().some(dirty => !resource || dirty.toString() === resource.toString());
|
||||
}
|
||||
|
||||
public save(resource: URI, options?: ISaveOptions): TPromise<boolean> {
|
||||
|
||||
// Run a forced save if we detect the file is not dirty so that save participants can still run
|
||||
if (options && options.force && resource.scheme === Schemas.file && !this.isDirty(resource)) {
|
||||
const model = this._models.get(resource);
|
||||
if (model) {
|
||||
model.save({ force: true, reason: SaveReason.EXPLICIT }).then(() => !model.isDirty());
|
||||
}
|
||||
}
|
||||
|
||||
return this.saveAll([resource], options).then(result => result.results.length === 1 && result.results[0].success);
|
||||
}
|
||||
|
||||
public saveAll(includeUntitled?: boolean, options?: ISaveOptions): TPromise<ITextFileOperationResult>;
|
||||
public saveAll(resources: URI[], options?: ISaveOptions): TPromise<ITextFileOperationResult>;
|
||||
public saveAll(arg1?: any, options?: ISaveOptions): TPromise<ITextFileOperationResult> {
|
||||
|
||||
// get all dirty
|
||||
let toSave: URI[] = [];
|
||||
if (Array.isArray(arg1)) {
|
||||
toSave = this.getDirty(arg1);
|
||||
} else {
|
||||
toSave = this.getDirty();
|
||||
}
|
||||
|
||||
// split up between files and untitled
|
||||
const filesToSave: URI[] = [];
|
||||
const untitledToSave: URI[] = [];
|
||||
toSave.forEach(s => {
|
||||
if (s.scheme === Schemas.file) {
|
||||
filesToSave.push(s);
|
||||
} else if ((Array.isArray(arg1) || arg1 === true /* includeUntitled */) && s.scheme === UNTITLED_SCHEMA) {
|
||||
untitledToSave.push(s);
|
||||
}
|
||||
});
|
||||
|
||||
return this.doSaveAll(filesToSave, untitledToSave, options);
|
||||
}
|
||||
|
||||
private doSaveAll(fileResources: URI[], untitledResources: URI[], options?: ISaveOptions): TPromise<ITextFileOperationResult> {
|
||||
|
||||
// Handle files first that can just be saved
|
||||
return this.doSaveAllFiles(fileResources, options).then(result => {
|
||||
|
||||
// Preflight for untitled to handle cancellation from the dialog
|
||||
const targetsForUntitled: URI[] = [];
|
||||
for (let i = 0; i < untitledResources.length; i++) {
|
||||
const untitled = untitledResources[i];
|
||||
if (this.untitledEditorService.exists(untitled)) {
|
||||
let targetPath: string;
|
||||
|
||||
// Untitled with associated file path don't need to prompt
|
||||
if (this.untitledEditorService.hasAssociatedFilePath(untitled)) {
|
||||
targetPath = untitled.fsPath;
|
||||
}
|
||||
|
||||
// Otherwise ask user
|
||||
else {
|
||||
targetPath = this.promptForPath(this.suggestFileName(untitled));
|
||||
if (!targetPath) {
|
||||
return TPromise.as({
|
||||
results: [...fileResources, ...untitledResources].map(r => {
|
||||
return {
|
||||
source: r
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
targetsForUntitled.push(URI.file(targetPath));
|
||||
}
|
||||
}
|
||||
|
||||
// Handle untitled
|
||||
const untitledSaveAsPromises: TPromise<void>[] = [];
|
||||
targetsForUntitled.forEach((target, index) => {
|
||||
const untitledSaveAsPromise = this.saveAs(untitledResources[index], target).then(uri => {
|
||||
result.results.push({
|
||||
source: untitledResources[index],
|
||||
target: uri,
|
||||
success: !!uri
|
||||
});
|
||||
});
|
||||
|
||||
untitledSaveAsPromises.push(untitledSaveAsPromise);
|
||||
});
|
||||
|
||||
return TPromise.join(untitledSaveAsPromises).then(() => {
|
||||
return result;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private doSaveAllFiles(resources?: URI[], options: ISaveOptions = Object.create(null)): TPromise<ITextFileOperationResult> {
|
||||
const dirtyFileModels = this.getDirtyFileModels(Array.isArray(resources) ? resources : void 0 /* Save All */)
|
||||
.filter(model => {
|
||||
if (model.hasState(ModelState.CONFLICT) && (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE)) {
|
||||
return false; // if model is in save conflict, do not save unless save reason is explicit or not provided at all
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const mapResourceToResult = new ResourceMap<IResult>();
|
||||
dirtyFileModels.forEach(m => {
|
||||
mapResourceToResult.set(m.getResource(), {
|
||||
source: m.getResource()
|
||||
});
|
||||
});
|
||||
|
||||
return TPromise.join(dirtyFileModels.map(model => {
|
||||
return model.save(options).then(() => {
|
||||
if (!model.isDirty()) {
|
||||
mapResourceToResult.get(model.getResource()).success = true;
|
||||
}
|
||||
});
|
||||
})).then(r => {
|
||||
return {
|
||||
results: mapResourceToResult.values()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getFileModels(resources?: URI[]): ITextFileEditorModel[];
|
||||
private getFileModels(resource?: URI): ITextFileEditorModel[];
|
||||
private getFileModels(arg1?: any): ITextFileEditorModel[] {
|
||||
if (Array.isArray(arg1)) {
|
||||
const models: ITextFileEditorModel[] = [];
|
||||
(<URI[]>arg1).forEach(resource => {
|
||||
models.push(...this.getFileModels(resource));
|
||||
});
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
return this._models.getAll(<URI>arg1);
|
||||
}
|
||||
|
||||
private getDirtyFileModels(resources?: URI[]): ITextFileEditorModel[];
|
||||
private getDirtyFileModels(resource?: URI): ITextFileEditorModel[];
|
||||
private getDirtyFileModels(arg1?: any): ITextFileEditorModel[] {
|
||||
return this.getFileModels(arg1).filter(model => model.isDirty());
|
||||
}
|
||||
|
||||
public saveAs(resource: URI, target?: URI): TPromise<URI> {
|
||||
|
||||
// Get to target resource
|
||||
if (!target) {
|
||||
let dialogPath = resource.fsPath;
|
||||
if (resource.scheme === UNTITLED_SCHEMA) {
|
||||
dialogPath = this.suggestFileName(resource);
|
||||
}
|
||||
|
||||
const pathRaw = this.promptForPath(dialogPath);
|
||||
if (pathRaw) {
|
||||
target = URI.file(pathRaw);
|
||||
}
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
return TPromise.as(null); // user canceled
|
||||
}
|
||||
|
||||
// Just save if target is same as models own resource
|
||||
if (resource.toString() === target.toString()) {
|
||||
return this.save(resource).then(() => resource);
|
||||
}
|
||||
|
||||
// Do it
|
||||
return this.doSaveAs(resource, target);
|
||||
}
|
||||
|
||||
private doSaveAs(resource: URI, target?: URI): TPromise<URI> {
|
||||
|
||||
// Retrieve text model from provided resource if any
|
||||
let modelPromise: TPromise<ITextFileEditorModel | UntitledEditorModel> = TPromise.as(null);
|
||||
if (resource.scheme === Schemas.file) {
|
||||
modelPromise = TPromise.as(this._models.get(resource));
|
||||
} else if (resource.scheme === UNTITLED_SCHEMA && this.untitledEditorService.exists(resource)) {
|
||||
modelPromise = this.untitledEditorService.loadOrCreate({ resource });
|
||||
}
|
||||
|
||||
return modelPromise.then<any>(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) {
|
||||
return this.doSaveTextFileAs(model, resource, target);
|
||||
}
|
||||
|
||||
// Otherwise we can only copy
|
||||
return this.fileService.copyFile(resource, target);
|
||||
}).then(() => {
|
||||
|
||||
// Revert the source
|
||||
return this.revert(resource).then(() => {
|
||||
|
||||
// Done: return target
|
||||
return target;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private doSaveTextFileAs(sourceModel: ITextFileEditorModel | UntitledEditorModel, resource: URI, target: URI): TPromise<void> {
|
||||
let targetModelResolver: TPromise<ITextFileEditorModel>;
|
||||
|
||||
// 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 = TPromise.as(targetModel);
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
return targetModelResolver.then(targetModel => {
|
||||
|
||||
// take over encoding and model value from source model
|
||||
targetModel.updatePreferredEncoding(sourceModel.getEncoding());
|
||||
targetModel.textEditorModel.setValue(sourceModel.getValue());
|
||||
|
||||
// save model
|
||||
return targetModel.save();
|
||||
}, error => {
|
||||
|
||||
// binary model: delete the file and run the operation again
|
||||
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_IS_BINARY || (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) {
|
||||
return this.fileService.del(target).then(() => this.doSaveTextFileAs(sourceModel, resource, target));
|
||||
}
|
||||
|
||||
return TPromise.wrapError(error);
|
||||
});
|
||||
}
|
||||
|
||||
private suggestFileName(untitledResource: URI): string {
|
||||
const root = this.historyService.getLastActiveWorkspaceRoot();
|
||||
if (root) {
|
||||
return URI.file(paths.join(root.fsPath, this.untitledEditorService.suggestFileName(untitledResource))).fsPath;
|
||||
}
|
||||
|
||||
return this.untitledEditorService.suggestFileName(untitledResource);
|
||||
}
|
||||
|
||||
public revert(resource: URI, options?: IRevertOptions): TPromise<boolean> {
|
||||
return this.revertAll([resource], options).then(result => result.results.length === 1 && result.results[0].success);
|
||||
}
|
||||
|
||||
public revertAll(resources?: URI[], options?: IRevertOptions): TPromise<ITextFileOperationResult> {
|
||||
|
||||
// Revert files first
|
||||
return this.doRevertAllFiles(resources, options).then(operation => {
|
||||
|
||||
// Revert untitled
|
||||
const reverted = this.untitledEditorService.revertAll(resources);
|
||||
reverted.forEach(res => operation.results.push({ source: res, success: true }));
|
||||
|
||||
return operation;
|
||||
});
|
||||
}
|
||||
|
||||
private doRevertAllFiles(resources?: URI[], options?: IRevertOptions): TPromise<ITextFileOperationResult> {
|
||||
const fileModels = options && options.force ? this.getFileModels(resources) : this.getDirtyFileModels(resources);
|
||||
|
||||
const mapResourceToResult = new ResourceMap<IResult>();
|
||||
fileModels.forEach(m => {
|
||||
mapResourceToResult.set(m.getResource(), {
|
||||
source: m.getResource()
|
||||
});
|
||||
});
|
||||
|
||||
return TPromise.join(fileModels.map(model => {
|
||||
return model.revert(options && options.soft).then(() => {
|
||||
if (!model.isDirty()) {
|
||||
mapResourceToResult.get(model.getResource()).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;
|
||||
}
|
||||
|
||||
// Otherwise bubble up the error
|
||||
else {
|
||||
return TPromise.wrapError(error);
|
||||
}
|
||||
|
||||
return void 0;
|
||||
});
|
||||
})).then(r => {
|
||||
return {
|
||||
results: mapResourceToResult.values()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public getAutoSaveMode(): AutoSaveMode {
|
||||
if (this.configuredAutoSaveOnFocusChange) {
|
||||
return AutoSaveMode.ON_FOCUS_CHANGE;
|
||||
}
|
||||
|
||||
if (this.configuredAutoSaveOnWindowChange) {
|
||||
return AutoSaveMode.ON_WINDOW_CHANGE;
|
||||
}
|
||||
|
||||
if (this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0) {
|
||||
return this.configuredAutoSaveDelay <= 1000 ? AutoSaveMode.AFTER_SHORT_DELAY : AutoSaveMode.AFTER_LONG_DELAY;
|
||||
}
|
||||
|
||||
return AutoSaveMode.OFF;
|
||||
}
|
||||
|
||||
public getAutoSaveConfiguration(): IAutoSaveConfiguration {
|
||||
return {
|
||||
autoSaveDelay: this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0 ? this.configuredAutoSaveDelay : void 0,
|
||||
autoSaveFocusChange: this.configuredAutoSaveOnFocusChange,
|
||||
autoSaveApplicationChange: this.configuredAutoSaveOnWindowChange
|
||||
};
|
||||
}
|
||||
|
||||
public get isHotExitEnabled(): boolean {
|
||||
return !this.environmentService.isExtensionDevelopment && this.configuredHotExit !== HotExitConfiguration.OFF;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.toUnbind = dispose(this.toUnbind);
|
||||
|
||||
// Clear all caches
|
||||
this._models.clear();
|
||||
}
|
||||
}
|
||||
317
src/vs/workbench/services/textfile/common/textfiles.ts
Normal file
317
src/vs/workbench/services/textfile/common/textfiles.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import Event from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IEncodingSupport, ConfirmResult } from 'vs/workbench/common/editor';
|
||||
import { IBaseStat, IResolveContentOptions } from 'vs/platform/files/common/files';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITextEditorModel } from 'vs/editor/common/services/resolverService';
|
||||
import { IRawTextSource } from 'vs/editor/common/model/textSource';
|
||||
|
||||
/**
|
||||
* The save error handler can be installed on the text text file editor model to install code that executes when save errors occur.
|
||||
*/
|
||||
export interface ISaveErrorHandler {
|
||||
|
||||
/**
|
||||
* Called whenever a save fails.
|
||||
*/
|
||||
onSaveError(error: Error, model: ITextFileEditorModel): void;
|
||||
}
|
||||
|
||||
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 }): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* States the text text file editor model can be in.
|
||||
*/
|
||||
export enum ModelState {
|
||||
SAVED,
|
||||
DIRTY,
|
||||
PENDING_SAVE,
|
||||
|
||||
/**
|
||||
* A model is in conflict mode when changes cannot be saved because the
|
||||
* underlying file has changed. Models in conflict mode are always dirty.
|
||||
*/
|
||||
CONFLICT,
|
||||
|
||||
/**
|
||||
* A model is in orphan state when the underlying file has been deleted.
|
||||
*/
|
||||
ORPHAN,
|
||||
|
||||
/**
|
||||
* Any error that happens during a save that is not causing the CONFLICT state.
|
||||
* Models in error mode are always diry.
|
||||
*/
|
||||
ERROR
|
||||
}
|
||||
|
||||
export enum StateChange {
|
||||
DIRTY,
|
||||
SAVING,
|
||||
SAVE_ERROR,
|
||||
SAVED,
|
||||
REVERTED,
|
||||
ENCODING,
|
||||
CONTENT_CHANGE,
|
||||
ORPHANED_CHANGE
|
||||
}
|
||||
|
||||
export class TextFileModelChangeEvent {
|
||||
private _resource: URI;
|
||||
private _kind: StateChange;
|
||||
|
||||
constructor(model: ITextFileEditorModel, kind: StateChange) {
|
||||
this._resource = model.getResource();
|
||||
this._kind = kind;
|
||||
}
|
||||
|
||||
public get resource(): URI {
|
||||
return this._resource;
|
||||
}
|
||||
|
||||
public get kind(): StateChange {
|
||||
return this._kind;
|
||||
}
|
||||
}
|
||||
|
||||
export const TEXT_FILE_SERVICE_ID = 'textFileService';
|
||||
|
||||
export interface ITextFileOperationResult {
|
||||
results: IResult[];
|
||||
}
|
||||
|
||||
export interface IResult {
|
||||
source: URI;
|
||||
target?: URI;
|
||||
success?: boolean;
|
||||
}
|
||||
|
||||
export interface IAutoSaveConfiguration {
|
||||
autoSaveDelay: number;
|
||||
autoSaveFocusChange: boolean;
|
||||
autoSaveApplicationChange: boolean;
|
||||
}
|
||||
|
||||
export enum AutoSaveMode {
|
||||
OFF,
|
||||
AFTER_SHORT_DELAY,
|
||||
AFTER_LONG_DELAY,
|
||||
ON_FOCUS_CHANGE,
|
||||
ON_WINDOW_CHANGE
|
||||
}
|
||||
|
||||
export enum SaveReason {
|
||||
EXPLICIT = 1,
|
||||
AUTO = 2,
|
||||
FOCUS_CHANGE = 3,
|
||||
WINDOW_CHANGE = 4
|
||||
}
|
||||
|
||||
export const ITextFileService = createDecorator<ITextFileService>(TEXT_FILE_SERVICE_ID);
|
||||
|
||||
export interface IRawTextContent extends IBaseStat {
|
||||
|
||||
/**
|
||||
* The line grouped content of a text file.
|
||||
*/
|
||||
value: IRawTextSource;
|
||||
|
||||
/**
|
||||
* The line grouped logical hash of a text file.
|
||||
*/
|
||||
valueLogicalHash: string;
|
||||
|
||||
/**
|
||||
* The encoding of the content if known.
|
||||
*/
|
||||
encoding: string;
|
||||
}
|
||||
|
||||
export interface IModelLoadOrCreateOptions {
|
||||
encoding?: string;
|
||||
reload?: boolean;
|
||||
}
|
||||
|
||||
export interface ITextFileEditorModelManager {
|
||||
|
||||
onModelDisposed: Event<URI>;
|
||||
onModelContentChanged: Event<TextFileModelChangeEvent>;
|
||||
onModelEncodingChanged: Event<TextFileModelChangeEvent>;
|
||||
|
||||
onModelDirty: Event<TextFileModelChangeEvent>;
|
||||
onModelSaveError: Event<TextFileModelChangeEvent>;
|
||||
onModelSaved: Event<TextFileModelChangeEvent>;
|
||||
onModelReverted: Event<TextFileModelChangeEvent>;
|
||||
onModelOrphanedChanged: Event<TextFileModelChangeEvent>;
|
||||
|
||||
onModelsDirty: Event<TextFileModelChangeEvent[]>;
|
||||
onModelsSaveError: Event<TextFileModelChangeEvent[]>;
|
||||
onModelsSaved: Event<TextFileModelChangeEvent[]>;
|
||||
onModelsReverted: Event<TextFileModelChangeEvent[]>;
|
||||
|
||||
get(resource: URI): ITextFileEditorModel;
|
||||
|
||||
getAll(resource?: URI): ITextFileEditorModel[];
|
||||
|
||||
loadOrCreate(resource: URI, options?: IModelLoadOrCreateOptions): TPromise<ITextFileEditorModel>;
|
||||
|
||||
disposeModel(model: ITextFileEditorModel): void;
|
||||
}
|
||||
|
||||
export interface ISaveOptions {
|
||||
force?: boolean;
|
||||
reason?: SaveReason;
|
||||
overwriteReadonly?: boolean;
|
||||
overwriteEncoding?: boolean;
|
||||
skipSaveParticipants?: boolean;
|
||||
}
|
||||
|
||||
export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport {
|
||||
|
||||
onDidContentChange: Event<StateChange>;
|
||||
onDidStateChange: Event<StateChange>;
|
||||
|
||||
getVersionId(): number;
|
||||
|
||||
getResource(): URI;
|
||||
|
||||
hasState(state: ModelState): boolean;
|
||||
|
||||
getETag(): string;
|
||||
|
||||
updatePreferredEncoding(encoding: string): void;
|
||||
|
||||
save(options?: ISaveOptions): TPromise<void>;
|
||||
|
||||
load(): TPromise<ITextFileEditorModel>;
|
||||
|
||||
revert(soft?: boolean): TPromise<void>;
|
||||
|
||||
getValue(): string;
|
||||
|
||||
isDirty(): boolean;
|
||||
|
||||
isResolved(): boolean;
|
||||
|
||||
isDisposed(): boolean;
|
||||
}
|
||||
|
||||
export interface IRevertOptions {
|
||||
|
||||
/**
|
||||
* Forces to load the contents from disk again even if the file is not dirty.
|
||||
*/
|
||||
force?: boolean;
|
||||
|
||||
/**
|
||||
* A soft revert will clear dirty state of a file but not attempt to load the contents from disk.
|
||||
*/
|
||||
soft?: boolean;
|
||||
}
|
||||
|
||||
export interface ITextFileService extends IDisposable {
|
||||
_serviceBrand: any;
|
||||
onAutoSaveConfigurationChange: Event<IAutoSaveConfiguration>;
|
||||
onFilesAssociationChange: Event<void>;
|
||||
|
||||
/**
|
||||
* Access to the manager of text file editor models providing further methods to work with them.
|
||||
*/
|
||||
models: ITextFileEditorModelManager;
|
||||
|
||||
/**
|
||||
* Resolve the contents of a file identified by the resource.
|
||||
*/
|
||||
resolveTextContent(resource: URI, options?: IResolveContentOptions): TPromise<IRawTextContent>;
|
||||
|
||||
/**
|
||||
* A resource is dirty if it has unsaved changes or is an untitled file not yet saved.
|
||||
*
|
||||
* @param resource the resource to check for being dirty. If it is not specified, will check for
|
||||
* all dirty resources.
|
||||
*/
|
||||
isDirty(resource?: URI): boolean;
|
||||
|
||||
/**
|
||||
* Returns all resources that are currently dirty matching the provided resources or all dirty resources.
|
||||
*
|
||||
* @param resources the resources to check for being dirty. If it is not specified, will check for
|
||||
* all dirty resources.
|
||||
*/
|
||||
getDirty(resources?: URI[]): URI[];
|
||||
|
||||
/**
|
||||
* Saves the resource.
|
||||
*
|
||||
* @param resource the resource to save
|
||||
* @return true if the resource was saved.
|
||||
*/
|
||||
save(resource: URI, options?: ISaveOptions): TPromise<boolean>;
|
||||
|
||||
/**
|
||||
* Saves the provided resource asking the user for a file name.
|
||||
*
|
||||
* @param resource the resource to save as.
|
||||
* @return true if the file was saved.
|
||||
*/
|
||||
saveAs(resource: URI, targetResource?: URI): TPromise<URI>;
|
||||
|
||||
/**
|
||||
* Saves the set of resources and returns a promise with the operation result.
|
||||
*
|
||||
* @param resources can be null to save all.
|
||||
* @param includeUntitled to save all resources and optionally exclude untitled ones.
|
||||
*/
|
||||
saveAll(includeUntitled?: boolean, options?: ISaveOptions): TPromise<ITextFileOperationResult>;
|
||||
saveAll(resources: URI[], options?: ISaveOptions): TPromise<ITextFileOperationResult>;
|
||||
|
||||
/**
|
||||
* Reverts the provided resource.
|
||||
*
|
||||
* @param resource the resource of the file to revert.
|
||||
* @param force to force revert even when the file is not dirty
|
||||
*/
|
||||
revert(resource: URI, options?: IRevertOptions): TPromise<boolean>;
|
||||
|
||||
/**
|
||||
* Reverts all the provided resources and returns a promise with the operation result.
|
||||
*/
|
||||
revertAll(resources?: URI[], options?: IRevertOptions): TPromise<ITextFileOperationResult>;
|
||||
|
||||
/**
|
||||
* Brings up the confirm dialog to either save, don't save or cancel.
|
||||
*
|
||||
* @param resources the resources of the files to ask for confirmation or null if
|
||||
* confirming for all dirty resources.
|
||||
*/
|
||||
confirmSave(resources?: URI[]): ConfirmResult;
|
||||
|
||||
/**
|
||||
* Convinient fast access to the current auto save mode.
|
||||
*/
|
||||
getAutoSaveMode(): AutoSaveMode;
|
||||
|
||||
/**
|
||||
* Convinient fast access to the raw configured auto save settings.
|
||||
*/
|
||||
getAutoSaveConfiguration(): IAutoSaveConfiguration;
|
||||
|
||||
/**
|
||||
* Convinient fast access to the hot exit file setting.
|
||||
*/
|
||||
isHotExitEnabled: boolean;
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { IStringStream } from 'vs/platform/files/common/files';
|
||||
import * as crypto from 'crypto';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
import { IRawTextSource } from 'vs/editor/common/model/textSource';
|
||||
|
||||
const AVOID_SLICED_STRINGS = true;
|
||||
|
||||
export interface ModelBuilderResult {
|
||||
readonly hash: string;
|
||||
readonly value: IRawTextSource;
|
||||
}
|
||||
|
||||
const PREALLOC_BUFFER_CHARS = 1000;
|
||||
|
||||
const emptyString = '';
|
||||
const asciiStrings: string[] = [];
|
||||
for (let i = 0; i < 128; i++) {
|
||||
asciiStrings[i] = String.fromCharCode(i);
|
||||
}
|
||||
|
||||
function optimizeStringMemory(buff: Buffer, s: string): string {
|
||||
const len = s.length;
|
||||
|
||||
if (len === 0) {
|
||||
return emptyString;
|
||||
}
|
||||
|
||||
if (len === 1) {
|
||||
const charCode = s.charCodeAt(0);
|
||||
if (charCode < 128) {
|
||||
return asciiStrings[charCode];
|
||||
}
|
||||
}
|
||||
|
||||
if (AVOID_SLICED_STRINGS) {
|
||||
// See https://bugs.chromium.org/p/v8/issues/detail?id=2869
|
||||
// See https://github.com/nodejs/help/issues/711
|
||||
|
||||
if (len < PREALLOC_BUFFER_CHARS) {
|
||||
// Use the same buffer instance that we have allocated and that can fit `PREALLOC_BUFFER_CHARS` characters
|
||||
const byteLen = buff.write(s, 0);
|
||||
return buff.toString(undefined, 0, byteLen);
|
||||
}
|
||||
|
||||
return Buffer.from(s).toString();
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
class ModelLineBasedBuilder {
|
||||
|
||||
private computeHash: boolean;
|
||||
private hash: crypto.Hash;
|
||||
private buff: Buffer;
|
||||
private BOM: string;
|
||||
private lines: string[];
|
||||
private currLineIndex: number;
|
||||
|
||||
constructor(computeHash: boolean) {
|
||||
this.computeHash = computeHash;
|
||||
if (this.computeHash) {
|
||||
this.hash = crypto.createHash('sha1');
|
||||
}
|
||||
this.BOM = '';
|
||||
this.lines = [];
|
||||
this.currLineIndex = 0;
|
||||
this.buff = Buffer.alloc(3/*any UTF16 code unit could expand to up to 3 UTF8 code units*/ * PREALLOC_BUFFER_CHARS);
|
||||
}
|
||||
|
||||
public acceptLines(lines: string[]): void {
|
||||
if (this.currLineIndex === 0) {
|
||||
// Remove the BOM (if present)
|
||||
if (strings.startsWithUTF8BOM(lines[0])) {
|
||||
this.BOM = strings.UTF8_BOM_CHARACTER;
|
||||
lines[0] = lines[0].substr(1);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0, len = lines.length; i < len; i++) {
|
||||
this.lines[this.currLineIndex++] = optimizeStringMemory(this.buff, lines[i]);
|
||||
}
|
||||
if (this.computeHash) {
|
||||
this.hash.update(lines.join('\n') + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
public finish(length: number, carriageReturnCnt: number, containsRTL: boolean, isBasicASCII: boolean): ModelBuilderResult {
|
||||
return {
|
||||
hash: this.computeHash ? this.hash.digest('hex') : null,
|
||||
value: {
|
||||
BOM: this.BOM,
|
||||
lines: this.lines,
|
||||
length,
|
||||
containsRTL: containsRTL,
|
||||
totalCRCount: carriageReturnCnt,
|
||||
isBasicASCII,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function computeHash(rawText: IRawTextSource): string {
|
||||
let hash = crypto.createHash('sha1');
|
||||
for (let i = 0, len = rawText.lines.length; i < len; i++) {
|
||||
hash.update(rawText.lines[i] + '\n');
|
||||
}
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
export class ModelBuilder {
|
||||
|
||||
private leftoverPrevChunk: string;
|
||||
private leftoverEndsInCR: boolean;
|
||||
private totalCRCount: number;
|
||||
private lineBasedBuilder: ModelLineBasedBuilder;
|
||||
private totalLength: number;
|
||||
private containsRTL: boolean;
|
||||
private isBasicASCII: boolean;
|
||||
|
||||
public static fromStringStream(stream: IStringStream): TPromise<ModelBuilderResult> {
|
||||
return new TPromise<ModelBuilderResult>((c, e, p) => {
|
||||
let done = false;
|
||||
let builder = new ModelBuilder(false);
|
||||
|
||||
stream.on('data', (chunk) => {
|
||||
builder.acceptChunk(chunk);
|
||||
});
|
||||
|
||||
stream.on('error', (error) => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
e(error);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
c(builder.finish());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
constructor(computeHash: boolean) {
|
||||
this.leftoverPrevChunk = '';
|
||||
this.leftoverEndsInCR = false;
|
||||
this.totalCRCount = 0;
|
||||
this.lineBasedBuilder = new ModelLineBasedBuilder(computeHash);
|
||||
this.totalLength = 0;
|
||||
this.containsRTL = false;
|
||||
this.isBasicASCII = true;
|
||||
}
|
||||
|
||||
private _updateCRCount(chunk: string): void {
|
||||
// Count how many \r are present in chunk to determine the majority EOL sequence
|
||||
let chunkCarriageReturnCnt = 0;
|
||||
let lastCarriageReturnIndex = -1;
|
||||
while ((lastCarriageReturnIndex = chunk.indexOf('\r', lastCarriageReturnIndex + 1)) !== -1) {
|
||||
chunkCarriageReturnCnt++;
|
||||
}
|
||||
this.totalCRCount += chunkCarriageReturnCnt;
|
||||
}
|
||||
|
||||
public acceptChunk(chunk: string): void {
|
||||
if (chunk.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.totalLength += chunk.length;
|
||||
|
||||
this._updateCRCount(chunk);
|
||||
|
||||
if (!this.containsRTL) {
|
||||
this.containsRTL = strings.containsRTL(chunk);
|
||||
}
|
||||
if (this.isBasicASCII) {
|
||||
this.isBasicASCII = strings.isBasicASCII(chunk);
|
||||
}
|
||||
|
||||
// Avoid dealing with a chunk that ends in \r (push the \r to the next chunk)
|
||||
if (this.leftoverEndsInCR) {
|
||||
chunk = '\r' + chunk;
|
||||
}
|
||||
if (chunk.charCodeAt(chunk.length - 1) === CharCode.CarriageReturn) {
|
||||
this.leftoverEndsInCR = true;
|
||||
chunk = chunk.substr(0, chunk.length - 1);
|
||||
} else {
|
||||
this.leftoverEndsInCR = false;
|
||||
}
|
||||
|
||||
let lines = chunk.split(/\r\n|\r|\n/);
|
||||
|
||||
if (lines.length === 1) {
|
||||
// no \r or \n encountered
|
||||
this.leftoverPrevChunk += lines[0];
|
||||
return;
|
||||
}
|
||||
|
||||
lines[0] = this.leftoverPrevChunk + lines[0];
|
||||
this.lineBasedBuilder.acceptLines(lines.slice(0, lines.length - 1));
|
||||
this.leftoverPrevChunk = lines[lines.length - 1];
|
||||
}
|
||||
|
||||
public finish(): ModelBuilderResult {
|
||||
let finalLines = [this.leftoverPrevChunk];
|
||||
if (this.leftoverEndsInCR) {
|
||||
finalLines.push('');
|
||||
}
|
||||
this.lineBasedBuilder.acceptLines(finalLines);
|
||||
return this.lineBasedBuilder.finish(this.totalLength, this.totalCRCount, this.containsRTL, this.isBasicASCII);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import nls = require('vs/nls');
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import paths = require('vs/base/common/paths');
|
||||
import strings = require('vs/base/common/strings');
|
||||
import { isWindows, isLinux } 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 { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { ModelBuilder } from 'vs/workbench/services/textfile/electron-browser/modelBuilder';
|
||||
import product from 'vs/platform/node/product';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IMessageService } from 'vs/platform/message/common/message';
|
||||
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 { mnemonicButtonLabel } from 'vs/base/common/labels';
|
||||
|
||||
export class TextFileService extends AbstractTextFileService {
|
||||
|
||||
private static MAX_CONFIRM_FILES = 10;
|
||||
|
||||
constructor(
|
||||
@IWorkspaceContextService contextService: IWorkspaceContextService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
|
||||
@ILifecycleService lifecycleService: ILifecycleService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IModeService private modeService: IModeService,
|
||||
@IWindowService private windowService: IWindowService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IMessageService messageService: IMessageService,
|
||||
@IBackupFileService backupFileService: IBackupFileService,
|
||||
@IWindowsService windowsService: IWindowsService,
|
||||
@IHistoryService historyService: IHistoryService
|
||||
) {
|
||||
super(lifecycleService, contextService, configurationService, telemetryService, fileService, untitledEditorService, instantiationService, messageService, environmentService, backupFileService, windowsService, historyService);
|
||||
}
|
||||
|
||||
public resolveTextContent(resource: URI, options?: IResolveContentOptions): TPromise<IRawTextContent> {
|
||||
return this.fileService.resolveStreamContent(resource, options).then(streamContent => {
|
||||
return ModelBuilder.fromStringStream(streamContent.value).then(res => {
|
||||
const r: IRawTextContent = {
|
||||
resource: streamContent.resource,
|
||||
name: streamContent.name,
|
||||
mtime: streamContent.mtime,
|
||||
etag: streamContent.etag,
|
||||
encoding: streamContent.encoding,
|
||||
value: res.value,
|
||||
valueLogicalHash: res.hash
|
||||
};
|
||||
return r;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public confirmSave(resources?: URI[]): 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)
|
||||
}
|
||||
|
||||
const resourcesToConfirm = this.getDirty(resources);
|
||||
if (resourcesToConfirm.length === 0) {
|
||||
return 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)) : nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", resourcesToConfirm.length)
|
||||
];
|
||||
|
||||
if (resourcesToConfirm.length > 1) {
|
||||
message.push('');
|
||||
message.push(...resourcesToConfirm.slice(0, TextFileService.MAX_CONFIRM_FILES).map(r => paths.basename(r.fsPath)));
|
||||
|
||||
if (resourcesToConfirm.length > TextFileService.MAX_CONFIRM_FILES) {
|
||||
if (resourcesToConfirm.length - TextFileService.MAX_CONFIRM_FILES === 1) {
|
||||
message.push(nls.localize('moreFile', "...1 additional file not shown"));
|
||||
} else {
|
||||
message.push(nls.localize('moreFiles', "...{0} additional files not shown", resourcesToConfirm.length - TextFileService.MAX_CONFIRM_FILES));
|
||||
}
|
||||
}
|
||||
|
||||
message.push('');
|
||||
}
|
||||
|
||||
// Button order
|
||||
// Windows: Save | Don't Save | Cancel
|
||||
// Mac: Save | Cancel | Don't Save
|
||||
// Linux: Don't Save | Cancel | Save
|
||||
|
||||
const save = { label: resourcesToConfirm.length > 1 ? mnemonicButtonLabel(nls.localize({ key: 'saveAll', comment: ['&& denotes a mnemonic'] }, "&&Save All")) : mnemonicButtonLabel(nls.localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save")), result: ConfirmResult.SAVE };
|
||||
const dontSave = { label: mnemonicButtonLabel(nls.localize({ key: 'dontSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save")), result: ConfirmResult.DONT_SAVE };
|
||||
const cancel = { label: nls.localize('cancel', "Cancel"), result: ConfirmResult.CANCEL };
|
||||
|
||||
const buttons: { label: string; result: ConfirmResult; }[] = [];
|
||||
if (isWindows) {
|
||||
buttons.push(save, dontSave, cancel);
|
||||
} else if (isLinux) {
|
||||
buttons.push(dontSave, cancel, save);
|
||||
} else {
|
||||
buttons.push(save, cancel, dontSave);
|
||||
}
|
||||
|
||||
const opts: Electron.ShowMessageBoxOptions = {
|
||||
title: product.nameLong,
|
||||
message: message.join('\n'),
|
||||
type: 'warning',
|
||||
detail: nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them."),
|
||||
buttons: buttons.map(b => b.label),
|
||||
noLink: true,
|
||||
cancelId: buttons.indexOf(cancel)
|
||||
};
|
||||
|
||||
if (isLinux) {
|
||||
opts.defaultId = 2;
|
||||
}
|
||||
|
||||
const choice = this.windowService.showMessageBox(opts);
|
||||
|
||||
return buttons[choice].result;
|
||||
}
|
||||
|
||||
public promptForPath(defaultPath?: string): string {
|
||||
return this.windowService.showSaveDialog(this.getSaveDialogOptions(defaultPath ? paths.normalize(defaultPath, true) : void 0));
|
||||
}
|
||||
|
||||
private getSaveDialogOptions(defaultPath?: string): Electron.SaveDialogOptions {
|
||||
const options: Electron.SaveDialogOptions = {
|
||||
defaultPath: defaultPath
|
||||
};
|
||||
|
||||
// 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 = paths.extname(defaultPath);
|
||||
let matchingFilter: IFilter;
|
||||
const filters: IFilter[] = 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;
|
||||
}).filter(f => !!f);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
152
src/vs/workbench/services/textfile/test/modelBuilder.test.ts
Normal file
152
src/vs/workbench/services/textfile/test/modelBuilder.test.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { ModelBuilder, computeHash } from 'vs/workbench/services/textfile/electron-browser/modelBuilder';
|
||||
import { ITextModelCreationOptions } from 'vs/editor/common/editorCommon';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { RawTextSource, IRawTextSource } from 'vs/editor/common/model/textSource';
|
||||
|
||||
export function testModelBuilder(chunks: string[], opts: ITextModelCreationOptions = TextModel.DEFAULT_CREATION_OPTIONS): string {
|
||||
let expectedTextSource = RawTextSource.fromString(chunks.join(''));
|
||||
let expectedHash = computeHash(expectedTextSource);
|
||||
|
||||
let builder = new ModelBuilder(true);
|
||||
for (let i = 0, len = chunks.length; i < len; i++) {
|
||||
builder.acceptChunk(chunks[i]);
|
||||
}
|
||||
let actual = builder.finish();
|
||||
|
||||
let actualTextSource = actual.value;
|
||||
let actualHash = actual.hash;
|
||||
|
||||
assert.equal(actualHash, expectedHash);
|
||||
assert.deepEqual(actualTextSource, expectedTextSource);
|
||||
|
||||
return expectedHash;
|
||||
}
|
||||
|
||||
function toTextSource(lines: string[]): IRawTextSource {
|
||||
return {
|
||||
BOM: '',
|
||||
lines: lines,
|
||||
totalCRCount: 0,
|
||||
length: 0,
|
||||
containsRTL: false,
|
||||
isBasicASCII: true
|
||||
};
|
||||
}
|
||||
|
||||
export function testDifferentHash(lines1: string[], lines2: string[]): void {
|
||||
let hash1 = computeHash(toTextSource(lines1));
|
||||
let hash2 = computeHash(toTextSource(lines2));
|
||||
assert.notEqual(hash1, hash2);
|
||||
}
|
||||
|
||||
suite('ModelBuilder', () => {
|
||||
|
||||
test('uses sha1', () => {
|
||||
// These are the sha1s of the string + \n
|
||||
assert.equal(computeHash(toTextSource([''])), 'adc83b19e793491b1c6ea0fd8b46cd9f32e592fc');
|
||||
assert.equal(computeHash(toTextSource(['hello world'])), '22596363b3de40b06f981fb85d82312e8c0ed511');
|
||||
});
|
||||
|
||||
test('no chunks', () => {
|
||||
testModelBuilder([]);
|
||||
});
|
||||
|
||||
test('single empty chunk', () => {
|
||||
testModelBuilder(['']);
|
||||
});
|
||||
|
||||
test('single line in one chunk', () => {
|
||||
testModelBuilder(['Hello world']);
|
||||
});
|
||||
|
||||
test('single line in multiple chunks', () => {
|
||||
testModelBuilder(['Hello', ' ', 'world']);
|
||||
});
|
||||
|
||||
test('two lines in single chunk', () => {
|
||||
testModelBuilder(['Hello world\nHow are you?']);
|
||||
});
|
||||
|
||||
test('two lines in multiple chunks 1', () => {
|
||||
testModelBuilder(['Hello worl', 'd\nHow are you?']);
|
||||
});
|
||||
|
||||
test('two lines in multiple chunks 2', () => {
|
||||
testModelBuilder(['Hello worl', 'd', '\n', 'H', 'ow are you?']);
|
||||
});
|
||||
|
||||
test('two lines in multiple chunks 3', () => {
|
||||
testModelBuilder(['Hello worl', 'd', '\nHow are you?']);
|
||||
});
|
||||
|
||||
test('multiple lines in single chunks', () => {
|
||||
testModelBuilder(['Hello world\nHow are you?\nIs everything good today?\nDo you enjoy the weather?']);
|
||||
});
|
||||
|
||||
test('multiple lines in multiple chunks 1', () => {
|
||||
testModelBuilder(['Hello world\nHow are you', '?\nIs everything good today?\nDo you enjoy the weather?']);
|
||||
});
|
||||
|
||||
test('multiple lines in multiple chunks 1', () => {
|
||||
testModelBuilder(['Hello world', '\nHow are you', '?\nIs everything good today?', '\nDo you enjoy the weather?']);
|
||||
});
|
||||
|
||||
test('multiple lines in multiple chunks 1', () => {
|
||||
testModelBuilder(['Hello world\n', 'How are you', '?\nIs everything good today?', '\nDo you enjoy the weather?']);
|
||||
});
|
||||
|
||||
test('carriage return detection (1 \\r\\n 2 \\n)', () => {
|
||||
testModelBuilder(['Hello world\r\n', 'How are you', '?\nIs everything good today?', '\nDo you enjoy the weather?']);
|
||||
});
|
||||
|
||||
test('carriage return detection (2 \\r\\n 1 \\n)', () => {
|
||||
testModelBuilder(['Hello world\r\n', 'How are you', '?\r\nIs everything good today?', '\nDo you enjoy the weather?']);
|
||||
});
|
||||
|
||||
test('carriage return detection (3 \\r\\n 0 \\n)', () => {
|
||||
testModelBuilder(['Hello world\r\n', 'How are you', '?\r\nIs everything good today?', '\r\nDo you enjoy the weather?']);
|
||||
});
|
||||
|
||||
test('carriage return detection (isolated \\r)', () => {
|
||||
testModelBuilder(['Hello world', '\r', '\n', 'How are you', '?', '\r', '\n', 'Is everything good today?', '\r', '\n', 'Do you enjoy the weather?']);
|
||||
});
|
||||
|
||||
test('BOM handling', () => {
|
||||
testModelBuilder([strings.UTF8_BOM_CHARACTER + 'Hello world!']);
|
||||
});
|
||||
|
||||
test('BOM handling', () => {
|
||||
testModelBuilder([strings.UTF8_BOM_CHARACTER, 'Hello world!']);
|
||||
});
|
||||
|
||||
test('RTL handling 1', () => {
|
||||
testModelBuilder(['Hello world!', 'זוהי עובדה מבוססת שדעתו']);
|
||||
});
|
||||
|
||||
test('RTL handling 2', () => {
|
||||
testModelBuilder(['Hello world!זוהי עובדה מבוססת שדעתו']);
|
||||
});
|
||||
|
||||
test('RTL handling 3', () => {
|
||||
testModelBuilder(['Hello world!זוהי \nעובדה מבוססת שדעתו']);
|
||||
});
|
||||
|
||||
test('ASCII handling 1', () => {
|
||||
testModelBuilder(['Hello world!!\nHow do you do?']);
|
||||
});
|
||||
test('ASCII handling 1', () => {
|
||||
testModelBuilder(['Hello world!!\nHow do you do?Züricha📚📚b']);
|
||||
});
|
||||
|
||||
test('issue #32819: some special string cannot be displayed completely', () => {
|
||||
testModelBuilder(['AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA123']);
|
||||
});
|
||||
});
|
||||
138
src/vs/workbench/services/textfile/test/modelBuilderAuto.test.ts
Normal file
138
src/vs/workbench/services/textfile/test/modelBuilderAuto.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { testModelBuilder, testDifferentHash } from './modelBuilder.test';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
|
||||
const GENERATE_TESTS = false;
|
||||
|
||||
suite('ModelBuilder Auto Tests', () => {
|
||||
|
||||
test('auto1', () => {
|
||||
testModelBuilder(['sarjniow', '\r', '\nbpb', 'ofb', '\njzldgxx', '\r\nkzwfjysng']);
|
||||
});
|
||||
|
||||
test('auto2', () => {
|
||||
testModelBuilder(['i', 'yyernubi\r\niimgn\n', 'ut\r']);
|
||||
});
|
||||
|
||||
test('auto3', () => {
|
||||
testDifferentHash([''], ['', '', '']);
|
||||
});
|
||||
});
|
||||
|
||||
function getRandomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function getRandomEOLSequence(): string {
|
||||
let rnd = getRandomInt(1, 3);
|
||||
if (rnd === 1) {
|
||||
return '\n';
|
||||
}
|
||||
if (rnd === 2) {
|
||||
return '\r';
|
||||
}
|
||||
return '\r\n';
|
||||
}
|
||||
|
||||
function getRandomString(minLength: number, maxLength: number): string {
|
||||
let length = getRandomInt(minLength, maxLength);
|
||||
let r = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
r += String.fromCharCode(getRandomInt(CharCode.a, CharCode.z));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
function generateRandomFile(): string {
|
||||
let lineCount = getRandomInt(1, 10);
|
||||
let mixedEOLSequence = getRandomInt(1, 2) === 1 ? true : false;
|
||||
let fixedEOL = getRandomEOLSequence();
|
||||
let lines: string[] = [];
|
||||
for (let i = 0; i < lineCount; i++) {
|
||||
if (i !== 0) {
|
||||
if (mixedEOLSequence) {
|
||||
lines.push(getRandomEOLSequence());
|
||||
} else {
|
||||
lines.push(fixedEOL);
|
||||
}
|
||||
}
|
||||
lines.push(getRandomString(0, 10));
|
||||
|
||||
}
|
||||
return lines.join('');
|
||||
}
|
||||
|
||||
function generateRandomChunks(file: string): string[] {
|
||||
let result: string[] = [];
|
||||
let cnt = getRandomInt(1, 20);
|
||||
|
||||
let maxOffset = file.length;
|
||||
|
||||
while (cnt > 0 && maxOffset > 0) {
|
||||
|
||||
let offset = getRandomInt(0, maxOffset);
|
||||
result.unshift(file.substring(offset, maxOffset));
|
||||
// let length = getRandomInt(0, maxOffset - offset);
|
||||
// let text = generateFile(true);
|
||||
|
||||
// result.push({
|
||||
// offset: offset,
|
||||
// length: length,
|
||||
// text: text
|
||||
// });
|
||||
|
||||
maxOffset = offset;
|
||||
cnt--;
|
||||
}
|
||||
if (maxOffset !== 0) {
|
||||
result.unshift(file.substring(0, maxOffset));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
let HASH_TO_CONTENT: { [hash: string]: string; } = {};
|
||||
|
||||
function testRandomFile(file: string): boolean {
|
||||
let tests = getRandomInt(5, 10);
|
||||
for (let i = 0; i < tests; i++) {
|
||||
let chunks = generateRandomChunks(file);
|
||||
try {
|
||||
let hash = testModelBuilder(chunks);
|
||||
let logicalContent = JSON.stringify(file.split(/\r\n|\r|\n/));
|
||||
if (HASH_TO_CONTENT.hasOwnProperty(hash)) {
|
||||
let prevLogicalContent = HASH_TO_CONTENT[hash];
|
||||
if (prevLogicalContent !== logicalContent) {
|
||||
console.log('HASH COLLISION: ');
|
||||
console.log(prevLogicalContent);
|
||||
console.log(logicalContent);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
HASH_TO_CONTENT[hash] = logicalContent;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.log(JSON.stringify(chunks));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (GENERATE_TESTS) {
|
||||
let number = 1;
|
||||
while (true) {
|
||||
console.log('------BEGIN NEW TEST: ' + number);
|
||||
|
||||
if (!testRandomFile(generateRandomFile())) {
|
||||
break;
|
||||
}
|
||||
|
||||
console.log('------END NEW TEST: ' + (number++));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { EncodingMode } from 'vs/workbench/common/editor';
|
||||
import { TextFileEditorModel, SaveSequentializer } from 'vs/workbench/services/textfile/common/textFileEditorModel';
|
||||
import { ITextFileService, ModelState, StateChange } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { workbenchInstantiationService, TestTextFileService, createFileInput, TestFileService } from 'vs/workbench/test/workbenchTestServices';
|
||||
import { onError, toResource } from 'vs/base/test/common/utils';
|
||||
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
|
||||
import { FileOperationResult, FileOperationError, IFileService } from 'vs/platform/files/common/files';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
|
||||
class ServiceAccessor {
|
||||
constructor( @ITextFileService public textFileService: TestTextFileService, @IModelService public modelService: IModelService, @IFileService public fileService: TestFileService) {
|
||||
}
|
||||
}
|
||||
|
||||
function getLastModifiedTime(model: TextFileEditorModel): number {
|
||||
const stat = model.getStat();
|
||||
|
||||
return stat ? stat.mtime : -1;
|
||||
}
|
||||
|
||||
suite('Files - TextFileEditorModel', () => {
|
||||
|
||||
let instantiationService: IInstantiationService;
|
||||
let accessor: ServiceAccessor;
|
||||
let content: string;
|
||||
|
||||
setup(() => {
|
||||
instantiationService = workbenchInstantiationService();
|
||||
accessor = instantiationService.createInstance(ServiceAccessor);
|
||||
content = accessor.fileService.getContent();
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
|
||||
TextFileEditorModel.setSaveParticipant(null); // reset any set participant
|
||||
accessor.fileService.setContent(content);
|
||||
});
|
||||
|
||||
test('Save', function (done) {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.load().done(() => {
|
||||
model.textEditorModel.setValue('bar');
|
||||
assert.ok(getLastModifiedTime(model) <= Date.now());
|
||||
|
||||
return model.save().then(() => {
|
||||
assert.ok(model.getLastSaveAttemptTime() <= Date.now());
|
||||
assert.ok(!model.isDirty());
|
||||
|
||||
model.dispose();
|
||||
assert.ok(!accessor.modelService.getModel(model.getResource()));
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('setEncoding - encode', function () {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.setEncoding('utf8', EncodingMode.Encode); // no-op
|
||||
assert.equal(getLastModifiedTime(model), -1);
|
||||
|
||||
model.setEncoding('utf16', EncodingMode.Encode);
|
||||
|
||||
assert.ok(getLastModifiedTime(model) <= Date.now()); // indicates model was saved due to encoding change
|
||||
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('setEncoding - decode', function () {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.setEncoding('utf16', EncodingMode.Decode);
|
||||
|
||||
assert.ok(model.isResolved()); // model got loaded due to decoding
|
||||
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('disposes when underlying model is destroyed', function (done) {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.load().done(() => {
|
||||
model.textEditorModel.destroy();
|
||||
|
||||
assert.ok(model.isDisposed());
|
||||
|
||||
done();
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('Load does not trigger save', function (done) {
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index.txt'), 'utf8');
|
||||
assert.ok(model.hasState(ModelState.SAVED));
|
||||
|
||||
model.onDidStateChange(e => {
|
||||
assert.ok(e !== StateChange.DIRTY && e !== StateChange.SAVED);
|
||||
});
|
||||
|
||||
model.load().done(() => {
|
||||
assert.ok(model.isResolved());
|
||||
|
||||
model.dispose();
|
||||
|
||||
assert.ok(!accessor.modelService.getModel(model.getResource()));
|
||||
|
||||
done();
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('Load returns dirty model as long as model is dirty', function (done) {
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.load().done(() => {
|
||||
model.textEditorModel.setValue('foo');
|
||||
|
||||
assert.ok(model.isDirty());
|
||||
assert.ok(model.hasState(ModelState.DIRTY));
|
||||
return model.load().then(() => {
|
||||
assert.ok(model.isDirty());
|
||||
|
||||
model.dispose();
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('Revert', function (done) {
|
||||
let eventCounter = 0;
|
||||
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.onDidStateChange(e => {
|
||||
if (e === StateChange.REVERTED) {
|
||||
eventCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
model.load().done(() => {
|
||||
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(eventCounter, 1);
|
||||
|
||||
model.dispose();
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('Revert (soft)', function (done) {
|
||||
let eventCounter = 0;
|
||||
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.onDidStateChange(e => {
|
||||
if (e === StateChange.REVERTED) {
|
||||
eventCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
model.load().done(() => {
|
||||
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(eventCounter, 1);
|
||||
|
||||
model.dispose();
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('Load and undo turns model dirty', function (done) {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
model.load().done(() => {
|
||||
accessor.fileService.setContent('Hello Change');
|
||||
model.load().done(() => {
|
||||
model.textEditorModel.undo();
|
||||
|
||||
assert.ok(model.isDirty());
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('File not modified error is handled gracefully', function (done) {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.load().done(() => {
|
||||
const mtime = getLastModifiedTime(model);
|
||||
accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_MODIFIED_SINCE));
|
||||
|
||||
return model.load().then((model: TextFileEditorModel) => {
|
||||
assert.ok(model);
|
||||
assert.equal(getLastModifiedTime(model), mtime);
|
||||
model.dispose();
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('Load error is handled gracefully if model already exists', function (done) {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.load().done(() => {
|
||||
accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_FOUND));
|
||||
|
||||
return model.load().then((model: TextFileEditorModel) => {
|
||||
assert.ok(model);
|
||||
model.dispose();
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('save() and isDirty() - proper with check for mtimes', function (done) {
|
||||
const input1 = createFileInput(instantiationService, toResource.call(this, '/path/index_async2.txt'));
|
||||
const input2 = createFileInput(instantiationService, toResource.call(this, '/path/index_async.txt'));
|
||||
|
||||
input1.resolve().done((model1: TextFileEditorModel) => {
|
||||
return input2.resolve().then((model2: TextFileEditorModel) => {
|
||||
model1.textEditorModel.setValue('foo');
|
||||
|
||||
const m1Mtime = model1.getStat().mtime;
|
||||
const m2Mtime = model2.getStat().mtime;
|
||||
assert.ok(m1Mtime > 0);
|
||||
assert.ok(m2Mtime > 0);
|
||||
|
||||
assert.ok(accessor.textFileService.isDirty());
|
||||
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');
|
||||
assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt')));
|
||||
|
||||
return TPromise.timeout(10).then(() => {
|
||||
accessor.textFileService.saveAll().then(() => {
|
||||
assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt')));
|
||||
assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt')));
|
||||
assert.ok(model1.getStat().mtime > m1Mtime);
|
||||
assert.ok(model2.getStat().mtime > m2Mtime);
|
||||
assert.ok(model1.getLastSaveAttemptTime() > m1Mtime);
|
||||
assert.ok(model2.getLastSaveAttemptTime() > m2Mtime);
|
||||
|
||||
model1.dispose();
|
||||
model2.dispose();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('Save Participant', function (done) {
|
||||
let eventCounter = 0;
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.onDidStateChange(e => {
|
||||
if (e === StateChange.SAVED) {
|
||||
assert.equal(model.getValue(), 'bar');
|
||||
assert.ok(!model.isDirty());
|
||||
eventCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
TextFileEditorModel.setSaveParticipant({
|
||||
participate: (model) => {
|
||||
assert.ok(model.isDirty());
|
||||
model.textEditorModel.setValue('bar');
|
||||
assert.ok(model.isDirty());
|
||||
eventCounter++;
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
model.load().done(() => {
|
||||
model.textEditorModel.setValue('foo');
|
||||
|
||||
return model.save().then(() => {
|
||||
model.dispose();
|
||||
|
||||
assert.equal(eventCounter, 2);
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('Save Participant, async participant', function (done) {
|
||||
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
TextFileEditorModel.setSaveParticipant({
|
||||
participate: (model) => {
|
||||
return TPromise.timeout(10);
|
||||
}
|
||||
});
|
||||
|
||||
return model.load().done(() => {
|
||||
model.textEditorModel.setValue('foo');
|
||||
const now = Date.now();
|
||||
return model.save().then(() => {
|
||||
assert.ok(Date.now() - now >= 10);
|
||||
model.dispose();
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('Save Participant, bad participant', function (done) {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
TextFileEditorModel.setSaveParticipant({
|
||||
participate: (model) => {
|
||||
return TPromise.wrapError(new Error('boom'));
|
||||
}
|
||||
});
|
||||
|
||||
return model.load().then(() => {
|
||||
model.textEditorModel.setValue('foo');
|
||||
return model.save().then(() => {
|
||||
assert.ok(true);
|
||||
model.dispose();
|
||||
|
||||
done();
|
||||
}, err => {
|
||||
assert.ok(false);
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('SaveSequentializer - pending basics', function (done) {
|
||||
const sequentializer = new SaveSequentializer();
|
||||
|
||||
assert.ok(!sequentializer.hasPendingSave());
|
||||
assert.ok(!sequentializer.hasPendingSave(2323));
|
||||
assert.ok(!sequentializer.pendingSave);
|
||||
|
||||
// pending removes itself after done
|
||||
sequentializer.setPending(1, TPromise.as(null));
|
||||
assert.ok(!sequentializer.hasPendingSave());
|
||||
assert.ok(!sequentializer.hasPendingSave(1));
|
||||
assert.ok(!sequentializer.pendingSave);
|
||||
|
||||
// pending removes itself after done (use timeout)
|
||||
sequentializer.setPending(2, TPromise.timeout(1));
|
||||
assert.ok(sequentializer.hasPendingSave());
|
||||
assert.ok(sequentializer.hasPendingSave(2));
|
||||
assert.ok(!sequentializer.hasPendingSave(1));
|
||||
assert.ok(sequentializer.pendingSave);
|
||||
|
||||
return TPromise.timeout(2).then(() => {
|
||||
assert.ok(!sequentializer.hasPendingSave());
|
||||
assert.ok(!sequentializer.hasPendingSave(2));
|
||||
assert.ok(!sequentializer.pendingSave);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('SaveSequentializer - pending and next (finishes instantly)', function (done) {
|
||||
const sequentializer = new SaveSequentializer();
|
||||
|
||||
let pendingDone = false;
|
||||
sequentializer.setPending(1, TPromise.timeout(1).then(() => { pendingDone = true; return null; }));
|
||||
|
||||
// next finishes instantly
|
||||
let nextDone = false;
|
||||
const res = sequentializer.setNext(() => TPromise.as(null).then(() => { nextDone = true; return null; }));
|
||||
|
||||
return res.done(() => {
|
||||
assert.ok(pendingDone);
|
||||
assert.ok(nextDone);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('SaveSequentializer - pending and next (finishes after timeout)', function (done) {
|
||||
const sequentializer = new SaveSequentializer();
|
||||
|
||||
let pendingDone = false;
|
||||
sequentializer.setPending(1, TPromise.timeout(1).then(() => { pendingDone = true; return null; }));
|
||||
|
||||
// next finishes after timeout
|
||||
let nextDone = false;
|
||||
const res = sequentializer.setNext(() => TPromise.timeout(1).then(() => { nextDone = true; return null; }));
|
||||
|
||||
return res.done(() => {
|
||||
assert.ok(pendingDone);
|
||||
assert.ok(nextDone);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('SaveSequentializer - pending and multiple next (last one wins)', function (done) {
|
||||
const sequentializer = new SaveSequentializer();
|
||||
|
||||
let pendingDone = false;
|
||||
sequentializer.setPending(1, TPromise.timeout(1).then(() => { pendingDone = true; return null; }));
|
||||
|
||||
// next finishes after timeout
|
||||
let firstDone = false;
|
||||
let firstRes = sequentializer.setNext(() => TPromise.timeout(2).then(() => { firstDone = true; return null; }));
|
||||
|
||||
let secondDone = false;
|
||||
let secondRes = sequentializer.setNext(() => TPromise.timeout(3).then(() => { secondDone = true; return null; }));
|
||||
|
||||
let thirdDone = false;
|
||||
let thirdRes = sequentializer.setNext(() => TPromise.timeout(4).then(() => { thirdDone = true; return null; }));
|
||||
|
||||
return TPromise.join([firstRes, secondRes, thirdRes]).then(() => {
|
||||
assert.ok(pendingDone);
|
||||
assert.ok(!firstDone);
|
||||
assert.ok(!secondDone);
|
||||
assert.ok(thirdDone);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,348 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
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, TestEditorGroupService, TestFileService } from 'vs/workbench/test/workbenchTestServices';
|
||||
import { onError } from 'vs/base/test/common/utils';
|
||||
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
|
||||
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';
|
||||
|
||||
export class TestTextFileEditorModelManager extends TextFileEditorModelManager {
|
||||
|
||||
protected debounceDelay(): number {
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
|
||||
class ServiceAccessor {
|
||||
constructor(
|
||||
@IEditorGroupService public editorGroupService: TestEditorGroupService,
|
||||
@IFileService public fileService: TestFileService,
|
||||
@IModelService public modelService: IModelService
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
function toResource(path: string): URI {
|
||||
return URI.file(join('C:\\', path));
|
||||
}
|
||||
|
||||
suite('Files - TextFileEditorModelManager', () => {
|
||||
|
||||
let instantiationService: IInstantiationService;
|
||||
let accessor: ServiceAccessor;
|
||||
|
||||
setup(() => {
|
||||
instantiationService = workbenchInstantiationService();
|
||||
accessor = instantiationService.createInstance(ServiceAccessor);
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
manager.add(URI.file('/test.html'), model1);
|
||||
manager.add(URI.file('/some/other.html'), model2);
|
||||
manager.add(URI.file('/some/this.txt'), model3);
|
||||
|
||||
const fileUpper = URI.file('/TEST.html');
|
||||
|
||||
assert(!manager.get(URI.file('foo')));
|
||||
assert.strictEqual(manager.get(URI.file('/test.html')), model1);
|
||||
|
||||
assert.ok(!manager.get(fileUpper));
|
||||
|
||||
let result = manager.getAll();
|
||||
assert.strictEqual(3, result.length);
|
||||
|
||||
result = manager.getAll(URI.file('/yes'));
|
||||
assert.strictEqual(0, result.length);
|
||||
|
||||
result = manager.getAll(URI.file('/some/other.txt'));
|
||||
assert.strictEqual(0, result.length);
|
||||
|
||||
result = manager.getAll(URI.file('/some/other.html'));
|
||||
assert.strictEqual(1, result.length);
|
||||
|
||||
result = manager.getAll(fileUpper);
|
||||
assert.strictEqual(0, result.length);
|
||||
|
||||
manager.remove(URI.file(''));
|
||||
|
||||
result = manager.getAll();
|
||||
assert.strictEqual(3, result.length);
|
||||
|
||||
manager.remove(URI.file('/some/other.html'));
|
||||
result = manager.getAll();
|
||||
assert.strictEqual(2, result.length);
|
||||
|
||||
manager.remove(fileUpper);
|
||||
result = manager.getAll();
|
||||
assert.strictEqual(2, result.length);
|
||||
|
||||
manager.clear();
|
||||
result = manager.getAll();
|
||||
assert.strictEqual(0, result.length);
|
||||
|
||||
model1.dispose();
|
||||
model2.dispose();
|
||||
model3.dispose();
|
||||
});
|
||||
|
||||
test('loadOrCreate', function (done) {
|
||||
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
|
||||
const resource = URI.file('/test.html');
|
||||
const encoding = 'utf8';
|
||||
|
||||
manager.loadOrCreate(resource, { encoding, reload: true }).done(model => {
|
||||
assert.ok(model);
|
||||
assert.equal(model.getEncoding(), encoding);
|
||||
assert.equal(manager.get(resource), model);
|
||||
|
||||
return manager.loadOrCreate(resource, { encoding }).then(model2 => {
|
||||
assert.equal(model2, model);
|
||||
|
||||
model.dispose();
|
||||
|
||||
return manager.loadOrCreate(resource, { encoding }).then(model3 => {
|
||||
assert.notEqual(model3, model2);
|
||||
assert.equal(manager.get(resource), model3);
|
||||
|
||||
model3.dispose();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
manager.add(URI.file('/test.html'), model1);
|
||||
manager.add(URI.file('/some/other.html'), model2);
|
||||
manager.add(URI.file('/some/this.txt'), model3);
|
||||
|
||||
assert.strictEqual(manager.get(URI.file('/test.html')), model1);
|
||||
|
||||
model1.dispose();
|
||||
assert(!manager.get(URI.file('/test.html')));
|
||||
|
||||
model2.dispose();
|
||||
model3.dispose();
|
||||
});
|
||||
|
||||
test('events', function (done) {
|
||||
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');
|
||||
|
||||
let dirtyCounter = 0;
|
||||
let revertedCounter = 0;
|
||||
let savedCounter = 0;
|
||||
let encodingCounter = 0;
|
||||
let orphanedCounter = 0;
|
||||
let disposeCounter = 0;
|
||||
let contentCounter = 0;
|
||||
|
||||
manager.onModelDirty(e => {
|
||||
if (e.resource.toString() === resource1.toString()) {
|
||||
dirtyCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
manager.onModelReverted(e => {
|
||||
if (e.resource.toString() === resource1.toString()) {
|
||||
revertedCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
manager.onModelSaved(e => {
|
||||
if (e.resource.toString() === resource1.toString()) {
|
||||
savedCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
manager.onModelEncodingChanged(e => {
|
||||
if (e.resource.toString() === resource1.toString()) {
|
||||
encodingCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
manager.onModelOrphanedChanged(e => {
|
||||
if (e.resource.toString() === resource1.toString()) {
|
||||
orphanedCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
manager.onModelContentChanged(e => {
|
||||
if (e.resource.toString() === resource1.toString()) {
|
||||
contentCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
manager.onModelDisposed(e => {
|
||||
disposeCounter++;
|
||||
});
|
||||
|
||||
manager.loadOrCreate(resource1, { encoding: 'utf8' }).done(model1 => {
|
||||
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.DELETED }]));
|
||||
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.ADDED }]));
|
||||
|
||||
return manager.loadOrCreate(resource2, { encoding: 'utf8' }).then(model2 => {
|
||||
model1.textEditorModel.setValue('changed');
|
||||
model1.updatePreferredEncoding('utf16');
|
||||
|
||||
return model1.revert().then(() => {
|
||||
model1.textEditorModel.setValue('changed again');
|
||||
|
||||
return model1.save().then(() => {
|
||||
model1.dispose();
|
||||
model2.dispose();
|
||||
assert.equal(disposeCounter, 2);
|
||||
|
||||
return model1.revert().then(() => { // should not trigger another event if disposed
|
||||
assert.equal(dirtyCounter, 2);
|
||||
assert.equal(revertedCounter, 1);
|
||||
assert.equal(savedCounter, 1);
|
||||
assert.equal(encodingCounter, 2);
|
||||
|
||||
// content change event if done async
|
||||
TPromise.timeout(10).then(() => {
|
||||
assert.equal(contentCounter, 2);
|
||||
assert.equal(orphanedCounter, 1);
|
||||
|
||||
model1.dispose();
|
||||
model2.dispose();
|
||||
|
||||
assert.ok(!accessor.modelService.getModel(resource1));
|
||||
assert.ok(!accessor.modelService.getModel(resource2));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('events debounced', function (done) {
|
||||
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
|
||||
|
||||
const resource1 = toResource('/path/index.txt');
|
||||
const resource2 = toResource('/path/other.txt');
|
||||
|
||||
let dirtyCounter = 0;
|
||||
let revertedCounter = 0;
|
||||
let savedCounter = 0;
|
||||
|
||||
TextFileEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = 0;
|
||||
|
||||
manager.onModelsDirty(e => {
|
||||
dirtyCounter += e.length;
|
||||
assert.equal(e[0].resource.toString(), resource1.toString());
|
||||
});
|
||||
|
||||
manager.onModelsReverted(e => {
|
||||
revertedCounter += e.length;
|
||||
assert.equal(e[0].resource.toString(), resource1.toString());
|
||||
});
|
||||
|
||||
manager.onModelsSaved(e => {
|
||||
savedCounter += e.length;
|
||||
assert.equal(e[0].resource.toString(), resource1.toString());
|
||||
});
|
||||
|
||||
manager.loadOrCreate(resource1, { encoding: 'utf8' }).done(model1 => {
|
||||
return manager.loadOrCreate(resource2, { encoding: 'utf8' }).then(model2 => {
|
||||
model1.textEditorModel.setValue('changed');
|
||||
model1.updatePreferredEncoding('utf16');
|
||||
|
||||
return model1.revert().then(() => {
|
||||
model1.textEditorModel.setValue('changed again');
|
||||
|
||||
return model1.save().then(() => {
|
||||
model1.dispose();
|
||||
model2.dispose();
|
||||
|
||||
return model1.revert().then(() => { // should not trigger another event if disposed
|
||||
return TPromise.timeout(20).then(() => {
|
||||
assert.equal(dirtyCounter, 2);
|
||||
assert.equal(revertedCounter, 1);
|
||||
assert.equal(savedCounter, 1);
|
||||
|
||||
model1.dispose();
|
||||
model2.dispose();
|
||||
|
||||
assert.ok(!accessor.modelService.getModel(resource1));
|
||||
assert.ok(!accessor.modelService.getModel(resource2));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('disposing model takes it out of the manager', function (done) {
|
||||
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
|
||||
|
||||
const resource = toResource('/path/index_something.txt');
|
||||
|
||||
manager.loadOrCreate(resource, { encoding: 'utf8' }).done(model => {
|
||||
model.dispose();
|
||||
|
||||
assert.ok(!manager.get(resource));
|
||||
assert.ok(!accessor.modelService.getModel(model.getResource()));
|
||||
|
||||
manager.dispose();
|
||||
done();
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('dispose prevents dirty model from getting disposed', function (done) {
|
||||
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
|
||||
|
||||
const resource = toResource('/path/index_something.txt');
|
||||
|
||||
manager.loadOrCreate(resource, { encoding: 'utf8' }).done((model: TextFileEditorModel) => {
|
||||
model.textEditorModel.setValue('make dirty');
|
||||
|
||||
manager.disposeModel(model);
|
||||
assert.ok(!model.isDisposed());
|
||||
|
||||
model.revert(true);
|
||||
|
||||
manager.disposeModel(model);
|
||||
assert.ok(model.isDisposed());
|
||||
|
||||
manager.dispose();
|
||||
done();
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
});
|
||||
415
src/vs/workbench/services/textfile/test/textFileService.test.ts
Normal file
415
src/vs/workbench/services/textfile/test/textFileService.test.ts
Normal file
@@ -0,0 +1,415 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { ILifecycleService, ShutdownEvent, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestWindowsService, TestContextService } from 'vs/workbench/test/workbenchTestServices';
|
||||
import { onError, toResource } from 'vs/base/test/common/utils';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IWindowsService } from 'vs/platform/windows/common/windows';
|
||||
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ConfirmResult } from 'vs/workbench/common/editor';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
|
||||
import { HotExitConfiguration } from 'vs/platform/files/common/files';
|
||||
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
|
||||
class ServiceAccessor {
|
||||
constructor(
|
||||
@ILifecycleService public lifecycleService: TestLifecycleService,
|
||||
@ITextFileService public textFileService: TestTextFileService,
|
||||
@IUntitledEditorService public untitledEditorService: IUntitledEditorService,
|
||||
@IWindowsService public windowsService: TestWindowsService,
|
||||
@IWorkspaceContextService public contextService: TestContextService
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
class ShutdownEventImpl implements ShutdownEvent {
|
||||
|
||||
public value: boolean | TPromise<boolean>;
|
||||
public reason = ShutdownReason.CLOSE;
|
||||
|
||||
veto(value: boolean | TPromise<boolean>): void {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
suite('Files - TextFileService', () => {
|
||||
|
||||
let instantiationService: IInstantiationService;
|
||||
let model: TextFileEditorModel;
|
||||
let accessor: ServiceAccessor;
|
||||
|
||||
setup(() => {
|
||||
instantiationService = workbenchInstantiationService();
|
||||
accessor = instantiationService.createInstance(ServiceAccessor);
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
model.dispose();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
|
||||
accessor.untitledEditorService.revertAll();
|
||||
});
|
||||
|
||||
test('confirm onWillShutdown - no veto', function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const event = new ShutdownEventImpl();
|
||||
accessor.lifecycleService.fireWillShutdown(event);
|
||||
|
||||
const veto = event.value;
|
||||
if (typeof veto === 'boolean') {
|
||||
assert.ok(!veto);
|
||||
} else {
|
||||
veto.then(veto => {
|
||||
assert.ok(!veto);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('confirm onWillShutdown - veto if user cancels', function (done) {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
service.setConfirmResult(ConfirmResult.CANCEL);
|
||||
|
||||
model.load().done(() => {
|
||||
model.textEditorModel.setValue('foo');
|
||||
|
||||
assert.equal(service.getDirty().length, 1);
|
||||
|
||||
const event = new ShutdownEventImpl();
|
||||
accessor.lifecycleService.fireWillShutdown(event);
|
||||
|
||||
assert.ok(event.value);
|
||||
|
||||
done();
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('confirm onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', function (done) {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
service.setConfirmResult(ConfirmResult.DONT_SAVE);
|
||||
service.onConfigurationChange({ files: { hotExit: 'off' } });
|
||||
|
||||
model.load().done(() => {
|
||||
model.textEditorModel.setValue('foo');
|
||||
|
||||
assert.equal(service.getDirty().length, 1);
|
||||
|
||||
const event = new ShutdownEventImpl();
|
||||
accessor.lifecycleService.fireWillShutdown(event);
|
||||
|
||||
const veto = event.value;
|
||||
if (typeof veto === 'boolean') {
|
||||
assert.ok(service.cleanupBackupsBeforeShutdownCalled);
|
||||
assert.ok(!veto);
|
||||
|
||||
done();
|
||||
} else {
|
||||
veto.then(veto => {
|
||||
assert.ok(service.cleanupBackupsBeforeShutdownCalled);
|
||||
assert.ok(!veto);
|
||||
|
||||
done();
|
||||
});
|
||||
}
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('confirm onWillShutdown - save (hot.exit: off)', function (done) {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
service.setConfirmResult(ConfirmResult.SAVE);
|
||||
service.onConfigurationChange({ files: { hotExit: 'off' } });
|
||||
|
||||
model.load().done(() => {
|
||||
model.textEditorModel.setValue('foo');
|
||||
|
||||
assert.equal(service.getDirty().length, 1);
|
||||
|
||||
const event = new ShutdownEventImpl();
|
||||
accessor.lifecycleService.fireWillShutdown(event);
|
||||
|
||||
return (<TPromise<boolean>>event.value).then(veto => {
|
||||
assert.ok(!veto);
|
||||
assert.ok(!model.isDirty());
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('isDirty/getDirty - files and untitled', function (done) {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
model.load().done(() => {
|
||||
assert.ok(!service.isDirty(model.getResource()));
|
||||
model.textEditorModel.setValue('foo');
|
||||
|
||||
assert.ok(service.isDirty(model.getResource()));
|
||||
assert.equal(service.getDirty().length, 1);
|
||||
assert.equal(service.getDirty([model.getResource()])[0].toString(), model.getResource().toString());
|
||||
|
||||
const untitled = accessor.untitledEditorService.createOrGet();
|
||||
return untitled.resolve().then((model: UntitledEditorModel) => {
|
||||
assert.ok(!service.isDirty(untitled.getResource()));
|
||||
assert.equal(service.getDirty().length, 1);
|
||||
model.textEditorModel.setValue('changed');
|
||||
|
||||
assert.ok(service.isDirty(untitled.getResource()));
|
||||
assert.equal(service.getDirty().length, 2);
|
||||
assert.equal(service.getDirty([untitled.getResource()])[0].toString(), untitled.getResource().toString());
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('save - file', function (done) {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
|
||||
model.load().done(() => {
|
||||
model.textEditorModel.setValue('foo');
|
||||
|
||||
assert.ok(service.isDirty(model.getResource()));
|
||||
|
||||
return service.save(model.getResource()).then(res => {
|
||||
assert.ok(res);
|
||||
assert.ok(!service.isDirty(model.getResource()));
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('saveAll - file', function (done) {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
|
||||
model.load().done(() => {
|
||||
model.textEditorModel.setValue('foo');
|
||||
|
||||
assert.ok(service.isDirty(model.getResource()));
|
||||
|
||||
return service.saveAll([model.getResource()]).then(res => {
|
||||
assert.ok(res);
|
||||
assert.ok(!service.isDirty(model.getResource()));
|
||||
assert.equal(res.results.length, 1);
|
||||
assert.equal(res.results[0].source.toString(), model.getResource().toString());
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('saveAs - file', function (done) {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
service.setPromptPath(model.getResource().fsPath);
|
||||
|
||||
model.load().done(() => {
|
||||
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.ok(!service.isDirty(model.getResource()));
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
test('revert - file', function (done) {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
service.setPromptPath(model.getResource().fsPath);
|
||||
|
||||
model.load().done(() => {
|
||||
model.textEditorModel.setValue('foo');
|
||||
|
||||
assert.ok(service.isDirty(model.getResource()));
|
||||
|
||||
return service.revert(model.getResource()).then(res => {
|
||||
assert.ok(res);
|
||||
assert.ok(!service.isDirty(model.getResource()));
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
});
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
/*
|
||||
suite('Hot Exit', () => {
|
||||
suite('"onExit" setting', () => {
|
||||
test('should hot exit on non-Mac (reason: CLOSE, windows: single, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, true, !!platform.isMacintosh, done);
|
||||
});
|
||||
test('should hot exit on non-Mac (reason: CLOSE, windows: single, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, false, !!platform.isMacintosh, done);
|
||||
});
|
||||
test('should NOT hot exit (reason: CLOSE, windows: multiple, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, true, true, true, done);
|
||||
});
|
||||
test('should NOT hot exit (reason: CLOSE, windows: multiple, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, true, false, true, done);
|
||||
});
|
||||
test('should hot exit (reason: QUIT, windows: single, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, false, true, false, done);
|
||||
});
|
||||
test('should hot exit (reason: QUIT, windows: single, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, false, false, false, done);
|
||||
});
|
||||
test('should hot exit (reason: QUIT, windows: multiple, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, true, true, false, done);
|
||||
});
|
||||
test('should hot exit (reason: QUIT, windows: multiple, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, true, false, false, done);
|
||||
});
|
||||
test('should hot exit (reason: RELOAD, windows: single, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, false, true, false, done);
|
||||
});
|
||||
test('should hot exit (reason: RELOAD, windows: single, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, false, false, false, done);
|
||||
});
|
||||
test('should hot exit (reason: RELOAD, windows: multiple, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, true, true, false, done);
|
||||
});
|
||||
test('should hot exit (reason: RELOAD, windows: multiple, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, true, false, false, done);
|
||||
});
|
||||
test('should NOT hot exit (reason: LOAD, windows: single, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, false, true, true, done);
|
||||
});
|
||||
test('should NOT hot exit (reason: LOAD, windows: single, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, false, false, true, done);
|
||||
});
|
||||
test('should NOT hot exit (reason: LOAD, windows: multiple, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, true, true, true, done);
|
||||
});
|
||||
test('should NOT hot exit (reason: LOAD, windows: multiple, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, true, false, true, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('"onExitAndWindowClose" setting', () => {
|
||||
test('should hot exit (reason: CLOSE, windows: single, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, false, true, false, done);
|
||||
});
|
||||
test('should hot exit (reason: CLOSE, windows: single, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, false, false, !!platform.isMacintosh, done);
|
||||
});
|
||||
test('should hot exit (reason: CLOSE, windows: multiple, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, true, true, false, done);
|
||||
});
|
||||
test('should NOT hot exit (reason: CLOSE, windows: multiple, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, true, false, true, done);
|
||||
});
|
||||
test('should hot exit (reason: QUIT, windows: single, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, false, true, false, done);
|
||||
});
|
||||
test('should hot exit (reason: QUIT, windows: single, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, false, false, false, done);
|
||||
});
|
||||
test('should hot exit (reason: QUIT, windows: multiple, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, true, true, false, done);
|
||||
});
|
||||
test('should hot exit (reason: QUIT, windows: multiple, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, true, false, false, done);
|
||||
});
|
||||
test('should hot exit (reason: RELOAD, windows: single, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, false, true, false, done);
|
||||
});
|
||||
test('should hot exit (reason: RELOAD, windows: single, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, false, false, false, done);
|
||||
});
|
||||
test('should hot exit (reason: RELOAD, windows: multiple, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, true, true, false, done);
|
||||
});
|
||||
test('should hot exit (reason: RELOAD, windows: multiple, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, true, false, false, done);
|
||||
});
|
||||
test('should hot exit (reason: LOAD, windows: single, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, false, true, false, done);
|
||||
});
|
||||
test('should NOT hot exit (reason: LOAD, windows: single, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, false, false, true, done);
|
||||
});
|
||||
test('should hot exit (reason: LOAD, windows: multiple, workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, true, true, false, done);
|
||||
});
|
||||
test('should NOT hot exit (reason: LOAD, windows: multiple, empty workspace)', function (done) {
|
||||
hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, true, false, true, done);
|
||||
});
|
||||
});
|
||||
|
||||
function hotExitTest(setting: string, shutdownReason: ShutdownReason, multipleWindows: boolean, workspace: true, shouldVeto: boolean, done: () => void): void {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
// Set hot exit config
|
||||
service.onConfigurationChange({ files: { hotExit: setting } });
|
||||
// Set empty workspace if required
|
||||
if (!workspace) {
|
||||
accessor.contextService.setWorkspace(null);
|
||||
}
|
||||
// Set multiple windows if required
|
||||
if (multipleWindows) {
|
||||
accessor.windowsService.windowCount = 2;
|
||||
}
|
||||
// Set cancel to force a veto if hot exit does not trigger
|
||||
service.setConfirmResult(ConfirmResult.CANCEL);
|
||||
|
||||
model.load().done(() => {
|
||||
model.textEditorModel.setValue('foo');
|
||||
|
||||
assert.equal(service.getDirty().length, 1);
|
||||
|
||||
const event = new ShutdownEventImpl();
|
||||
event.reason = shutdownReason;
|
||||
accessor.lifecycleService.fireWillShutdown(event);
|
||||
|
||||
return (<TPromise<boolean>>event.value).then(veto => {
|
||||
// When hot exit is set, backups should never be cleaned since the confirm result is cancel
|
||||
assert.ok(!service.cleanupBackupsBeforeShutdownCalled);
|
||||
assert.equal(veto, shouldVeto);
|
||||
|
||||
done();
|
||||
});
|
||||
}, error => onError(error, done));
|
||||
}
|
||||
});
|
||||
// {{SQL CARBON EDIT}}
|
||||
*/
|
||||
});
|
||||
Reference in New Issue
Block a user