Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

auto save - make it language overridable and add more settings #200326

Merged
merged 11 commits into from Dec 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 23 additions & 19 deletions src/vs/platform/files/common/files.ts
Expand Up @@ -1453,25 +1453,29 @@ export interface IGlobPatterns {
}

export interface IFilesConfiguration {
files: {
associations: { [filepattern: string]: string };
exclude: IExpression;
watcherExclude: IGlobPatterns;
watcherInclude: string[];
encoding: string;
autoGuessEncoding: boolean;
defaultLanguage: string;
trimTrailingWhitespace: boolean;
autoSave: string;
autoSaveDelay: number;
eol: string;
enableTrash: boolean;
hotExit: string;
saveConflictResolution: 'askUser' | 'overwriteFileOnDisk';
readonlyInclude: IGlobPatterns;
readonlyExclude: IGlobPatterns;
readonlyFromPermissions: boolean;
};
files: IFilesConfigurationNode;
}

export interface IFilesConfigurationNode {
associations: { [filepattern: string]: string };
exclude: IExpression;
watcherExclude: IGlobPatterns;
watcherInclude: string[];
encoding: string;
autoGuessEncoding: boolean;
defaultLanguage: string;
trimTrailingWhitespace: boolean;
autoSave: string;
autoSaveDelay: number;
autoSaveWorkspaceFilesOnly: boolean;
autoSaveWhenNoErrors: boolean;
eol: string;
enableTrash: boolean;
hotExit: string;
saveConflictResolution: 'askUser' | 'overwriteFileOnDisk';
readonlyInclude: IGlobPatterns;
readonlyExclude: IGlobPatterns;
readonlyFromPermissions: boolean;
}

