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