Skip to content

Commit e3114f4

Browse files
author
Benjamin Pasero
committed
introduce early version of working copy service
Adopt it for untitled and text file models. Use it for indicating dirty count in explorer and document edited on macOS.
1 parent a660498 commit e3114f4

19 files changed

Lines changed: 617 additions & 255 deletions

File tree

src/vs/workbench/common/editor/textEditorModel.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import { withUndefinedAsNull } from 'vs/base/common/types';
1717
* The base text editor model leverages the code editor model. This class is only intended to be subclassed and not instantiated.
1818
*/
1919
export abstract class BaseTextEditorModel extends EditorModel implements ITextEditorModel, IModeSupport {
20+
2021
protected textEditorModelHandle: URI | null = null;
22+
2123
private createdEditorModel: boolean | undefined;
2224

2325
private readonly modelDisposeListener = this._register(new MutableDisposable());

src/vs/workbench/common/editor/untitledTextEditorModel.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/res
1616
import { ITextBufferFactory } from 'vs/editor/common/model';
1717
import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
1818
import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
19+
import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService';
1920

20-
export class UntitledTextEditorModel extends BaseTextEditorModel implements IEncodingSupport {
21+
export class UntitledTextEditorModel extends BaseTextEditorModel implements IEncodingSupport, IWorkingCopy {
2122

2223
static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY;
2324

@@ -30,33 +31,33 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IEnc
3031
private readonly _onDidChangeEncoding: Emitter<void> = this._register(new Emitter<void>());
3132
readonly onDidChangeEncoding: Event<void> = this._onDidChangeEncoding.event;
3233

33-
private dirty: boolean = false;
34-
private versionId: number = 0;
35-
private readonly contentChangeEventScheduler: RunOnceScheduler;
34+
readonly capabilities = 0;
35+
36+
private dirty = false;
37+
private versionId = 0;
38+
private readonly contentChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidChangeContent.fire(), UntitledTextEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY));
3639
private configuredEncoding?: string;
3740

3841
constructor(
3942
private readonly preferredMode: string | undefined,
40-
private readonly resource: URI,
41-
private _hasAssociatedFilePath: boolean,
43+
public readonly resource: URI,
44+
public readonly hasAssociatedFilePath: boolean,
4245
private readonly initialValue: string | undefined,
4346
private preferredEncoding: string | undefined,
4447
@IModeService modeService: IModeService,
4548
@IModelService modelService: IModelService,
4649
@IBackupFileService private readonly backupFileService: IBackupFileService,
47-
@ITextResourceConfigurationService private readonly configurationService: ITextResourceConfigurationService
50+
@ITextResourceConfigurationService private readonly configurationService: ITextResourceConfigurationService,
51+
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService
4852
) {
4953
super(modelService, modeService);
5054

51-
this.contentChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidChangeContent.fire(), UntitledTextEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY));
55+
// Make known to working copy service
56+
this._register(this.workingCopyService.registerWorkingCopy(this));
5257

5358
this.registerListeners();
5459
}
5560

56-
get hasAssociatedFilePath(): boolean {
57-
return this._hasAssociatedFilePath;
58-
}
59-
6061
private registerListeners(): void {
6162

6263
// Config Changes
@@ -147,7 +148,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IEnc
147148
}
148149

149150
// untitled associated to file path are dirty right away as well as untitled with content
150-
this.setDirty(this._hasAssociatedFilePath || !!backup || !!this.initialValue);
151+
this.setDirty(this.hasAssociatedFilePath || !!backup || !!this.initialValue);
151152

152153
let untitledContents: ITextBufferFactory;
153154
if (backup) {
@@ -190,7 +191,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IEnc
190191

191192
// mark the untitled text editor as non-dirty once its content becomes empty and we do
192193
// not have an associated path set. we never want dirty indicator in that case.
193-
if (!this._hasAssociatedFilePath && this.textEditorModel.getLineCount() === 1 && this.textEditorModel.getLineContent(1) === '') {
194+
if (!this.hasAssociatedFilePath && this.textEditorModel.getLineCount() === 1 && this.textEditorModel.getLineContent(1) === '') {
194195
this.setDirty(false);
195196
}
196197

src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri';
88
import * as resources from 'vs/base/common/resources';
99
import { IEditorViewState } from 'vs/editor/common/editorCommon';
1010
import { toResource, SideBySideEditorInput, IWorkbenchEditorConfiguration, SideBySideEditor as SideBySideEditorChoice } from 'vs/workbench/common/editor';
11-
import { ITextFileService, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
11+
import { ITextFileService, ITextFileEditorModel, TextFileModelChangeEvent, ModelState } from 'vs/workbench/services/textfile/common/textfiles';
1212
import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files';
1313
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
1414
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
@@ -61,6 +61,9 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
6161
// Update editors from disk changes
6262
this._register(this.fileService.onFileChanges(e => this.onFileChanges(e)));
6363

64+
// Open editors from dirty text file models
65+
this._register(this.textFileService.models.onModelsDirty(e => this.onTextFilesDirty(e)));
66+
6467
// Editor changing
6568
this._register(this.editorService.onDidVisibleEditorsChange(() => this.handleOutOfWorkspaceWatchers()));
6669

@@ -359,6 +362,31 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
359362
});
360363
}
361364

