提交 e3114f4c 编写于 作者: B Benjamin Pasero

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.
上级 a660498d
......@@ -17,7 +17,9 @@ import { withUndefinedAsNull } from 'vs/base/common/types';
* The base text editor model leverages the code editor model. This class is only intended to be subclassed and not instantiated.
*/
export abstract class BaseTextEditorModel extends EditorModel implements ITextEditorModel, IModeSupport {
protected textEditorModelHandle: URI | null = null;
private createdEditorModel: boolean | undefined;
private readonly modelDisposeListener = this._register(new MutableDisposable());
......
......@@ -16,8 +16,9 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/res
import { ITextBufferFactory } from 'vs/editor/common/model';
import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService';
export class UntitledTextEditorModel extends BaseTextEditorModel implements IEncodingSupport {
export class UntitledTextEditorModel extends BaseTextEditorModel implements IEncodingSupport, IWorkingCopy {
static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY;
......@@ -30,33 +31,33 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IEnc
private readonly _onDidChangeEncoding: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeEncoding: Event<void> = this._onDidChangeEncoding.event;
private dirty: boolean = false;
private versionId: number = 0;
private readonly contentChangeEventScheduler: RunOnceScheduler;
readonly capabilities = 0;
private dirty = false;
private versionId = 0;
private readonly contentChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidChangeContent.fire(), UntitledTextEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY));
private configuredEncoding?: string;
constructor(
private readonly preferredMode: string | undefined,
private readonly resource: URI,
private _hasAssociatedFilePath: boolean,
public readonly resource: URI,
public readonly hasAssociatedFilePath: boolean,
private readonly initialValue: string | undefined,
private preferredEncoding: string | undefined,
@IModeService modeService: IModeService,
@IModelService modelService: IModelService,
@IBackupFileService private readonly backupFileService: IBackupFileService,
@ITextResourceConfigurationService private readonly configurationService: ITextResourceConfigurationService
@ITextResourceConfigurationService private readonly configurationService: ITextResourceConfigurationService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService
) {
super(modelService, modeService);
this.contentChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidChangeContent.fire(), UntitledTextEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY));
// Make known to working copy service
this._register(this.workingCopyService.registerWorkingCopy(this));
this.registerListeners();
}
get hasAssociatedFilePath(): boolean {
return this._hasAssociatedFilePath;
}
private registerListeners(): void {
// Config Changes
......@@ -147,7 +148,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IEnc
}
// untitled associated to file path are dirty right away as well as untitled with content
this.setDirty(this._hasAssociatedFilePath || !!backup || !!this.initialValue);
this.setDirty(this.hasAssociatedFilePath || !!backup || !!this.initialValue);
let untitledContents: ITextBufferFactory;
if (backup) {
......@@ -190,7 +191,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IEnc
// mark the untitled text editor as non-dirty once its content becomes empty and we do
// not have an associated path set. we never want dirty indicator in that case.
if (!this._hasAssociatedFilePath && this.textEditorModel.getLineCount() === 1 && this.textEditorModel.getLineContent(1) === '') {
if (!this.hasAssociatedFilePath && this.textEditorModel.getLineCount() === 1 && this.textEditorModel.getLineContent(1) === '') {
this.setDirty(false);
}
......
......@@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri';
import * as resources from 'vs/base/common/resources';
import { IEditorViewState } from 'vs/editor/common/editorCommon';
import { toResource, SideBySideEditorInput, IWorkbenchEditorConfiguration, SideBySideEditor as SideBySideEditorChoice } from 'vs/workbench/common/editor';
import { ITextFileService, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
import { ITextFileService, ITextFileEditorModel, TextFileModelChangeEvent, ModelState } from 'vs/workbench/services/textfile/common/textfiles';
import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files';
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
......@@ -61,6 +61,9 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
// Update editors from disk changes
this._register(this.fileService.onFileChanges(e => this.onFileChanges(e)));
// Open editors from dirty text file models
this._register(this.textFileService.models.onModelsDirty(e => this.onTextFilesDirty(e)));
// Editor changing
this._register(this.editorService.onDidVisibleEditorsChange(() => this.handleOutOfWorkspaceWatchers()));
......@@ -359,6 +362,31 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
});
}
private onTextFilesDirty(e: readonly TextFileModelChangeEvent[]): void {
// If files become dirty but are not opened, we open it in the background unless there are pending to be saved
this.doOpenDirtyResources(distinct(e.filter(e => {
// Only dirty models that are not PENDING_SAVE
const model = this.textFileService.models.get(e.resource);
const shouldOpen = model?.isDirty() && !model.hasState(ModelState.PENDING_SAVE);
// Only if not open already
return shouldOpen && !this.editorService.isOpen({ resource: e.resource });
}).map(e => e.resource), r => r.toString()));
}
private doOpenDirtyResources(resources: URI[]): void {
// Open
this.editorService.openEditors(resources.map(resource => {
return {
resource,
options: { inactive: true, pinned: true, preserveFocus: true }
};
}));
}
dispose(): void {
super.dispose();
......
......@@ -38,6 +38,7 @@ import { ExplorerService } from 'vs/workbench/contrib/files/common/explorerServi
import { SUPPORTED_ENCODINGS } from 'vs/workbench/services/textfile/common/textfiles';
import { Schemas } from 'vs/base/common/network';
import { WorkspaceWatcher } from 'vs/workbench/contrib/files/common/workspaceWatcher';
import { DirtyFilesIndicator } from 'vs/workbench/contrib/files/common/dirtyFilesIndicator';
// Viewlet Action
export class OpenExplorerViewletAction extends ShowViewletAction {
......@@ -170,9 +171,12 @@ Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).regi
// Register uri display for file uris
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FileUriLabelContribution, LifecyclePhase.Starting);
// Workspace Watcher
// Register Workspace Watcher
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkspaceWatcher, LifecyclePhase.Restored);
// Register Dirty Files Indicator
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DirtyFilesIndicator, LifecyclePhase.Starting);
// Configuration
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
......
......@@ -10,9 +10,6 @@ import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileE
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor';
import { DirtyFilesTracker } from 'vs/workbench/contrib/files/common/dirtyFilesTracker';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
// Register file editor
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
......@@ -25,6 +22,3 @@ Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
new SyncDescriptor<EditorInput>(FileEditorInput)
]
);
// Register Dirty Files Tracker
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DirtyFilesTracker, LifecyclePhase.Starting);
......@@ -6,25 +6,26 @@
import * as nls from 'vs/nls';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { VIEWLET_ID } from 'vs/workbench/contrib/files/common/files';
import { TextFileModelChangeEvent, ITextFileService, AutoSaveMode, ModelState } from 'vs/workbench/services/textfile/common/textfiles';
import { ITextFileService, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import * as arrays from 'vs/base/common/arrays';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService';
export class DirtyFilesTracker extends Disposable implements IWorkbenchContribution {
private lastKnownDirtyCount: number | undefined;
export class DirtyFilesIndicator extends Disposable implements IWorkbenchContribution {
private readonly badgeHandle = this._register(new MutableDisposable());
private lastKnownDirtyCount: number | undefined;
private get hasDirtyCount(): boolean {
return typeof this.lastKnownDirtyCount === 'number' && this.lastKnownDirtyCount > 0;
}
constructor(
@ITextFileService protected readonly textFileService: ITextFileService,
@ITextFileService private readonly textFileService: ITextFileService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@IEditorService private readonly editorService: IEditorService,
@IActivityService private readonly activityService: IActivityService,
@IUntitledTextEditorService protected readonly untitledTextEditorService: IUntitledTextEditorService
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService
) {
super();
......@@ -33,81 +34,37 @@ export class DirtyFilesTracker extends Disposable implements IWorkbenchContribut
private registerListeners(): void {
// Local text file changes
this._register(this.untitledTextEditorService.onDidChangeDirty(e => this.onUntitledDidChangeDirty(e)));
this._register(this.textFileService.models.onModelsDirty(e => this.onTextFilesDirty(e)));
this._register(this.textFileService.models.onModelsSaved(e => this.onTextFilesSaved(e)));
this._register(this.textFileService.models.onModelsSaveError(e => this.onTextFilesSaveError(e)));
this._register(this.textFileService.models.onModelsReverted(e => this.onTextFilesReverted(e)));
// Working copy dirty indicator
this._register(this.workingCopyService.onDidChangeDirty(c => this.onWorkingCopyDidChangeDirty(c)));
// Lifecycle
this.lifecycleService.onShutdown(this.dispose, this);
}
private get hasDirtyCount(): boolean {
return typeof this.lastKnownDirtyCount === 'number' && this.lastKnownDirtyCount > 0;
}
protected onUntitledDidChangeDirty(resource: URI): void {
const gotDirty = this.untitledTextEditorService.isDirty(resource);
if (gotDirty || this.hasDirtyCount) {
this.updateActivityBadge();
}
}
protected onTextFilesDirty(e: readonly TextFileModelChangeEvent[]): void {
if (this.textFileService.getAutoSaveMode() !== AutoSaveMode.AFTER_SHORT_DELAY) {
this.updateActivityBadge(); // no indication needed when auto save is enabled for short delay
}
// If files become dirty but are not opened, we open it in the background unless there are pending to be saved
this.doOpenDirtyResources(arrays.distinct(e.filter(e => {
// Only dirty models that are not PENDING_SAVE
const model = this.textFileService.models.get(e.resource);
const shouldOpen = model?.isDirty() && !model.hasState(ModelState.PENDING_SAVE);
// Only if not open already
return shouldOpen && !this.editorService.isOpen({ resource: e.resource });
}).map(e => e.resource), r => r.toString()));
}
private doOpenDirtyResources(resources: URI[]): void {
// Open
this.editorService.openEditors(resources.map(resource => {
return {
resource,
options: { inactive: true, pinned: true, preserveFocus: true }
};
}));
}
protected onTextFilesSaved(e: readonly TextFileModelChangeEvent[]): void {
if (this.hasDirtyCount) {
this.updateActivityBadge();
private onWorkingCopyDidChangeDirty(copy: IWorkingCopy): void {
if (!!(copy.capabilities & WorkingCopyCapabilities.AutoSave) && this.textFileService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) {
return; // do not indicate changes to working copies that are auto saved after short delay
}
}
protected onTextFilesSaveError(e: readonly TextFileModelChangeEvent[]): void {
this.updateActivityBadge();
}
protected onTextFilesReverted(e: readonly TextFileModelChangeEvent[]): void {
if (this.hasDirtyCount) {
const gotDirty = copy.isDirty();
if (gotDirty || this.hasDirtyCount) {
this.updateActivityBadge();
}
}
private updateActivityBadge(): void {
const dirtyCount = this.textFileService.getDirty().length;
const dirtyCount = this.workingCopyService.dirtyCount;
this.lastKnownDirtyCount = dirtyCount;
this.badgeHandle.clear();
// Indicate dirty count in badge if any
if (dirtyCount > 0) {
this.badgeHandle.value = this.activityService.showActivity(VIEWLET_ID, new NumberBadge(dirtyCount, num => num === 1 ? nls.localize('dirtyFile', "1 unsaved file") : nls.localize('dirtyFiles', "{0} unsaved files", dirtyCount)), 'explorer-viewlet-label');
this.badgeHandle.value = this.activityService.showActivity(
VIEWLET_ID,
new NumberBadge(dirtyCount, num => num === 1 ? nls.localize('dirtyFile', "1 unsaved file") : nls.localize('dirtyFiles', "{0} unsaved files", dirtyCount)),
'explorer-viewlet-label'
);
} else {
this.badgeHandle.clear();
}
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TextFileModelChangeEvent, ITextFileService, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles';
import { platform, Platform } from 'vs/base/common/platform';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IActivityService } from 'vs/workbench/services/activity/common/activity';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { DirtyFilesTracker } from 'vs/workbench/contrib/files/common/dirtyFilesTracker';
import { IElectronService } from 'vs/platform/electron/node/electron';
export class NativeDirtyFilesTracker extends DirtyFilesTracker {
private isDocumentedEdited: boolean;
constructor(
@ITextFileService protected readonly textFileService: ITextFileService,
@ILifecycleService lifecycleService: ILifecycleService,
@IEditorService editorService: IEditorService,
@IActivityService activityService: IActivityService,
@IUntitledTextEditorService protected readonly untitledTextEditorService: IUntitledTextEditorService,
@IElectronService private readonly electronService: IElectronService
) {
super(textFileService, lifecycleService, editorService, activityService, untitledTextEditorService);
this.isDocumentedEdited = false;
}
protected onUntitledDidChangeDirty(resource: URI): void {
const gotDirty = this.untitledTextEditorService.isDirty(resource);
if ((!this.isDocumentedEdited && gotDirty) || (this.isDocumentedEdited && !gotDirty)) {
this.updateDocumentEdited();
}
super.onUntitledDidChangeDirty(resource);
}
protected onTextFilesDirty(e: readonly TextFileModelChangeEvent[]): void {
if ((this.textFileService.getAutoSaveMode() !== AutoSaveMode.AFTER_SHORT_DELAY) && !this.isDocumentedEdited) {
this.updateDocumentEdited(); // no indication needed when auto save is enabled for short delay
}
super.onTextFilesDirty(e);
}
protected onTextFilesSaved(e: readonly TextFileModelChangeEvent[]): void {
if (this.isDocumentedEdited) {
this.updateDocumentEdited();
}
super.onTextFilesSaved(e);
}
protected onTextFilesSaveError(e: readonly TextFileModelChangeEvent[]): void {
if (!this.isDocumentedEdited) {
this.updateDocumentEdited();
}
super.onTextFilesSaveError(e);
}
protected onTextFilesReverted(e: readonly TextFileModelChangeEvent[]): void {
if (this.isDocumentedEdited) {
this.updateDocumentEdited();
}
super.onTextFilesReverted(e);
}
private updateDocumentEdited(): void {
if (platform === Platform.Mac) {
const hasDirtyFiles = this.textFileService.isDirty();
this.isDocumentedEdited = hasDirtyFiles;
this.electronService.setDocumentEdited(hasDirtyFiles);
}
}
}
......@@ -10,9 +10,6 @@ import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileE
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
import { NativeTextFileEditor } from 'vs/workbench/contrib/files/electron-browser/textFileEditor';
import { NativeDirtyFilesTracker } from 'vs/workbench/contrib/files/electron-browser/dirtyFilesTracker';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
// Register file editor
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
......@@ -25,6 +22,3 @@ Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
new SyncDescriptor<EditorInput>(FileEditorInput)
]
);
// Register Dirty Files Tracker
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NativeDirtyFilesTracker, LifecyclePhase.Starting);
......@@ -24,7 +24,9 @@ import { isEqual } from 'vs/base/common/resources';
import { generateUuid } from 'vs/base/common/uuid';
import { CancellationToken } from 'vs/base/common/cancellation';
import { editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry';
import { IWorkingCopy, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
const CUSTOM_SCHEME = 'testCustomEditor';
const ENABLE = false;
class TestCustomEditorsAction extends Action {
......@@ -35,13 +37,15 @@ class TestCustomEditorsAction extends Action {
constructor(
id: string,
label: string,
@IEditorService private readonly editorService: IEditorService
@IEditorService private readonly editorService: IEditorService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super(id, label);
}
async run(): Promise<boolean> {
await this.editorService.openEditor(new TestCustomEditorInput(URI.parse(`testCustomEditor:/${generateUuid()}`)));
const input = this.instantiationService.createInstance(TestCustomEditorInput, URI.parse(`${CUSTOM_SCHEME}:/${generateUuid()}`));
await this.editorService.openEditor(input);
return true;
}
......@@ -111,12 +115,17 @@ class TestCustomEditor extends BaseEditor {
layout(dimension: Dimension): void { }
}
class TestCustomEditorInput extends EditorInput {
class TestCustomEditorInput extends EditorInput implements IWorkingCopy {
private model: TestCustomEditorModel | undefined = undefined;
private dirty = false;
constructor(public readonly resource: URI) {
readonly capabilities = 0;
constructor(public readonly resource: URI, @IWorkingCopyService workingCopyService: IWorkingCopyService) {
super();
this._register(workingCopyService.registerWorkingCopy(this));
}
getResource(): URI {
......@@ -128,7 +137,7 @@ class TestCustomEditorInput extends EditorInput {
}
getName(): string {
return `Custom Editor: ${this.resource.toString()}`;
return this.resource.toString();
}
setValue(value: string) {
......@@ -139,9 +148,11 @@ class TestCustomEditorInput extends EditorInput {
this.setDirty(value.length > 0);
}
setDirty(dirty: boolean) {
this.dirty = dirty;
this._onDidChangeDirty.fire();
private setDirty(dirty: boolean) {
if (this.dirty !== dirty) {
this.dirty = dirty;
this._onDidChangeDirty.fire();
}
}
isDirty(): boolean {
......@@ -176,6 +187,17 @@ class TestCustomEditorInput extends EditorInput {
matches(other: EditorInput) {
return other instanceof TestCustomEditorInput && isEqual(other.resource, this.resource);
}
dispose(): void {
this.setDirty(false);
if (this.model) {
this.model.dispose();
this.model = undefined;
}
super.dispose();
}
}
class TestCustomEditorModel extends EditorModel {
......@@ -212,7 +234,7 @@ if (ENABLE) {
}
deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): TestCustomEditorInput {
return new TestCustomEditorInput(URI.parse(JSON.parse(serializedEditorInput).resource));
return instantiationService.createInstance(TestCustomEditorInput, URI.parse(JSON.parse(serializedEditorInput).resource));
}
}
......
......@@ -60,6 +60,7 @@ import { ITunnelService, extractLocalHostUriMetaDataForPortMapping } from 'vs/pl
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IElectronEnvironmentService } from 'vs/workbench/services/electron/electron-browser/electronEnvironmentService';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
export class ElectronWindow extends Disposable {
......@@ -67,14 +68,16 @@ export class ElectronWindow extends Disposable {
private readonly touchBarDisposables = this._register(new DisposableStore());
private lastInstalledTouchedBar: ICommandAction[][] | undefined;
private customTitleContextMenuDisposable = this._register(new DisposableStore());
private readonly customTitleContextMenuDisposable = this._register(new DisposableStore());
private previousConfiguredZoomLevel: number | undefined;
private addFoldersScheduler: RunOnceScheduler;
private pendingFoldersToAdd: URI[];
private readonly addFoldersScheduler = this._register(new RunOnceScheduler(() => this.doAddFolders(), 100));
private pendingFoldersToAdd: URI[] = [];
private closeEmptyWindowScheduler: RunOnceScheduler = this._register(new RunOnceScheduler(() => this.onAllEditorsClosed(), 50));
private readonly closeEmptyWindowScheduler: RunOnceScheduler = this._register(new RunOnceScheduler(() => this.onAllEditorsClosed(), 50));
private isDocumentedEdited = false;
constructor(
@IEditorService private readonly editorService: EditorServiceImpl,
......@@ -99,13 +102,11 @@ export class ElectronWindow extends Disposable {
@IElectronService private readonly electronService: IElectronService,
@ITunnelService private readonly tunnelService: ITunnelService,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@IElectronEnvironmentService private readonly electronEnvironmentService: IElectronEnvironmentService
@IElectronEnvironmentService private readonly electronEnvironmentService: IElectronEnvironmentService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService
) {
super();
this.pendingFoldersToAdd = [];
this.addFoldersScheduler = this._register(new RunOnceScheduler(() => this.doAddFolders(), 100));
this.registerListeners();
this.create();
}
......@@ -261,6 +262,19 @@ export class ElectronWindow extends Disposable {
this.electronService.handleTitleDoubleClick();
}));
}
// Document edited (macOS only): indicate for dirty working copies
if (isMacintosh) {
this._register(this.workingCopyService.onDidChangeDirty(workingCopy => {
const gotDirty = workingCopy.isDirty();
if ((!this.isDocumentedEdited && gotDirty) || (this.isDocumentedEdited && !gotDirty)) {
const hasDirtyFiles = this.workingCopyService.hasDirty;
this.isDocumentedEdited = hasDirtyFiles;
this.electronService.setDocumentEdited(hasDirtyFiles);
}
}));
}
}
private onDidVisibleEditorsChange(): void {
......
......@@ -39,7 +39,7 @@ import { KeybindingsEditingService } from 'vs/workbench/services/keybinding/comm
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService';
import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { TestBackupFileService, TestContextService, TestEditorGroupsService, TestEditorService, TestLifecycleService, TestTextFileService, TestTextResourcePropertiesService } from 'vs/workbench/test/workbenchTestServices';
import { TestBackupFileService, TestContextService, TestEditorGroupsService, TestEditorService, TestLifecycleService, TestTextFileService, TestTextResourcePropertiesService, TestWorkingCopyService } from 'vs/workbench/test/workbenchTestServices';
import { FileService } from 'vs/platform/files/common/fileService';
import { Schemas } from 'vs/base/common/network';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
......@@ -49,6 +49,7 @@ import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
class TestEnvironmentService extends NativeWorkbenchEnvironmentService {
......@@ -67,7 +68,7 @@ interface Modifiers {
shiftKey?: boolean;
}
suite('KeybindingsEditing', () => {
suite.skip('KeybindingsEditing', () => {
let instantiationService: TestInstantiationService;
let testObject: KeybindingsEditingService;
......@@ -93,6 +94,7 @@ suite('KeybindingsEditing', () => {
instantiationService.stub(IContextKeyService, <IContextKeyService>instantiationService.createInstance(MockContextKeyService));
instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService());
instantiationService.stub(IEditorService, new TestEditorService());
instantiationService.stub(IWorkingCopyService, new TestWorkingCopyService());
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IModeService, ModeServiceImpl);
instantiationService.stub(ILogService, new NullLogService());
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { Emitter } from 'vs/base/common/event';
import { guessMimeTypes } from 'vs/base/common/mime';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { URI } from 'vs/base/common/uri';
......@@ -29,6 +29,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { isEqual, isEqualOrParent, extname, basename, joinPath } from 'vs/base/common/resources';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Schemas } from 'vs/base/common/network';
import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService';
export interface IBackupMetaData {
mtime: number;
......@@ -57,7 +58,7 @@ type TelemetryData = {
/**
* The text file editor model listens to changes to its underlying code editor model and saves these changes through the file service back to the disk.
*/
export class TextFileEditorModel extends BaseTextEditorModel implements ITextFileEditorModel {
export class TextFileEditorModel extends BaseTextEditorModel implements ITextFileEditorModel, IWorkingCopy {
static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY;
static DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 100;
......@@ -70,11 +71,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
private static saveParticipant: ISaveParticipant | null;
static setSaveParticipant(handler: ISaveParticipant | null): void { TextFileEditorModel.saveParticipant = handler; }
private readonly _onDidContentChange: Emitter<StateChange> = this._register(new Emitter<StateChange>());
readonly onDidContentChange: Event<StateChange> = this._onDidContentChange.event;
private readonly _onDidContentChange = this._register(new Emitter<StateChange>());
readonly onDidContentChange = this._onDidContentChange.event;
private readonly _onDidStateChange: Emitter<StateChange> = this._register(new Emitter<StateChange>());
readonly onDidStateChange: Event<StateChange> = this._onDidStateChange.event;
private readonly _onDidStateChange = this._register(new Emitter<StateChange>());
readonly onDidStateChange = this._onDidStateChange.event;
private readonly _onDidChangeDirty = this._register(new Emitter<void>());
readonly onDidChangeDirty = this._onDidChangeDirty.event;
readonly capabilities = WorkingCopyCapabilities.AutoSave;
private contentEncoding: string | undefined; // encoding as reported from disk
......@@ -101,7 +107,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
private disposed = false;
constructor(
private readonly resource: URI,
public readonly resource: URI,
private preferredEncoding: string | undefined, // encoding as chosen by the user
private preferredMode: string | undefined, // mode as chosen by the user
@INotificationService private readonly notificationService: INotificationService,
......@@ -114,12 +120,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
@IBackupFileService private readonly backupFileService: IBackupFileService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@ILogService private readonly logService: ILogService
@ILogService private readonly logService: ILogService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService
) {
super(modelService, modeService);
this.updateAutoSaveConfiguration(textFileService.getAutoSaveConfiguration());
// Make known to working copy service
this._register(this.workingCopyService.registerWorkingCopy(this));
this.registerListeners();
}
......@@ -249,6 +259,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.autoSaveDisposable.clear();
// Unset flags
const wasDirty = this.dirty;
const undo = this.setDirty(false);
// Force read from disk unless reverting soft
......@@ -266,6 +277,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Emit file change event
this._onDidStateChange.fire(StateChange.REVERTED);
// Emit dirty change event
if (wasDirty) {
this._onDidChangeDirty.fire();
}
}
async load(options?: ILoadOptions): Promise<ITextFileEditorModel> {
......@@ -528,6 +544,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Emit event
if (wasDirty) {
this._onDidStateChange.fire(StateChange.REVERTED);
this._onDidChangeDirty.fire();
}
return;
......@@ -568,6 +585,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Emit as Event if we turned dirty
if (!wasDirty) {
this._onDidStateChange.fire(StateChange.DIRTY);
this._onDidChangeDirty.fire();
}
}
......@@ -739,8 +757,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Cancel any content change event promises as they are no longer valid
this.contentChangeEventScheduler.cancel();
// Emit File Saved Event
// Emit Events
this._onDidStateChange.fire(StateChange.SAVED);
this._onDidChangeDirty.fire();
// Telemetry
const settingsType = this.getTypeIfSettings();
......
......@@ -16,9 +16,15 @@ import { IModelService } from 'vs/editor/common/services/modelService';
import { timeout } from 'vs/base/common/async';
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
import { assertIsDefined } from 'vs/base/common/types';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
class ServiceAccessor {
constructor(@ITextFileService public textFileService: TestTextFileService, @IModelService public modelService: IModelService, @IFileService public fileService: TestFileService) {
constructor(
@ITextFileService public readonly textFileService: TestTextFileService,
@IModelService public readonly modelService: IModelService,
@IFileService public readonly fileService: TestFileService,
@IWorkingCopyService public readonly workingCopyService: IWorkingCopyService
) {
}
}
......@@ -51,10 +57,15 @@ suite('Files - TextFileEditorModel', () => {
await model.load();
assert.equal(accessor.workingCopyService.dirtyCount, 0);
model.textEditorModel!.setValue('bar');
assert.ok(getLastModifiedTime(model) <= Date.now());
assert.ok(model.hasState(ModelState.DIRTY));
assert.equal(accessor.workingCopyService.dirtyCount, 1);
assert.equal(accessor.workingCopyService.isDirty(model.resource), true);
let savedEvent = false;
model.onDidStateChange(e => {
if (e === StateChange.SAVED) {
......@@ -62,6 +73,13 @@ suite('Files - TextFileEditorModel', () => {
}
});
let workingCopyEvent = false;
accessor.workingCopyService.onDidChangeDirty(e => {
if (e.resource.toString() === model.resource.toString()) {
workingCopyEvent = true;
}
});
const pendingSave = model.save();
assert.ok(model.hasState(ModelState.PENDING_SAVE));
......@@ -71,6 +89,10 @@ suite('Files - TextFileEditorModel', () => {
assert.ok(model.hasState(ModelState.SAVED));
assert.ok(!model.isDirty());
assert.ok(savedEvent);
assert.ok(workingCopyEvent);
assert.equal(accessor.workingCopyService.dirtyCount, 0);
assert.equal(accessor.workingCopyService.isDirty(model.resource), false);
model.dispose();
assert.ok(!accessor.modelService.getModel(model.getResource()));
......@@ -88,9 +110,17 @@ suite('Files - TextFileEditorModel', () => {
}
});
let workingCopyEvent = false;
accessor.workingCopyService.onDidChangeDirty(e => {
if (e.resource.toString() === model.resource.toString()) {
workingCopyEvent = true;
}
});
await model.save({ force: true });
assert.ok(savedEvent);
assert.ok(!workingCopyEvent);
model.dispose();
assert.ok(!accessor.modelService.getModel(model.getResource()));
......@@ -182,14 +212,29 @@ suite('Files - TextFileEditorModel', () => {
}
});
let workingCopyEvent = false;
accessor.workingCopyService.onDidChangeDirty(e => {
if (e.resource.toString() === model.resource.toString()) {
workingCopyEvent = true;
}
});
await model.load();
model.textEditorModel!.setValue('foo');
assert.ok(model.isDirty());
assert.equal(accessor.workingCopyService.dirtyCount, 1);
assert.equal(accessor.workingCopyService.isDirty(model.resource), true);
await model.revert();
assert.ok(!model.isDirty());
assert.equal(model.textEditorModel!.getValue(), 'Hello Html');
assert.equal(eventCounter, 1);
assert.ok(workingCopyEvent);
assert.equal(accessor.workingCopyService.dirtyCount, 0);
assert.equal(accessor.workingCopyService.isDirty(model.resource), false);
model.dispose();
});
......@@ -204,14 +249,29 @@ suite('Files - TextFileEditorModel', () => {
}
});
let workingCopyEvent = false;
accessor.workingCopyService.onDidChangeDirty(e => {
if (e.resource.toString() === model.resource.toString()) {
workingCopyEvent = true;
}
});
await model.load();
model.textEditorModel!.setValue('foo');
assert.ok(model.isDirty());
assert.equal(accessor.workingCopyService.dirtyCount, 1);
assert.equal(accessor.workingCopyService.isDirty(model.resource), true);
await model.revert(true /* soft revert */);
assert.ok(!model.isDirty());
assert.equal(model.textEditorModel!.getValue(), 'foo');
assert.equal(eventCounter, 1);
assert.ok(workingCopyEvent);
assert.equal(accessor.workingCopyService.dirtyCount, 0);
assert.equal(accessor.workingCopyService.isDirty(model.resource), false);
model.dispose();
});
......@@ -223,6 +283,9 @@ suite('Files - TextFileEditorModel', () => {
await model.load();
model.textEditorModel!.undo();
assert.ok(model.isDirty());
assert.equal(accessor.workingCopyService.dirtyCount, 1);
assert.equal(accessor.workingCopyService.isDirty(model.resource), true);
});
test('Make Dirty', async function () {
......@@ -246,9 +309,18 @@ suite('Files - TextFileEditorModel', () => {
}
});
let workingCopyEvent = false;
accessor.workingCopyService.onDidChangeDirty(e => {
if (e.resource.toString() === model.resource.toString()) {
workingCopyEvent = true;
}
});
model.makeDirty();
assert.ok(model.isDirty());
assert.equal(eventCounter, 1);
assert.ok(workingCopyEvent);
model.dispose();
});
......@@ -343,7 +415,6 @@ suite('Files - TextFileEditorModel', () => {
});
test('Save Participant, async participant', async function () {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
TextFileEditorModel.setSaveParticipant({
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Event, Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { Disposable, IDisposable, toDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle';
import { TernarySearchTree } from 'vs/base/common/map';
export const enum WorkingCopyCapabilities {
/**
* Signals that the working copy participates
* in auto saving as configured by the user.
*/
AutoSave = 1 << 1
}
export interface IWorkingCopy {
//#region Dirty Tracking
readonly onDidChangeDirty: Event<void>;
isDirty(): boolean;
//#endregion
readonly resource: URI;
readonly capabilities: WorkingCopyCapabilities;
}
export const IWorkingCopyService = createDecorator<IWorkingCopyService>('workingCopyService');
export interface IWorkingCopyService {
_serviceBrand: undefined;
//#region Dirty Tracking
readonly onDidChangeDirty: Event<IWorkingCopy>;
readonly dirtyCount: number;
readonly hasDirty: boolean;
isDirty(resource: URI): boolean;
//#endregion
//#region Registry
registerWorkingCopy(workingCopy: IWorkingCopy): IDisposable;
//#endregion
}
export class WorkingCopyService extends Disposable implements IWorkingCopyService {
_serviceBrand: undefined;
//#region Dirty Tracking
private readonly _onDidChangeDirty = this._register(new Emitter<IWorkingCopy>());
readonly onDidChangeDirty = this._onDidChangeDirty.event;
isDirty(resource: URI): boolean {
const workingCopies = this.mapResourceToWorkingCopy.get(resource.toString());
if (workingCopies) {
for (const workingCopy of workingCopies) {
if (workingCopy.isDirty()) {
return true;
}
}
}
return false;
}
get hasDirty(): boolean {
for (const workingCopy of this.workingCopies) {
if (workingCopy.isDirty()) {
return true;
}
}
return false;
}
get dirtyCount(): number {
let totalDirtyCount = 0;
for (const workingCopy of this.workingCopies) {
if (workingCopy.isDirty()) {
totalDirtyCount++;
}
}
return totalDirtyCount;
}
//#endregion
//#region Registry
private mapResourceToWorkingCopy = TernarySearchTree.forPaths<Set<IWorkingCopy>>();
private workingCopies = new Set<IWorkingCopy>();
registerWorkingCopy(workingCopy: IWorkingCopy): IDisposable {
const disposables = new DisposableStore();
// Registry
let workingCopiesForResource = this.mapResourceToWorkingCopy.get(workingCopy.resource.toString());
if (!workingCopiesForResource) {
workingCopiesForResource = new Set<IWorkingCopy>();
this.mapResourceToWorkingCopy.set(workingCopy.resource.toString(), workingCopiesForResource);
}
workingCopiesForResource.add(workingCopy);
this.workingCopies.add(workingCopy);
// Dirty Events
disposables.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy)));
if (workingCopy.isDirty()) {
this._onDidChangeDirty.fire(workingCopy);
}
return toDisposable(() => {
this.unregisterWorkingCopy(workingCopy);
dispose(disposables);
});
}
private unregisterWorkingCopy(workingCopy: IWorkingCopy): void {
// Remove from registry
const workingCopiesForResource = this.mapResourceToWorkingCopy.get(workingCopy.resource.toString());
if (workingCopiesForResource && workingCopiesForResource.delete(workingCopy) && workingCopiesForResource.size === 0) {
this.mapResourceToWorkingCopy.delete(workingCopy.resource.toString());
}
this.workingCopies.delete(workingCopy);
// If copy is dirty, ensure to fire an event to signal the dirty change
// (a disposed working copy cannot account for being dirty in our model)
if (workingCopy.isDirty()) {
this._onDidChangeDirty.fire(workingCopy);
}
}
//#endregion
}
registerSingleton(IWorkingCopyService, WorkingCopyService, true);
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { WorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { URI } from 'vs/base/common/uri';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
suite('WorkingCopyService', () => {
class TestWorkingCopy extends Disposable implements IWorkingCopy {
private readonly _onDidChangeDirty = this._register(new Emitter<void>());
readonly onDidChangeDirty = this._onDidChangeDirty.event;
private readonly _onDispose = this._register(new Emitter<void>());
readonly onDispose = this._onDispose.event;
readonly capabilities = 0;
private dirty = false;
constructor(public readonly resource: URI, isDirty = false) {
super();
this.dirty = isDirty;
}
setDirty(dirty: boolean): void {
if (this.dirty !== dirty) {
this.dirty = dirty;
this._onDidChangeDirty.fire();
}
}
isDirty(): boolean {
return this.dirty;
}
dispose(): void {
this._onDispose.fire();
super.dispose();
}
}
test('registry - basics', () => {
const service = new WorkingCopyService();
const onDidChangeDirty: IWorkingCopy[] = [];
service.onDidChangeDirty(copy => onDidChangeDirty.push(copy));
assert.equal(service.hasDirty, false);
assert.equal(service.dirtyCount, 0);
assert.equal(service.isDirty(URI.file('/')), false);
// resource 1
const resource1 = URI.file('/some/folder/file.txt');
const copy1 = new TestWorkingCopy(resource1);
const unregister1 = service.registerWorkingCopy(copy1);
assert.equal(service.dirtyCount, 0);
assert.equal(service.isDirty(resource1), false);
assert.equal(service.hasDirty, false);
copy1.setDirty(true);
assert.equal(service.dirtyCount, 1);
assert.equal(service.isDirty(resource1), true);
assert.equal(service.hasDirty, true);
assert.equal(onDidChangeDirty.length, 1);
assert.equal(onDidChangeDirty[0], copy1);
copy1.setDirty(false);
assert.equal(service.dirtyCount, 0);
assert.equal(service.isDirty(resource1), false);
assert.equal(service.hasDirty, false);
assert.equal(onDidChangeDirty.length, 2);
assert.equal(onDidChangeDirty[1], copy1);
unregister1.dispose();
// resource 2
const resource2 = URI.file('/some/folder/file-dirty.txt');
const copy2 = new TestWorkingCopy(resource2, true);
const unregister2 = service.registerWorkingCopy(copy2);
assert.equal(service.dirtyCount, 1);
assert.equal(service.isDirty(resource2), true);
assert.equal(service.hasDirty, true);
assert.equal(onDidChangeDirty.length, 3);
assert.equal(onDidChangeDirty[2], copy2);
unregister2.dispose();
assert.equal(service.dirtyCount, 0);
assert.equal(service.hasDirty, false);
assert.equal(onDidChangeDirty.length, 4);
assert.equal(onDidChangeDirty[3], copy2);
});
test('registry - multiple copies on same resource', () => {
const service = new WorkingCopyService();
const onDidChangeDirty: IWorkingCopy[] = [];
service.onDidChangeDirty(copy => onDidChangeDirty.push(copy));
const resource = URI.parse('custom://some/folder/custom.txt');
const copy1 = new TestWorkingCopy(resource);
const unregister1 = service.registerWorkingCopy(copy1);
const copy2 = new TestWorkingCopy(resource);
const unregister2 = service.registerWorkingCopy(copy2);
copy1.setDirty(true);
assert.equal(service.dirtyCount, 1);
assert.equal(onDidChangeDirty.length, 1);
assert.equal(service.isDirty(resource), true);
copy2.setDirty(true);
assert.equal(service.dirtyCount, 2);
assert.equal(onDidChangeDirty.length, 2);
assert.equal(service.isDirty(resource), true);
unregister1.dispose();
assert.equal(service.dirtyCount, 1);
assert.equal(onDidChangeDirty.length, 3);
assert.equal(service.isDirty(resource), true);
unregister2.dispose();
assert.equal(service.dirtyCount, 0);
assert.equal(onDidChangeDirty.length, 4);
assert.equal(service.isDirty(resource), false);
});
});
......@@ -17,6 +17,7 @@ import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledText
import { timeout } from 'vs/base/common/async';
import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles';
import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService';
export class TestUntitledTextEditorService extends UntitledTextEditorService {
get(resource: URI) { return super.get(resource); }
......@@ -25,9 +26,10 @@ export class TestUntitledTextEditorService extends UntitledTextEditorService {
class ServiceAccessor {
constructor(
@IUntitledTextEditorService public untitledTextEditorService: TestUntitledTextEditorService,
@IModeService public modeService: ModeServiceImpl,
@IConfigurationService public testConfigurationService: TestConfigurationService) {
@IUntitledTextEditorService public readonly untitledTextEditorService: TestUntitledTextEditorService,
@IWorkingCopyService public readonly workingCopyService: IWorkingCopyService,
@IModeService public readonly modeService: ModeServiceImpl,
@IConfigurationService public readonly testConfigurationService: TestConfigurationService) {
}
}
......@@ -48,6 +50,8 @@ suite('Workbench untitled text editors', () => {
test('Untitled Text Editor Service', async (done) => {
const service = accessor.untitledTextEditorService;
const workingCopyService = accessor.workingCopyService;
assert.equal(service.getAll().length, 0);
const input1 = service.createOrGet();
......@@ -83,11 +87,17 @@ suite('Workbench untitled text editors', () => {
assert.equal(service.getDirty([input2.getResource()])[0].toString(), input2.getResource().toString());
assert.equal(service.getDirty([input1.getResource()]).length, 0);
assert.ok(workingCopyService.isDirty(input2.getResource()));
assert.equal(workingCopyService.dirtyCount, 1);
service.revertAll();
assert.equal(service.getAll().length, 0);
assert.ok(!input2.isDirty());
assert.ok(!model.isDirty());
assert.ok(!workingCopyService.isDirty(input2.getResource()));
assert.equal(workingCopyService.dirtyCount, 0);
input2.dispose();
assert.ok(!service.exists(input2.getResource()));
......@@ -109,14 +119,17 @@ suite('Workbench untitled text editors', () => {
test('Untitled no longer dirty when content gets empty', async () => {
const service = accessor.untitledTextEditorService;
const workingCopyService = accessor.workingCopyService;
const input = service.createOrGet();
// dirty
const model = await input.resolve();
model.textEditorModel.setValue('foo bar');
assert.ok(model.isDirty());
assert.ok(workingCopyService.isDirty(model.resource));
model.textEditorModel.setValue('');
assert.ok(!model.isDirty());
assert.ok(!workingCopyService.isDirty(model.resource));
input.dispose();
});
......@@ -176,11 +189,21 @@ suite('Workbench untitled text editors', () => {
test('Untitled with initial content is dirty', async () => {
const service = accessor.untitledTextEditorService;
const input = service.createOrGet(undefined, undefined, 'Hello World');
const workingCopyService = accessor.workingCopyService;
let onDidChangeDirty: IWorkingCopy | undefined = undefined;
const listener = workingCopyService.onDidChangeDirty(copy => {
onDidChangeDirty = copy;
});
// dirty
const model = await input.resolve();
assert.ok(model.isDirty());
assert.equal(workingCopyService.dirtyCount, 1);
assert.equal(onDidChangeDirty, model);
input.dispose();
listener.dispose();
});
test('Untitled created with files.defaultLanguage setting', () => {
......
......@@ -92,6 +92,7 @@ import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/ele
import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
import { find } from 'vs/base/common/arrays';
import { WorkingCopyService, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput {
return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined);
......@@ -330,6 +331,7 @@ export function workbenchInstantiationService(): IInstantiationService {
instantiationService.stub(IEditorService, editorService);
instantiationService.stub(ICodeEditorService, new TestCodeEditorService());
instantiationService.stub(IViewletService, new TestViewletService());
instantiationService.stub(IWorkingCopyService, new TestWorkingCopyService());
return instantiationService;
}
......@@ -1464,3 +1466,7 @@ export class TestDialogMainService implements IDialogMainService {
throw new Error('Method not implemented.');
}
}
export class TestWorkingCopyService extends WorkingCopyService {
}
......@@ -82,6 +82,7 @@ import 'vs/workbench/services/extensions/common/staticExtensions';
import 'vs/workbench/services/userDataSync/common/settingsMergeService';
import 'vs/workbench/services/path/common/remotePathService';
import 'vs/workbench/services/remote/common/remoteExplorerService';
import 'vs/workbench/services/workingCopy/common/workingCopyService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册