From 46c696ac6b1727875e820c78797ec7370b93f6d1 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 9 Dec 2019 06:58:06 +0100 Subject: [PATCH] Option to disable "Failed to save '...': The content on disk is newer." check (fixes #66035) --- src/vs/platform/files/common/files.ts | 1 + .../files/browser/files.contribution.ts | 8 ++- .../files/browser/textFileSaveErrorHandler.ts | 56 ++++++++++++++----- .../common/filesConfigurationService.ts | 7 +++ .../textfile/common/textFileEditorModel.ts | 2 +- .../services/textfile/common/textfiles.ts | 1 + 6 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 83ed4656a1b..c00e9b17fef 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -796,6 +796,7 @@ export interface IFilesConfiguration { eol: string; enableTrash: boolean; hotExit: string; + preventSaveConflicts: boolean; }; } diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index a3ffb18b605..73b686fa6d6 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -333,10 +333,16 @@ configurationRegistry.registerConfiguration({ 'markdownDescription': nls.localize('maxMemoryForLargeFilesMB', "Controls the memory available to VS Code after restart when trying to open large files. Same effect as specifying `--max-memory=NEWSIZE` on the command line."), included: platform.isNative }, + 'files.preventSaveConflicts': { + 'type': 'boolean', + 'description': nls.localize('files.preventSaveConflicts', "When enabled, will prevent to save a file that has been changed since it was last edited. Instead, a diff editor is provided to compare the changes and accept or revert them. This setting should only be disabled if you frequently encounter save conflict errors and may result in data loss if used without caution."), + 'default': true, + 'scope': ConfigurationScope.RESOURCE + }, 'files.simpleDialog.enable': { 'type': 'boolean', 'description': nls.localize('files.simpleDialog.enable', "Enables the simple file dialog. The simple file dialog replaces the system file dialog when enabled."), - 'default': false, + 'default': false } } }); diff --git a/src/vs/workbench/contrib/files/browser/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/textFileSaveErrorHandler.ts index f4fdf7261cf..7de229cf583 100644 --- a/src/vs/workbench/contrib/files/browser/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/textFileSaveErrorHandler.ts @@ -21,9 +21,7 @@ import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorIn import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; 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'; -import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { INotificationService, INotificationHandle, INotificationActions, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -33,6 +31,8 @@ import { Event } from 'vs/base/common/event'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { isWindows } from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { SaveReason } from 'vs/workbench/common/editor'; export const CONFLICT_RESOLUTION_CONTEXT = 'saveConflictResolutionContext'; export const CONFLICT_RESOLUTION_SCHEME = 'conflictResolution'; @@ -126,9 +126,12 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa // 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 of the file is newer. Please compare your version with the file contents.", 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 or overwrite the content of the file with your changes.", basename(resource)); primaryActions.push(this.instantiationService.createInstance(ResolveSaveConflictAction, model)); + primaryActions.push(this.instantiationService.createInstance(SaveIgnoreModifiedSinceAction, model)); + + secondaryActions.push(this.instantiationService.createInstance(ConfigureSaveConflictAction)); } } @@ -278,7 +281,8 @@ class SaveElevatedAction extends Action { if (!this.model.isDisposed()) { this.model.save({ writeElevated: true, - overwriteReadonly: this.triedToMakeWriteable + overwriteReadonly: this.triedToMakeWriteable, + reason: SaveReason.EXPLICIT }); } @@ -296,17 +300,48 @@ class OverwriteReadonlyAction extends Action { run(): Promise { if (!this.model.isDisposed()) { - this.model.save({ overwriteReadonly: true }); + this.model.save({ overwriteReadonly: true, reason: SaveReason.EXPLICIT }); + } + + return Promise.resolve(true); + } +} + +class SaveIgnoreModifiedSinceAction extends Action { + + constructor( + private model: ITextFileEditorModel + ) { + super('workbench.files.action.saveIgnoreModifiedSince', nls.localize('overwrite', "Overwrite")); + } + + run(): Promise { + if (!this.model.isDisposed()) { + this.model.save({ ignoreModifiedSince: true, reason: SaveReason.EXPLICIT }); } return Promise.resolve(true); } } +class ConfigureSaveConflictAction extends Action { + + constructor( + @IPreferencesService private readonly preferencesService: IPreferencesService + ) { + super('workbench.files.action.configureSaveConflict', nls.localize('configure', "Configure")); + } + + run(): Promise { + this.preferencesService.openSettings(undefined, 'files.preventSaveConflicts'); + + return Promise.resolve(true); + } +} + export const acceptLocalChangesCommand = async (accessor: ServicesAccessor, resource: URI) => { const editorService = accessor.get(IEditorService); const resolverService = accessor.get(ITextModelService); - const modelService = accessor.get(IModelService); const control = editorService.activeControl; if (!control) { @@ -318,18 +353,11 @@ export const acceptLocalChangesCommand = async (accessor: ServicesAccessor, reso const reference = await resolverService.createModelReference(resource); const model = reference.object as IResolvedTextFileEditorModel; - const localModelSnapshot = model.createSnapshot(); clearPendingResolveSaveConflictMessages(); // hide any previously shown message about how to use these actions - // Revert to be able to save - await model.revert(); - - // Restore user value (without loosing undo stack) - modelService.updateModel(model.textEditorModel, createTextBufferFactoryFromSnapshot(localModelSnapshot)); - // Trigger save - await model.save(); + await model.save({ ignoreModifiedSince: true, reason: SaveReason.EXPLICIT }); // Reopen file input await editorService.openEditor({ resource: model.resource }, group); diff --git a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts index 6595d5b8ed0..8d4238c59fa 100644 --- a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts +++ b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts @@ -13,6 +13,7 @@ import { IFilesConfiguration, AutoSaveConfiguration, HotExitConfiguration } from import { isUndefinedOrNull } from 'vs/base/common/types'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { equals } from 'vs/base/common/objects'; +import { URI } from 'vs/base/common/uri'; export const AutoSaveAfterShortDelayContext = new RawContextKey('autoSaveAfterShortDelayContext', false); @@ -53,6 +54,8 @@ export interface IFilesConfigurationService { readonly isHotExitEnabled: boolean; readonly hotExitConfiguration: string | undefined; + + preventSaveConflicts(resource: URI): boolean; } export class FilesConfigurationService extends Disposable implements IFilesConfigurationService { @@ -203,6 +206,10 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi get hotExitConfiguration(): string { return this.currentHotExitConfig; } + + preventSaveConflicts(resource: URI): boolean { + return this.configurationService.getValue('files.preventSaveConflicts', { resource }); + } } registerSingleton(IFilesConfigurationService, FilesConfigurationService); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 8ab10dc191a..1ab5da03cf5 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -743,7 +743,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil overwriteEncoding: options.overwriteEncoding, mtime: lastResolvedFileStat.mtime, encoding: this.getEncoding(), - etag: lastResolvedFileStat.etag, + etag: (options.ignoreModifiedSince || !this.filesConfigurationService.preventSaveConflicts(lastResolvedFileStat.resource)) ? ETAG_DISABLED : lastResolvedFileStat.etag, writeElevated: options.writeElevated }).then(stat => { this.logService.trace(`doSave(${versionId}) - after write()`, this.resource); diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 45ba8a4375c..8b6750df496 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -412,6 +412,7 @@ export interface ITextFileSaveOptions extends ISaveOptions { overwriteReadonly?: boolean; overwriteEncoding?: boolean; writeElevated?: boolean; + ignoreModifiedSince?: boolean; } export interface ILoadOptions { -- GitLab