//#endregion
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/browser/parts/editor/editorActions.ts
Expand Up @@ -605,14 +605,14 @@ abstract class AbstractCloseAllAction extends Action2 {

// Editor will be saved on focus change when a
// dialog appears, so just track that separate
else if (filesConfigurationService.getAutoSaveMode() === AutoSaveMode.ON_FOCUS_CHANGE && !editor.hasCapability(EditorInputCapabilities.Untitled)) {
else if (filesConfigurationService.getAutoSaveMode(editor) === AutoSaveMode.ON_FOCUS_CHANGE && !editor.hasCapability(EditorInputCapabilities.Untitled)) {
dirtyAutoSaveOnFocusChangeEditors.add({ editor, groupId });
}

// Windows, Linux: editor will be saved on window change
// when a native dialog appears, so just track that separate
// (see https://github.com/microsoft/vscode/issues/134250)
else if ((isNative && (isWindows || isLinux)) && filesConfigurationService.getAutoSaveMode() === AutoSaveMode.ON_WINDOW_CHANGE && !editor.hasCapability(EditorInputCapabilities.Untitled)) {
else if ((isNative && (isWindows || isLinux)) && filesConfigurationService.getAutoSaveMode(editor) === AutoSaveMode.ON_WINDOW_CHANGE && !editor.hasCapability(EditorInputCapabilities.Untitled)) {
dirtyAutoSaveOnWindowChangeEditors.add({ editor, groupId });
}

Expand Down
86 changes: 39 additions & 47 deletions src/vs/workbench/browser/parts/editor/editorAutoSave.ts
Expand Up @@ -5,7 +5,7 @@

import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { Disposable, DisposableStore, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import { IFilesConfigurationService, AutoSaveMode, IAutoSaveConfiguration } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { SaveReason, IEditorIdentifier, GroupIdentifier, ISaveOptions, EditorInputCapabilities } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
Expand All @@ -18,7 +18,6 @@ import { ILogService } from 'vs/platform/log/common/log';
export class EditorAutoSave extends Disposable implements IWorkbenchContribution {

// Auto save: after delay
private autoSaveAfterDelay: number | undefined;
private readonly pendingAutoSavesAfterDelay = new Map<IWorkingCopy, IDisposable>();

// Auto save: focus change & window change
Expand All @@ -36,9 +35,6 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution
) {
super();

// Figure out initial auto save config
this.onAutoSaveConfigurationChange(filesConfigurationService.getAutoSaveConfiguration(), false);

// Fill in initial dirty working copies
for (const dirtyWorkingCopy of this.workingCopyService.dirtyWorkingCopies) {
this.onDidRegister(dirtyWorkingCopy);
Expand All @@ -51,7 +47,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution
this._register(this.hostService.onDidChangeFocus(focused => this.onWindowFocusChange(focused)));
this._register(this.hostService.onDidChangeActiveWindow(() => this.onActiveWindowChange()));
this._register(this.editorService.onDidActiveEditorChange(() => this.onDidActiveEditorChange()));
this._register(this.filesConfigurationService.onAutoSaveConfigurationChange(config => this.onAutoSaveConfigurationChange(config, true)));
this._register(this.filesConfigurationService.onDidChangeAutoSaveConfiguration(() => this.onDidChangeAutoSaveConfiguration()));

// Working Copy events
this._register(this.workingCopyService.onDidRegister(workingCopy => this.onDidRegister(workingCopy)));
Expand Down Expand Up @@ -95,57 +91,52 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution
}

private maybeTriggerAutoSave(reason: SaveReason, editorIdentifier?: IEditorIdentifier): void {
if (editorIdentifier?.editor.isReadonly() || editorIdentifier?.editor.hasCapability(EditorInputCapabilities.Untitled)) {
return; // no auto save for readonly or untitled editors
}
if (editorIdentifier) {
if (editorIdentifier.editor.isReadonly() || editorIdentifier.editor.hasCapability(EditorInputCapabilities.Untitled)) {
return; // no auto save for readonly or untitled editors
}

// Determine if we need to save all. In case of a window focus change we also save if
// auto save mode is configured to be ON_FOCUS_CHANGE (editor focus change)
const mode = this.filesConfigurationService.getAutoSaveMode();
if (
(reason === SaveReason.WINDOW_CHANGE && (mode === AutoSaveMode.ON_FOCUS_CHANGE || mode === AutoSaveMode.ON_WINDOW_CHANGE)) ||
(reason === SaveReason.FOCUS_CHANGE && mode === AutoSaveMode.ON_FOCUS_CHANGE)
) {
this.logService.trace(`[editor auto save] triggering auto save with reason ${reason}`);
// Determine if we need to save all. In case of a window focus change we also save if
// auto save mode is configured to be ON_FOCUS_CHANGE (editor focus change)
const mode = this.filesConfigurationService.getAutoSaveMode(editorIdentifier.editor);
if (
(reason === SaveReason.WINDOW_CHANGE && (mode === AutoSaveMode.ON_FOCUS_CHANGE || mode === AutoSaveMode.ON_WINDOW_CHANGE)) ||
(reason === SaveReason.FOCUS_CHANGE && mode === AutoSaveMode.ON_FOCUS_CHANGE)
) {
this.logService.trace(`[editor auto save] triggering auto save with reason ${reason}`);

if (editorIdentifier) {
this.editorService.save(editorIdentifier, { reason });
} else {
this.saveAllDirty({ reason });
}
} else {
this.saveAllDirtyAutoSaveables({ reason });
}
}

private onAutoSaveConfigurationChange(config: IAutoSaveConfiguration, fromEvent: boolean): void {

// Update auto save after delay config
this.autoSaveAfterDelay = (typeof config.autoSaveDelay === 'number') && config.autoSaveDelay >= 0 ? config.autoSaveDelay : undefined;
private onDidChangeAutoSaveConfiguration(): void {

// Trigger a save-all when auto save is enabled
if (fromEvent) {
let reason: SaveReason | undefined = undefined;
switch (this.filesConfigurationService.getAutoSaveMode()) {
case AutoSaveMode.ON_FOCUS_CHANGE:
reason = SaveReason.FOCUS_CHANGE;
break;
case AutoSaveMode.ON_WINDOW_CHANGE:
reason = SaveReason.WINDOW_CHANGE;
break;
case AutoSaveMode.AFTER_SHORT_DELAY:
case AutoSaveMode.AFTER_LONG_DELAY:
reason = SaveReason.AUTO;
break;
}
let reason: SaveReason | undefined = undefined;
switch (this.filesConfigurationService.getAutoSaveMode(undefined)) {
case AutoSaveMode.ON_FOCUS_CHANGE:
reason = SaveReason.FOCUS_CHANGE;
break;
case AutoSaveMode.ON_WINDOW_CHANGE:
reason = SaveReason.WINDOW_CHANGE;
break;
case AutoSaveMode.AFTER_SHORT_DELAY:
case AutoSaveMode.AFTER_LONG_DELAY:
reason = SaveReason.AUTO;
break;
}

if (reason) {
this.saveAllDirty({ reason });
}
if (reason) {
this.saveAllDirtyAutoSaveables({ reason });
}
}

private saveAllDirty(options?: ISaveOptions): void {
private saveAllDirtyAutoSaveables(options?: ISaveOptions): void {
for (const workingCopy of this.workingCopyService.dirtyWorkingCopies) {
if (!(workingCopy.capabilities & WorkingCopyCapabilities.Untitled)) {
if (!(workingCopy.capabilities & WorkingCopyCapabilities.Untitled) && this.filesConfigurationService.getAutoSaveMode(workingCopy.resource) !== AutoSaveMode.OFF) {
workingCopy.save(options);
}
}
Expand Down Expand Up @@ -179,7 +170,8 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution
}

private scheduleAutoSave(workingCopy: IWorkingCopy): void {
if (typeof this.autoSaveAfterDelay !== 'number') {
const autoSaveAfterDelay = this.filesConfigurationService.getAutoSaveConfiguration(workingCopy.resource).autoSaveDelay;
if (typeof autoSaveAfterDelay !== 'number') {
return; // auto save after delay must be enabled
}

Expand All @@ -190,7 +182,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution
// Clear any running auto save operation
this.discardAutoSave(workingCopy);

this.logService.trace(`[editor auto save] scheduling auto save after ${this.autoSaveAfterDelay}ms`, workingCopy.resource.toString(), workingCopy.typeId);
this.logService.trace(`[editor auto save] scheduling auto save after ${autoSaveAfterDelay}ms`, workingCopy.resource.toString(), workingCopy.typeId);

// Schedule new auto save
const handle = setTimeout(() => {
Expand All @@ -199,11 +191,11 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution
this.discardAutoSave(workingCopy);

// Save if dirty
if (workingCopy.isDirty()) {
if (workingCopy.isDirty() && this.filesConfigurationService.getAutoSaveMode(workingCopy.resource) !== AutoSaveMode.OFF) {
this.logService.trace(`[editor auto save] running auto save`, workingCopy.resource.toString(), workingCopy.typeId);
workingCopy.save({ reason: SaveReason.AUTO });
}
}, this.autoSaveAfterDelay);
}, autoSaveAfterDelay);

// Keep in map for disposal as needed
this.pendingAutoSavesAfterDelay.set(workingCopy, toDisposable(() => {
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/browser/parts/editor/editorGroupView.ts
Expand Up @@ -1574,7 +1574,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {

// Auto-save on focus change: save, because a dialog would steal focus
// (see https://github.com/microsoft/vscode/issues/108752)
if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.ON_FOCUS_CHANGE) {
if (this.filesConfigurationService.getAutoSaveMode(editor) === AutoSaveMode.ON_FOCUS_CHANGE) {
autoSave = true;
confirmation = ConfirmResult.SAVE;
saveReason = SaveReason.FOCUS_CHANGE;
Expand All @@ -1583,7 +1583,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
// Auto-save on window change: save, because on Windows and Linux, a
// native dialog triggers the window focus change
// (see https://github.com/microsoft/vscode/issues/134250)
else if ((isNative && (isWindows || isLinux)) && this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.ON_WINDOW_CHANGE) {
else if ((isNative && (isWindows || isLinux)) && this.filesConfigurationService.getAutoSaveMode(editor) === AutoSaveMode.ON_WINDOW_CHANGE) {
autoSave = true;
confirmation = ConfirmResult.SAVE;
saveReason = SaveReason.WINDOW_CHANGE;
Expand Down
Expand Up @@ -318,7 +318,7 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements
// and it could result in bad UX where an editor can be closed even though
// it shows up as dirty and has not finished saving yet.

if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) {
if (this.filesConfigurationService.getAutoSaveMode(this) === AutoSaveMode.AFTER_SHORT_DELAY) {
return true; // a short auto save is configured, treat this as being saved
}

Expand Down
Expand Up @@ -69,7 +69,7 @@ export class TextFileEditorTracker extends Disposable implements IWorkbenchContr
return false; // resource must not be pending to save
}

if (resource.scheme !== Schemas.untitled && this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY && !fileModel?.hasState(TextFileEditorModelState.ERROR)) {
if (resource.scheme !== Schemas.untitled && this.filesConfigurationService.getAutoSaveMode(resource) === AutoSaveMode.AFTER_SHORT_DELAY && !fileModel?.hasState(TextFileEditorModelState.ERROR)) {
// leave models auto saved after short delay unless
// the save resulted in an error and not for untitled
// that are not auto-saved anyway
Expand Down
18 changes: 16 additions & 2 deletions src/vs/workbench/contrib/files/browser/files.contribution.ts
Expand Up @@ -250,13 +250,27 @@ configurationRegistry.registerConfiguration({
nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onWindowChange' }, "An editor with changes is automatically saved when the window loses focus.")
],
'default': isWeb ? AutoSaveConfiguration.AFTER_DELAY : AutoSaveConfiguration.OFF,
'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSave' }, "Controls [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors that have unsaved changes.", AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE, AutoSaveConfiguration.AFTER_DELAY)
'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSave' }, "Controls [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors that have unsaved changes.", AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE, AutoSaveConfiguration.AFTER_DELAY),
scope: ConfigurationScope.LANGUAGE_OVERRIDABLE
},
'files.autoSaveDelay': {
'type': 'number',
'default': 1000,
'minimum': 0,
'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveDelay' }, "Controls the delay in milliseconds after which an editor with unsaved changes is saved automatically. Only applies when `#files.autoSave#` is set to `{0}`.", AutoSaveConfiguration.AFTER_DELAY)
'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveDelay' }, "Controls the delay in milliseconds after which an editor with unsaved changes is saved automatically. Only applies when `#files.autoSave#` is set to `{0}`.", AutoSaveConfiguration.AFTER_DELAY),
scope: ConfigurationScope.LANGUAGE_OVERRIDABLE
},
'files.autoSaveWorkspaceFilesOnly': {
'type': 'boolean',
'default': false,
'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveWorkspaceFilesOnly' }, "When enabled, will limit [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors to files that are inside the opened workspace. Only applies when `#files.autoSave#` is enabled."),
scope: ConfigurationScope.LANGUAGE_OVERRIDABLE
},
'files.autoSaveWhenNoErrors': {
'type': 'boolean',
'default': false,
'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveWhenNoErrors' }, "When enabled, will limit [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors to files that have no errors reported in them. Only applies when `#files.autoSave#` is enabled."),
scope: ConfigurationScope.LANGUAGE_OVERRIDABLE
},
'files.watcherExclude': {
'type': 'object',
Expand Down
Expand Up @@ -460,7 +460,7 @@ export class OpenEditorsView extends ViewPane {
private updateDirtyIndicator(workingCopy?: IWorkingCopy): void {
if (workingCopy) {
const gotDirty = workingCopy.isDirty();
if (gotDirty && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled) && this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) {
if (gotDirty && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled) && this.filesConfigurationService.getAutoSaveMode(workingCopy.resource) === AutoSaveMode.AFTER_SHORT_DELAY) {
return; // do not indicate dirty of working copies that are auto saved after short delay
}
}
Expand Down
Expand Up @@ -42,7 +42,7 @@ export class DirtyFilesIndicator extends Disposable implements IWorkbenchContrib

private onWorkingCopyDidChangeDirty(workingCopy: IWorkingCopy): void {
const gotDirty = workingCopy.isDirty();
if (gotDirty && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled) && this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) {
if (gotDirty && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled) && this.filesConfigurationService.getAutoSaveMode(workingCopy.resource) === AutoSaveMode.AFTER_SHORT_DELAY) {
return; // do not indicate dirty of working copies that are auto saved after short delay
}

Expand Down
Expand Up @@ -7,7 +7,7 @@ import * as assert from 'assert';
import { Event } from 'vs/base/common/event';
import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { TestFilesConfigurationService, workbenchInstantiationService, TestServiceAccessor, registerTestFileEditor, createEditorPart, TestEnvironmentService, TestFileService } from 'vs/workbench/test/browser/workbenchTestServices';
import { TestFilesConfigurationService, workbenchInstantiationService, TestServiceAccessor, registerTestFileEditor, createEditorPart, TestEnvironmentService, TestFileService, TestTextResourceConfigurationService } from 'vs/workbench/test/browser/workbenchTestServices';
import { ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { DisposableStore } from 'vs/base/common/lifecycle';
Expand All @@ -21,7 +21,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor';
import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace';
import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices';
import { TestContextService, TestMarkerService } from 'vs/workbench/test/common/workbenchTestServices';
import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService';
import { IAccessibleNotificationService } from 'vs/platform/accessibility/common/accessibility';
import { TestAccessibleNotificationService } from 'vs/workbench/contrib/accessibility/browser/accessibleNotificationService';
Expand Down Expand Up @@ -51,7 +51,9 @@ suite('EditorAutoSave', () => {
new TestContextService(TestWorkspace),
TestEnvironmentService,
disposables.add(new UriIdentityService(disposables.add(new TestFileService()))),
disposables.add(new TestFileService())
disposables.add(new TestFileService()),
new TestMarkerService(),
new TestTextResourceConfigurationService(configurationService)
)));

const part = await createEditorPart(instantiationService, disposables);
Expand Down