SQL Operations Studio Public Preview 1 (0.23) release source code

This commit is contained in:
Karl Burtram
2017-11-09 14:30:27 -08:00
parent b88ecb8d93
commit 3cdac41339
8829 changed files with 759707 additions and 286 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}
}

View 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();
}
}

View 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;
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View 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(['123']);
});
});

View 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++));
}
}

View File

@@ -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();
});
});
});

View File

@@ -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));
});
});

View 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}}
*/
});