365+
private onTextFilesDirty(e: readonly TextFileModelChangeEvent[]): void {
366+
367+
// If files become dirty but are not opened, we open it in the background unless there are pending to be saved
368+
this.doOpenDirtyResources(distinct(e.filter(e => {
369+
370+
// Only dirty models that are not PENDING_SAVE
371+
const model = this.textFileService.models.get(e.resource);
372+
const shouldOpen = model?.isDirty() && !model.hasState(ModelState.PENDING_SAVE);
373+
374+
// Only if not open already
375+
return shouldOpen && !this.editorService.isOpen({ resource: e.resource });
376+
}).map(e => e.resource), r => r.toString()));
377+
}
378+
379+
private doOpenDirtyResources(resources: URI[]): void {
380+
381+
// Open
382+
this.editorService.openEditors(resources.map(resource => {
383+
return {
384+
resource,
385+
options: { inactive: true, pinned: true, preserveFocus: true }
386+
};
387+
}));
388+
}
389+
362390
dispose(): void {
363391
super.dispose();
364392

src/vs/workbench/contrib/files/browser/files.contribution.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { ExplorerService } from 'vs/workbench/contrib/files/common/explorerServi
3838
import { SUPPORTED_ENCODINGS } from 'vs/workbench/services/textfile/common/textfiles';
3939
import { Schemas } from 'vs/base/common/network';
4040
import { WorkspaceWatcher } from 'vs/workbench/contrib/files/common/workspaceWatcher';
41+
import { DirtyFilesIndicator } from 'vs/workbench/contrib/files/common/dirtyFilesIndicator';
4142

4243
// Viewlet Action
4344
export class OpenExplorerViewletAction extends ShowViewletAction {
@@ -170,9 +171,12 @@ Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).regi
170171
// Register uri display for file uris
171172
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FileUriLabelContribution, LifecyclePhase.Starting);
172173

173-
// Workspace Watcher
174+
// Register Workspace Watcher
174175
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkspaceWatcher, LifecyclePhase.Restored);
175176

177+
// Register Dirty Files Indicator
178+
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DirtyFilesIndicator, LifecyclePhase.Starting);
179+
176180
// Configuration
177181
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
178182

src/vs/workbench/contrib/files/browser/files.web.contribution.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileE
1010
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
1111
import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
1212
import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor';
13-
import { DirtyFilesTracker } from 'vs/workbench/contrib/files/common/dirtyFilesTracker';
14-
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
15-
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
1613

1714
// Register file editor
1815
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
@@ -25,6 +22,3 @@ Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
2522
new SyncDescriptor<EditorInput>(FileEditorInput)
2623
]
2724
);
28-
29-
// Register Dirty Files Tracker
30-
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DirtyFilesTracker, LifecyclePhase.Starting);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as nls from 'vs/nls';
7+
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
8+
import { VIEWLET_ID } from 'vs/workbench/contrib/files/common/files';
9+
import { ITextFileService, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles';
10+
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
11+
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
12+
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
13+
import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService';
14+
15+
export class DirtyFilesIndicator extends Disposable implements IWorkbenchContribution {
16+
private readonly badgeHandle = this._register(new MutableDisposable());
17+
18+
private lastKnownDirtyCount: number | undefined;
19+
20+
private get hasDirtyCount(): boolean {
21+
return typeof this.lastKnownDirtyCount === 'number' && this.lastKnownDirtyCount > 0;
22+
}
23+
24+
constructor(
25+
@ITextFileService private readonly textFileService: ITextFileService,
26+
@ILifecycleService private readonly lifecycleService: ILifecycleService,
27+
@IActivityService private readonly activityService: IActivityService,
28+
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService
29+
) {
30+
super();
31+
32+
this.registerListeners();
33+
}
34+
35+
private registerListeners(): void {
36+
37+
// Working copy dirty indicator
38+
this._register(this.workingCopyService.onDidChangeDirty(c => this.onWorkingCopyDidChangeDirty(c)));
39+
40+
// Lifecycle
41+
this.lifecycleService.onShutdown(this.dispose, this);
42+
}
43+
44+
private onWorkingCopyDidChangeDirty(copy: IWorkingCopy): void {
45+
if (!!(copy.capabilities & WorkingCopyCapabilities.AutoSave) && this.textFileService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) {
46+
return; // do not indicate changes to working copies that are auto saved after short delay
47+
}
48+
49+
const gotDirty = copy.isDirty();
50+
if (gotDirty || this.hasDirtyCount) {
51+
this.updateActivityBadge();
52+
}
53+
}
54+
55+
private updateActivityBadge(): void {
56+
const dirtyCount = this.workingCopyService.dirtyCount;
57+
this.lastKnownDirtyCount = dirtyCount;
58+
59+
// Indicate dirty count in badge if any
60+
if (dirtyCount > 0) {
61+
this.badgeHandle.value = this.activityService.showActivity(
62+
VIEWLET_ID,
63+
new NumberBadge(dirtyCount, num => num === 1 ? nls.localize('dirtyFile', "1 unsaved file") : nls.localize('dirtyFiles', "{0} unsaved files", dirtyCount)),
64+
'explorer-viewlet-label'
65+
);
66+
} else {
67+
this.badgeHandle.clear();
68+
}
69+
}
70+
}

src/vs/workbench/contrib/files/common/dirtyFilesTracker.ts

Lines changed: 0 additions & 113 deletions
This file was deleted.

0 commit comments

Comments
 (0)