diff --git a/src/vs/workbench/contrib/debug/electron-browser/debugService.ts b/src/vs/workbench/contrib/debug/electron-browser/debugService.ts index 2dd13b297e382..8bd52bacb4daa 100644 --- a/src/vs/workbench/contrib/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/electron-browser/debugService.ts @@ -392,7 +392,7 @@ export class DebugService implements IDebugService { return this.showError(err.message).then(() => false); } if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - return this.showError(nls.localize('noFolderWorkspaceDebugError', "The active file can not be debugged. Make sure it is saved on disk and that you have a debug extension installed for that file type.")) + return this.showError(nls.localize('noFolderWorkspaceDebugError', "The active file can not be debugged. Make sure it is saved and that you have a debug extension installed for that file type.")) .then(() => false); } diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 5dc462a1263a6..5ef1b8eb03280 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -160,11 +160,11 @@ function appendEditorTitleContextMenuItem(id: string, title: string, when: Conte } // Editor Title Menu for Conflict Resolution -appendSaveConflictEditorTitleAction('workbench.files.action.acceptLocalChanges', nls.localize('acceptLocalChanges', "Use your changes and overwrite disk contents"), { +appendSaveConflictEditorTitleAction('workbench.files.action.acceptLocalChanges', nls.localize('acceptLocalChanges', "Use your changes and overwrite file contents"), { light: URI.parse(require.toUrl(`vs/workbench/contrib/files/browser/media/check.svg`)), dark: URI.parse(require.toUrl(`vs/workbench/contrib/files/browser/media/check-inverse.svg`)) }, -10, acceptLocalChangesCommand); -appendSaveConflictEditorTitleAction('workbench.files.action.revertLocalChanges', nls.localize('revertLocalChanges', "Discard your changes and revert to content on disk"), { +appendSaveConflictEditorTitleAction('workbench.files.action.revertLocalChanges', nls.localize('revertLocalChanges', "Discard your changes and revert to file contents"), { light: URI.parse(require.toUrl(`vs/workbench/contrib/files/browser/media/undo.svg`)), dark: URI.parse(require.toUrl(`vs/workbench/contrib/files/browser/media/undo-inverse.svg`)) }, -9, revertLocalChangesCommand); diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 9bda6cadedf97..747a78e70b860 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -10,7 +10,7 @@ import { IWindowsService, IWindowService, IURIToOpen, IOpenSettings, INewWindowO import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ExplorerFocusCondition, FileOnDiskContentProvider, VIEWLET_ID, IExplorerService, resourceToFileOnDisk } from 'vs/workbench/contrib/files/common/files'; +import { ExplorerFocusCondition, TextFileContentProvider, VIEWLET_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { ExplorerViewlet } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ITextFileService, ISaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; @@ -319,7 +319,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ if (providerDisposables.length === 0) { registerEditorListener = true; - const provider = instantiationService.createInstance(FileOnDiskContentProvider); + const provider = instantiationService.createInstance(TextFileContentProvider); providerDisposables.push(provider); providerDisposables.push(textModelService.registerTextModelContentProvider(COMPARE_WITH_SAVED_SCHEMA, provider)); } @@ -328,9 +328,9 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const uri = getResourceForCommand(resource, accessor.get(IListService), editorService); if (uri && fileService.canHandleResource(uri)) { const name = basename(uri); - const editorLabel = nls.localize('modifiedLabel', "{0} (on disk) ↔ {1}", name, name); + const editorLabel = nls.localize('modifiedLabel', "{0} (in file) ↔ {1}", name, name); - editorService.openEditor({ leftResource: resourceToFileOnDisk(COMPARE_WITH_SAVED_SCHEMA, uri), rightResource: uri, label: editorLabel }).then(() => { + TextFileContentProvider.open(uri, COMPARE_WITH_SAVED_SCHEMA, editorLabel, editorService).then(() => { // Dispose once no more diff editor is opened with the scheme if (registerEditorListener) { diff --git a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts index 0e8c29a270627..a6ca3697b6049 100644 --- a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts @@ -19,7 +19,7 @@ import { ResourceMap } from 'vs/base/common/map'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { FileOnDiskContentProvider, resourceToFileOnDisk } from 'vs/workbench/contrib/files/common/files'; +import { TextFileContentProvider } from 'vs/workbench/contrib/files/common/files'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { IModelService } from 'vs/editor/common/services/modelService'; import { SAVE_FILE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL } from 'vs/workbench/contrib/files/browser/fileCommands'; @@ -39,7 +39,7 @@ export const CONFLICT_RESOLUTION_SCHEME = 'conflictResolution'; const LEARN_MORE_DIRTY_WRITE_IGNORE_KEY = 'learnMoreDirtyWriteError'; -const conflictEditorHelp = nls.localize('userGuide', "Use the actions in the editor tool bar to either undo your changes or overwrite the content on disk with your changes."); +const conflictEditorHelp = nls.localize('userGuide', "Use the actions in the editor tool bar to either undo your changes or overwrite the content of the file with your changes."); // A handler for save error happening with conflict resolution actions export class SaveErrorHandler extends Disposable implements ISaveErrorHandler, IWorkbenchContribution { @@ -61,7 +61,7 @@ export class SaveErrorHandler extends Disposable implements ISaveErrorHandler, I this.messages = new ResourceMap(); this.conflictResolutionContext = new RawContextKey(CONFLICT_RESOLUTION_CONTEXT, false).bindTo(contextKeyService); - const provider = this._register(instantiationService.createInstance(FileOnDiskContentProvider)); + const provider = this._register(instantiationService.createInstance(TextFileContentProvider)); this._register(textModelService.registerTextModelContentProvider(CONFLICT_RESOLUTION_SCHEME, provider)); // Hook into model @@ -125,7 +125,7 @@ export class SaveErrorHandler extends Disposable implements ISaveErrorHandler, I // Otherwise show the message that will lead the user into the save conflict editor. else { - message = nls.localize('staleSaveError', "Failed to save '{0}': The content on disk is newer. Please compare your version with the one on disk.", basename(resource)); + message = nls.localize('staleSaveError', "Failed to save '{0}': The content of the file is newer. Please compare your version with the file contents.", basename(resource)); actions.primary!.push(this.instantiationService.createInstance(ResolveSaveConflictAction, model)); } @@ -244,16 +244,9 @@ class ResolveSaveConflictAction extends Action { if (!this.model.isDisposed()) { const resource = this.model.getResource(); const name = basename(resource); - const editorLabel = nls.localize('saveConflictDiffLabel', "{0} (on disk) ↔ {1} (in {2}) - Resolve save conflict", name, name, this.environmentService.appNameLong); - - return this.editorService.openEditor( - { - leftResource: resourceToFileOnDisk(CONFLICT_RESOLUTION_SCHEME, resource), - rightResource: resource, - label: editorLabel, - options: { pinned: true } - } - ).then(() => { + const editorLabel = nls.localize('saveConflictDiffLabel', "{0} (in file) ↔ {1} (in {2}) - Resolve save conflict", name, name, this.environmentService.appNameLong); + + return TextFileContentProvider.open(resource, CONFLICT_RESOLUTION_SCHEME, editorLabel, this.editorService, { pinned: true }).then(() => { if (this.storageService.getBoolean(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, StorageScope.GLOBAL)) { return; // return if this message is ignored } diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts index 02eab17958bd9..ecd630e356ab0 100644 --- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts @@ -219,7 +219,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { private decorateLabel(label: string): string { const model = this.textFileService.models.get(this.resource); if (model && model.hasState(ModelState.ORPHAN)) { - return localize('orphanedFile', "{0} (deleted from disk)", label); + return localize('orphanedFile', "{0} (deleted)", label); } if (model && model.isReadonly()) { diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index f0b2a14c926d7..43bbbb927869a 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -23,6 +23,8 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { once } from 'vs/base/common/functional'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; /** * Explorer viewlet id. @@ -58,8 +60,8 @@ export interface IExplorerService { isCut(stat: ExplorerItem): boolean; /** - * Selects and reveal the file element provided by the given resource if its found in the explorer. Will try to - * resolve the path from the disk in case the explorer is not yet expanded to the file yet. + * Selects and reveal the file element provided by the given resource if its found in the explorer. + * Will try to resolve the path in case the explorer is not yet expanded to the file yet. */ select(resource: URI, reveal?: boolean): Promise; } @@ -131,15 +133,7 @@ export const SortOrderConfiguration = { export type SortOrder = 'default' | 'mixed' | 'filesFirst' | 'type' | 'modified'; -export function resourceToFileOnDisk(scheme: string, resource: URI): URI { - return resource.with({ scheme, query: JSON.stringify({ scheme: resource.scheme }) }); -} - -export function fileOnDiskToResource(resource: URI): URI { - return resource.with({ scheme: JSON.parse(resource.query)['scheme'], query: null }); -} - -export class FileOnDiskContentProvider implements ITextModelContentProvider { +export class TextFileContentProvider implements ITextModelContentProvider { private fileWatcherDisposable: IDisposable | undefined; constructor( @@ -147,16 +141,34 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider { @IFileService private readonly fileService: IFileService, @IModeService private readonly modeService: IModeService, @IModelService private readonly modelService: IModelService - ) { + ) { } + + static open(resource: URI, scheme: string, label: string, editorService: IEditorService, options?: ITextEditorOptions): Promise { + return editorService.openEditor( + { + leftResource: TextFileContentProvider.resourceToTextFile(scheme, resource), + rightResource: resource, + label, + options + } + ).then(); + } + + private static resourceToTextFile(scheme: string, resource: URI): URI { + return resource.with({ scheme, query: JSON.stringify({ scheme: resource.scheme }) }); + } + + private static textFileToResource(resource: URI): URI { + return resource.with({ scheme: JSON.parse(resource.query)['scheme'], query: null }); } provideTextContent(resource: URI): Promise { - const savedFileResource = fileOnDiskToResource(resource); + const savedFileResource = TextFileContentProvider.textFileToResource(resource); - // Make sure our file from disk is resolved up to date + // Make sure our text file is resolved up to date return this.resolveEditorModel(resource).then(codeEditorModel => { - // Make sure to keep contents on disk up to date when it changes + // Make sure to keep contents up to date when it changes if (!this.fileWatcherDisposable) { this.fileWatcherDisposable = this.fileService.onFileChanges(changes => { if (changes.contains(savedFileResource, FileChangeType.UPDATED)) { @@ -179,18 +191,18 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider { private resolveEditorModel(resource: URI, createAsNeeded?: true): Promise; private resolveEditorModel(resource: URI, createAsNeeded?: boolean): Promise; private resolveEditorModel(resource: URI, createAsNeeded: boolean = true): Promise { - const savedFileResource = fileOnDiskToResource(resource); + const savedFileResource = TextFileContentProvider.textFileToResource(resource); return this.textFileService.readStream(savedFileResource).then(content => { let codeEditorModel = this.modelService.getModel(resource); if (codeEditorModel) { this.modelService.updateModel(codeEditorModel, content.value); } else if (createAsNeeded) { - const fileOnDiskModel = this.modelService.getModel(savedFileResource); + const textFileModel = this.modelService.getModel(savedFileResource); let languageSelector: ILanguageSelection; - if (fileOnDiskModel) { - languageSelector = this.modeService.create(fileOnDiskModel.getModeId()); + if (textFileModel) { + languageSelector = this.modeService.create(textFileModel.getModeId()); } else { languageSelector = this.modeService.createByFilepathOrFirstLine(savedFileResource.fsPath); } diff --git a/src/vs/workbench/contrib/files/test/common/fileOnDiskProvider.test.ts b/src/vs/workbench/contrib/files/test/common/fileOnDiskProvider.test.ts index 62fcf91ca425e..9c2064028d2bc 100644 --- a/src/vs/workbench/contrib/files/test/common/fileOnDiskProvider.test.ts +++ b/src/vs/workbench/contrib/files/test/common/fileOnDiskProvider.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestFileService } from 'vs/workbench/test/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { FileOnDiskContentProvider, resourceToFileOnDisk } from 'vs/workbench/contrib/files/common/files'; +import { TextFileContentProvider } from 'vs/workbench/contrib/files/common/files'; import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; import { IFileService } from 'vs/platform/files/common/files'; @@ -29,10 +29,10 @@ suite('Files - FileOnDiskContentProvider', () => { }); test('provideTextContent', async () => { - const provider = instantiationService.createInstance(FileOnDiskContentProvider); + const provider = instantiationService.createInstance(TextFileContentProvider); const uri = URI.parse('testFileOnDiskContentProvider://foo'); - const content = await provider.provideTextContent(resourceToFileOnDisk('conflictResolution', uri)); + const content = await provider.provideTextContent(uri.with({ scheme: 'conflictResolution', query: JSON.stringify({ scheme: uri.scheme }) })); assert.equal(snapshotToString(content.createSnapshot()), 'Hello Html'); assert.equal(accessor.fileService.getLastReadFileUri().toString(), uri.toString()); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 199e8c65bb692..c114ccbe3b93b 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -72,7 +72,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private bufferSavedVersionId: number; private blockModelContentChange: boolean; - private lastResolvedDiskStat: IFileStatWithMetadata; + private lastResolvedFileStat: IFileStatWithMetadata; private autoSaveAfterMillies?: number; private autoSaveAfterMilliesEnabled: boolean; @@ -228,11 +228,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Only fill in model metadata if resource matches let meta: IBackupMetaData | undefined = undefined; - if (isEqual(target, this.resource) && this.lastResolvedDiskStat) { + if (isEqual(target, this.resource) && this.lastResolvedFileStat) { meta = { - mtime: this.lastResolvedDiskStat.mtime, - size: this.lastResolvedDiskStat.size, - etag: this.lastResolvedDiskStat.etag, + mtime: this.lastResolvedFileStat.mtime, + size: this.lastResolvedFileStat.size, + etag: this.lastResolvedFileStat.etag, orphaned: this.inOrphanMode }; } @@ -345,8 +345,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil let etag: string | undefined; if (forceReadFromDisk) { etag = ETAG_DISABLED; // disable ETag if we enforce to read from disk - } else if (this.lastResolvedDiskStat) { - etag = this.lastResolvedDiskStat.etag; // otherwise respect etag to support caching + } else if (this.lastResolvedFileStat) { + etag = this.lastResolvedFileStat.etag; // otherwise respect etag to support caching } // Ensure to track the versionId before doing a long running operation @@ -402,7 +402,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.logService.trace('load() - resolved content', this.resource); // Update our resolved disk stat model - this.updateLastResolvedDiskStat({ + this.updateLastResolvedFileStat({ resource: this.resource, name: content.name, mtime: content.mtime, @@ -720,12 +720,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Save to Disk // mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering) this.logService.trace(`doSave(${versionId}) - before write()`, this.resource); - return this.saveSequentializer.setPending(newVersionId, this.textFileService.write(this.lastResolvedDiskStat.resource, this.createSnapshot(), { + return this.saveSequentializer.setPending(newVersionId, this.textFileService.write(this.lastResolvedFileStat.resource, this.createSnapshot(), { overwriteReadonly: options.overwriteReadonly, overwriteEncoding: options.overwriteEncoding, - mtime: this.lastResolvedDiskStat.mtime, + mtime: this.lastResolvedFileStat.mtime, encoding: this.getEncoding(), - etag: this.lastResolvedDiskStat.etag, + etag: this.lastResolvedFileStat.etag, writeElevated: options.writeElevated }).then(stat => { this.logService.trace(`doSave(${versionId}) - after write()`, this.resource); @@ -739,7 +739,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } // Updated resolved stat with updated stat - this.updateLastResolvedDiskStat(stat); + this.updateLastResolvedFileStat(stat); // Cancel any content change event promises as they are no longer valid this.contentChangeEventScheduler.cancel(); @@ -857,14 +857,14 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return Promise.resolve(); } - return this.saveSequentializer.setPending(versionId, this.textFileService.write(this.lastResolvedDiskStat.resource, this.createSnapshot(), { - mtime: this.lastResolvedDiskStat.mtime, + return this.saveSequentializer.setPending(versionId, this.textFileService.write(this.lastResolvedFileStat.resource, this.createSnapshot(), { + mtime: this.lastResolvedFileStat.mtime, encoding: this.getEncoding(), - etag: this.lastResolvedDiskStat.etag + etag: this.lastResolvedFileStat.etag }).then(stat => { // Updated resolved stat with updated stat since touching it might have changed mtime - this.updateLastResolvedDiskStat(stat); + this.updateLastResolvedFileStat(stat); // Emit File Saved Event this._onDidStateChange.fire(StateChange.SAVED); @@ -907,18 +907,18 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } } - private updateLastResolvedDiskStat(newVersionOnDiskStat: IFileStatWithMetadata): void { + private updateLastResolvedFileStat(newFileStat: IFileStatWithMetadata): void { // First resolve - just take - if (!this.lastResolvedDiskStat) { - this.lastResolvedDiskStat = newVersionOnDiskStat; + if (!this.lastResolvedFileStat) { + this.lastResolvedFileStat = newFileStat; } // Subsequent resolve - make sure that we only assign it if the mtime is equal or has advanced. // This prevents race conditions from loading and saving. If a save comes in late after a revert // was called, the mtime could be out of sync. - else if (this.lastResolvedDiskStat.mtime <= newVersionOnDiskStat.mtime) { - this.lastResolvedDiskStat = newVersionOnDiskStat; + else if (this.lastResolvedFileStat.mtime <= newFileStat.mtime) { + this.lastResolvedFileStat = newFileStat; } } @@ -1027,7 +1027,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } isReadonly(): boolean { - return !!(this.lastResolvedDiskStat && this.lastResolvedDiskStat.isReadonly); + return !!(this.lastResolvedFileStat && this.lastResolvedFileStat.isReadonly); } isDisposed(): boolean { @@ -1039,7 +1039,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } getStat(): IFileStatWithMetadata { - return this.lastResolvedDiskStat; + return this.lastResolvedFileStat; } dispose(): void {