diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 83ed4656a1bf27c055f973ffae69e9dc2a15284f..c00e9b17fefc6538578ae4bcdc29e8563aa3e18b 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 a3ffb18b605af6ea80cc1d0b5391333a93301fed..73b686fa6d62657bfd758b026055784e0ef80491 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 f4fdf7261cf96811b2340ba1e817d0f62f3242be..7de229cf5837ff0fd3ac220a966bff7f58da04fd 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 6595d5b8ed002a4ebeeb4ea55e3ec61c64703327..8d4238c59fa7f4ec3ab99d78daddbdaeab548487 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 8ab10dc191a51713465fb72577cb6def64ad8dbc..1ab5da03cf5f76fa59d566e219fae8efc4e60f60 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 45ba8a4375c7afe116792412d032f086862dd5a8..8b6750df496e0c25b20361263128957737000416 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 {