diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 9f473f2a5ec296cbd4c87b50a8624089bbd7dc14..fe73cfd090e46abaa46802e4426106b5bd410542 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -126,6 +126,11 @@ export interface IFileService { */ updateOptions(options: any): void; + /** + * Returns the preferred encoding to use for a given resource. + */ + getEncoding(resource: URI): string; + /** * Frees up any resources occupied by this service. */ diff --git a/src/vs/workbench/parts/backup/common/backupRestorer.ts b/src/vs/workbench/parts/backup/common/backupRestorer.ts index de7ffed148f579b51da5fffee4342b45fe963e2b..dbef7d438fe45aa8da1371dacd1dd28cdef8f701 100644 --- a/src/vs/workbench/parts/backup/common/backupRestorer.ts +++ b/src/vs/workbench/parts/backup/common/backupRestorer.ts @@ -59,6 +59,27 @@ export class BackupRestorer implements IWorkbenchContribution { }); } + private doResolveOpenedBackups(backups: URI[]): TPromise { + const stacks = this.groupService.getStacksModel(); + + const restorePromises: TPromise[] = []; + const unresolved: URI[] = []; + + backups.forEach(backup => { + if (stacks.isOpen(backup)) { + if (backup.scheme === 'file') { + restorePromises.push(this.textModelResolverService.createModelReference(backup).then(null, () => unresolved.push(backup))); + } else if (backup.scheme === 'untitled') { + restorePromises.push(this.untitledEditorService.get(backup).resolve().then(null, () => unresolved.push(backup))); + } + } else { + unresolved.push(backup); + } + }); + + return TPromise.join(restorePromises).then(() => unresolved, () => unresolved); + } + private doOpenEditors(inputs: URI[]): TPromise { const stacks = this.groupService.getStacksModel(); const hasOpenedEditors = stacks.groups.length > 0; @@ -83,27 +104,6 @@ export class BackupRestorer implements IWorkbenchContribution { return this.editorService.createInput({ resource }); } - private doResolveOpenedBackups(backups: URI[]): TPromise { - const stacks = this.groupService.getStacksModel(); - - const restorePromises: TPromise[] = []; - const unresolved: URI[] = []; - - backups.forEach(backup => { - if (stacks.isOpen(backup)) { - if (backup.scheme === 'file') { - restorePromises.push(this.textModelResolverService.createModelReference(backup).then(null, () => unresolved.push(backup))); - } else if (backup.scheme === 'untitled') { - restorePromises.push(this.untitledEditorService.get(backup).resolve().then(null, () => unresolved.push(backup))); - } - } else { - unresolved.push(backup); - } - }); - - return TPromise.join(restorePromises).then(() => unresolved, () => unresolved); - } - public getId(): string { return 'vs.backup.backupRestorer'; } diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index 18e7c0c9c602421f5667d5b3fc6ebb7cc09eee87..5ec0409db58610a532bae052dab6e89322dc8d92 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -289,6 +289,10 @@ export class FileService implements IFileService { this.raw.unwatchFileChanges(arg1); } + public getEncoding(resource: uri): string { + return this.raw.getEncoding(resource); + } + public dispose(): void { this.toUnbind = dispose(this.toUnbind); diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 687eff0b625f21a6354e969be01c61ab4dd1ab0b..ed34861731271a51cf926aa6827059fb33f2aa83 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -518,7 +518,7 @@ export class FileService implements IFileService { }); } - private getEncoding(resource: uri, preferredEncoding?: string): string { + public getEncoding(resource: uri, preferredEncoding?: string): string { let fileEncoding: string; const override = this.getEncodingOverride(resource); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 447167900f79fbe86b91b9c19cfb03a28b1002dc..1b168a94ffc812a923bd98b87d145a52a175761b 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -19,11 +19,11 @@ import types = require('vs/base/common/types'); import { IModelContentChangedEvent, IRawText } from 'vs/editor/common/editorCommon'; import { IMode } from 'vs/editor/common/modes'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, IModelSaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, IModelSaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, IRawTextContent } from 'vs/workbench/services/textfile/common/textfiles'; import { EncodingMode, EditorModel } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { IBackupFileService, BACKUP_FILE_RESOLVE_OPTIONS } from 'vs/workbench/services/backup/common/backup'; -import { IFileService, IFileStat, IFileOperationResult, FileOperationResult } from 'vs/platform/files/common/files'; +import { IFileService, IFileStat, IFileOperationResult, FileOperationResult, IContent } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IMessageService, Severity } from 'vs/platform/message/common/message'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -196,7 +196,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Emit file change event this._onDidStateChange.fire(StateChange.REVERTED); - }, (error) => { + }, error => { // FileNotFound means the file got deleted meanwhile, so emit revert event because thats ok if ((error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { @@ -233,109 +233,135 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } // Resolve Content - return this.textFileService.resolveTextContent(this.resource, { acceptTextOnly: true, etag: etag, encoding: this.preferredEncoding }).then((content) => { - diag('load() - resolved content', this.resource, new Date()); - - // Telemetry - this.telemetryService.publicLog('fileGet', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: paths.extname(this.resource.fsPath), path: anonymize(this.resource.fsPath) }); - - // Update our resolved disk stat model - const resolvedStat: IFileStat = { - resource: this.resource, - name: content.name, - mtime: content.mtime, - etag: content.etag, - isDirectory: false, - hasChildren: false, - children: void 0, - }; - this.updateVersionOnDiskStat(resolvedStat); + return this.textFileService.resolveTextContent(this.resource, { acceptTextOnly: true, etag: etag, encoding: this.preferredEncoding }).then(content => this.loadWithContent(content), error => { + const result = (error).fileOperationResult; - // Keep the original encoding to not loose it when saving - const oldEncoding = this.contentEncoding; - this.contentEncoding = content.encoding; + // NotModified status is expected and can be handled gracefully + if (result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) { + this.setDirty(false); // Ensure we are not tracking a stale state - // Handle events if encoding changed - if (this.preferredEncoding) { - this.updatePreferredEncoding(this.contentEncoding); // make sure to reflect the real encoding of the file (never out of sync) - } else if (oldEncoding !== this.contentEncoding) { - this._onDidStateChange.fire(StateChange.ENCODING); + return TPromise.as(this); } - // Update Existing Model - if (this.textEditorModel) { - diag('load() - updated text editor model', this.resource, new Date()); + // FileNotFound needs to be handled if we have a backup + if (result === FileOperationResult.FILE_NOT_FOUND) { + if (!this.textEditorModel && !this.createTextEditorModelPromise) { + return this.backupFileService.loadBackupResource(this.resource).then(backup => { + if (!!backup) { + const content: IContent = { + resource: this.resource, + name: paths.basename(this.resource.fsPath), + mtime: Date.now(), + etag: void 0, + value: '', /* will be filled later from backup */ + encoding: this.fileService.getEncoding(this.resource) + }; + + return this.loadWithContent(content); + } + + // Otherwise bubble up the error + return TPromise.wrapError(error); + }, ignoreError => TPromise.wrapError(error)); + } + } - this.setDirty(false); // Ensure we are not tracking a stale state + // Otherwise bubble up the error + return TPromise.wrapError(error); + }); + } - this.blockModelContentChange = true; - try { - this.updateTextEditorModel(content.value); - } finally { - this.blockModelContentChange = false; - } + private loadWithContent(content: IRawTextContent | IContent): TPromise { + diag('load() - resolved content', this.resource, new Date()); - return TPromise.as(this); - } + // Telemetry + this.telemetryService.publicLog('fileGet', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: paths.extname(this.resource.fsPath), path: anonymize(this.resource.fsPath) }); - // Join an existing request to create the editor model to avoid race conditions - else if (this.createTextEditorModelPromise) { - diag('load() - join existing text editor model promise', this.resource, new Date()); + // Update our resolved disk stat model + const resolvedStat: IFileStat = { + resource: this.resource, + name: content.name, + mtime: content.mtime, + etag: content.etag, + isDirectory: false, + hasChildren: false, + children: void 0, + }; + this.updateVersionOnDiskStat(resolvedStat); - return this.createTextEditorModelPromise; - } + // Keep the original encoding to not loose it when saving + const oldEncoding = this.contentEncoding; + this.contentEncoding = content.encoding; - // Create New Model - else { - diag('load() - created text editor model', this.resource, new Date()); - - return this.backupFileService.loadBackupResource(this.resource).then(backupResource => { - let resolveBackupPromise: TPromise; - - // Try get restore content, if there is an issue fallback silently to the original file's content - if (backupResource) { - resolveBackupPromise = this.textFileService.resolveTextContent(backupResource, BACKUP_FILE_RESOLVE_OPTIONS).then(backup => { - return this.backupFileService.parseBackupContent(backup); - }, error => content.value); - } else { - resolveBackupPromise = TPromise.as(content.value); - } - - this.createTextEditorModelPromise = resolveBackupPromise.then(fileContent => { - return this.createTextEditorModel(fileContent, content.resource).then(() => { - this.createTextEditorModelPromise = null; - - if (backupResource) { - this.makeDirty(); - } else { - this.setDirty(false); // Ensure we are not tracking a stale state - } - - this.toDispose.push(this.textEditorModel.onDidChangeRawContent((e: IModelContentChangedEvent) => this.onModelContentChanged(e))); - - return this; - }, (error) => { - this.createTextEditorModelPromise = null; - - return TPromise.wrapError(error); - }); - }); + // Handle events if encoding changed + if (this.preferredEncoding) { + this.updatePreferredEncoding(this.contentEncoding); // make sure to reflect the real encoding of the file (never out of sync) + } else if (oldEncoding !== this.contentEncoding) { + this._onDidStateChange.fire(StateChange.ENCODING); + } - return this.createTextEditorModelPromise; - }); - } - }, (error) => { + // Update Existing Model + if (this.textEditorModel) { + diag('load() - updated text editor model', this.resource, new Date()); - // NotModified status code is expected and can be handled gracefully - if ((error).fileOperationResult === FileOperationResult.FILE_NOT_MODIFIED_SINCE) { - this.setDirty(false); // Ensure we are not tracking a stale state + this.setDirty(false); // Ensure we are not tracking a stale state - return TPromise.as(this); + this.blockModelContentChange = true; + try { + this.updateTextEditorModel(content.value); + } finally { + this.blockModelContentChange = false; } - // Otherwise bubble up the error - return TPromise.wrapError(error); - }); + return TPromise.as(this); + } + + // Join an existing request to create the editor model to avoid race conditions + else if (this.createTextEditorModelPromise) { + diag('load() - join existing text editor model promise', this.resource, new Date()); + + return this.createTextEditorModelPromise; + } + + // Create New Model + else { + diag('load() - created text editor model', this.resource, new Date()); + + return this.backupFileService.loadBackupResource(this.resource).then(backupResource => { + let resolveBackupPromise: TPromise; + + // Try get restore content, if there is an issue fallback silently to the original file's content + if (backupResource) { + resolveBackupPromise = this.textFileService.resolveTextContent(backupResource, BACKUP_FILE_RESOLVE_OPTIONS).then(backup => { + return this.backupFileService.parseBackupContent(backup); + }, error => content.value); + } else { + resolveBackupPromise = TPromise.as(content.value); + } + + this.createTextEditorModelPromise = resolveBackupPromise.then(fileContent => { + return this.createTextEditorModel(fileContent, content.resource).then(() => { + this.createTextEditorModelPromise = null; + + if (backupResource) { + this.makeDirty(); + } else { + this.setDirty(false); // Ensure we are not tracking a stale state + } + + this.toDispose.push(this.textEditorModel.onDidChangeRawContent((e: IModelContentChangedEvent) => this.onModelContentChanged(e))); + + return this; + }, error => { + this.createTextEditorModelPromise = null; + + return TPromise.wrapError(error); + }); + }); + + return this.createTextEditorModelPromise; + }); + } } protected getOrCreateMode(modeService: IModeService, preferredModeIds: string, firstLineText?: string): TPromise { @@ -561,7 +587,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Emit File Saved Event this._onDidStateChange.fire(StateChange.SAVED); - }, (error) => { + }, error => { diag(`doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource, new Date()); // Remove from pending saves