diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 83632b3ef2b19723ae74a5fa1e83e61eff774e1f..621adcab2cdd6be95fdd8b37698d4e28cb21789e 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { mixin } from 'vs/base/common/objects'; -import { IEditorInput, EditorInput, IEditorIdentifier, IEditorCommandsContext, CloseDirection, SaveReason, EditorsOrder } from 'vs/workbench/common/editor'; +import { IEditorInput, EditorInput, IEditorIdentifier, IEditorCommandsContext, CloseDirection, SaveReason, EditorsOrder, SideBySideEditorInput } from 'vs/workbench/common/editor'; import { QuickOpenEntryGroup } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { EditorQuickOpenEntry, EditorQuickOpenEntryGroup, IEditorQuickOpenEntry, QuickOpenAction } from 'vs/workbench/browser/quickopen'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; @@ -23,7 +23,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { ResourceMap, values } from 'vs/base/common/map'; +import { values } from 'vs/base/common/map'; export class ExecuteCommandAction extends Action { @@ -640,23 +640,24 @@ export abstract class BaseCloseAllAction extends Action { return undefined; })); - const dirtyEditorsToConfirmByName = new Set(); - const dirtyEditorsToConfirmByResource = new ResourceMap(); + const dirtyEditorsToConfirm = new Set(); for (const editor of this.editorService.editors) { if (!editor.isDirty() || editor.isSaving()) { continue; // only interested in dirty editors (unless in the process of saving) } - const resource = editor.getResource(); - if (resource) { - dirtyEditorsToConfirmByResource.set(resource, true); + let name: string; + if (editor instanceof SideBySideEditorInput) { + name = editor.master.getName(); // prefer shorter names by using master's name in this case } else { - dirtyEditorsToConfirmByName.add(editor.getName()); + name = editor.getName(); } + + dirtyEditorsToConfirm.add(name); } - const confirm = await this.fileDialogService.showSaveConfirm([...dirtyEditorsToConfirmByResource.keys(), ...values(dirtyEditorsToConfirmByName)]); + const confirm = await this.fileDialogService.showSaveConfirm(values(dirtyEditorsToConfirm)); if (confirm === ConfirmResult.CANCEL) { return; } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index f349610649791e8f85cb245c3302f204e8f7867e..1b15bea8db514e0fa17a070cb396df52438e7523 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/editorgroupview'; import { EditorGroup, IEditorOpenOptions, EditorCloseEvent, ISerializedEditorGroup, isSerializedEditorGroup } from 'vs/workbench/common/editor/editorGroup'; -import { EditorInput, EditorOptions, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext, IEditor, EditorGroupEditorsCountContext, toResource, SideBySideEditor, SaveReason, SaveContext, IEditorPartOptionsChangeEvent, EditorsOrder } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext, IEditor, EditorGroupEditorsCountContext, SaveReason, SaveContext, IEditorPartOptionsChangeEvent, EditorsOrder } from 'vs/workbench/common/editor'; import { Event, Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { addClass, addClasses, Dimension, trackFocus, toggleClass, removeClass, addDisposableListener, EventType, EventHelper, findParentWithClass, clearNode, isAncestor } from 'vs/base/browser/dom'; @@ -1306,8 +1306,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Switch to editor that we want to handle and confirm to save/revert await this.openEditor(editor); - const editorResource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); - const res = await this.fileDialogService.showSaveConfirm(editorResource ? [editorResource] : [editor.getName()]); + let name: string; + if (editor instanceof SideBySideEditorInput) { + name = editor.master.getName(); // prefer shorter names by using master's name in this case + } else { + name = editor.getName(); + } + + const res = await this.fileDialogService.showSaveConfirm([name]); // It could be that the editor saved meanwhile or is saving, so we check // again to see if anything needs to happen before closing for good. diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index b3f3aabad43f8b82371eade33c8dbc275f725bf4..b7eabf42b6723ec3616d63439600a1ee71e8c3f3 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { assign } from 'vs/base/common/objects'; import { withNullAsUndefined, assertIsDefined } from 'vs/base/common/types'; @@ -682,7 +683,7 @@ export class SideBySideEditorInput extends EditorInput { static readonly ID: string = 'workbench.editorinputs.sidebysideEditorInput'; constructor( - private readonly name: string, + protected readonly name: string | undefined, private readonly description: string | undefined, private readonly _details: EditorInput, private readonly _master: EditorInput @@ -700,6 +701,22 @@ export class SideBySideEditorInput extends EditorInput { return this._details; } + getTypeId(): string { + return SideBySideEditorInput.ID; + } + + getName(): string { + if (!this.name) { + return localize('sideBySideLabels', "{0} - {1}", this._details.getName(), this._master.getName()); + } + + return this.name; + } + + getDescription(): string | undefined { + return this.description; + } + isReadonly(): boolean { return this.master.isReadonly(); } @@ -760,18 +777,6 @@ export class SideBySideEditorInput extends EditorInput { return null; } - getTypeId(): string { - return SideBySideEditorInput.ID; - } - - getName(): string { - return this.name; - } - - getDescription(): string | undefined { - return this.description; - } - matches(otherInput: unknown): boolean { if (super.matches(otherInput) === true) { return true; diff --git a/src/vs/workbench/common/editor/diffEditorInput.ts b/src/vs/workbench/common/editor/diffEditorInput.ts index b9f1bc2811c8fb711cf8d6ac322c68b573e69c3b..e076cfb8df5237bc465a9f8f30fa321db4318901 100644 --- a/src/vs/workbench/common/editor/diffEditorInput.ts +++ b/src/vs/workbench/common/editor/diffEditorInput.ts @@ -7,6 +7,7 @@ import { EditorModel, EditorInput, SideBySideEditorInput, TEXT_DIFF_EDITOR_ID, B import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { DiffEditorModel } from 'vs/workbench/common/editor/diffEditorModel'; import { TextDiffEditorModel } from 'vs/workbench/common/editor/textDiffEditorModel'; +import { localize } from 'vs/nls'; /** * The base editor input for the diff editor. It is made up of two editor inputs, the original version @@ -19,33 +20,25 @@ export class DiffEditorInput extends SideBySideEditorInput { private cachedModel: DiffEditorModel | null = null; constructor( - name: string, + protected name: string | undefined, description: string | undefined, - original: EditorInput, - modified: EditorInput, + public readonly originalInput: EditorInput, + public readonly modifiedInput: EditorInput, private readonly forceOpenAsBinary?: boolean ) { - super(name, description, original, modified); - } - - matches(otherInput: unknown): boolean { - if (!super.matches(otherInput)) { - return false; - } - - return otherInput instanceof DiffEditorInput && otherInput.forceOpenAsBinary === this.forceOpenAsBinary; + super(name, description, originalInput, modifiedInput); } getTypeId(): string { return DiffEditorInput.ID; } - get originalInput(): EditorInput { - return this.details; - } + getName(): string { + if (!this.name) { + return localize('sideBySideLabels', "{0} ↔ {1}", this.originalInput.getName(), this.modifiedInput.getName()); + } - get modifiedInput(): EditorInput { - return this.master; + return this.name; } async resolve(): Promise { @@ -88,6 +81,14 @@ export class DiffEditorInput extends SideBySideEditorInput { return new DiffEditorModel(originalEditorModel, modifiedEditorModel); } + matches(otherInput: unknown): boolean { + if (!super.matches(otherInput)) { + return false; + } + + return otherInput instanceof DiffEditorInput && otherInput.forceOpenAsBinary === this.forceOpenAsBinary; + } + dispose(): void { // Free the diff editor model but do not propagate the dispose() call to the two inputs diff --git a/src/vs/workbench/common/editor/untitledTextEditorInput.ts b/src/vs/workbench/common/editor/untitledTextEditorInput.ts index 2366bcf133b8d9868be40e1e907579b829d23cfd..33247e1cfbe8e5a8bbf1c119d21bad43f3df2d57 100644 --- a/src/vs/workbench/common/editor/untitledTextEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledTextEditorInput.ts @@ -28,10 +28,14 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin private static readonly MEMOIZER = createMemoizer(); + private static readonly FIRST_LINE_MAX_TITLE_LENGTH = 50; + private readonly _onDidModelChangeEncoding = this._register(new Emitter()); readonly onDidModelChangeEncoding = this._onDidModelChangeEncoding.event; private cachedModel: UntitledTextEditorModel | null = null; + private cachedModelFirstLine: string | undefined = undefined; + private modelResolve: Promise | null = null; private preferredMode: string | undefined; @@ -71,6 +75,15 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin } getName(): string { + + // Take name from first line if present and only if + // we have no associated file path. In that case we + // prefer the file name as title. + if (!this._hasAssociatedFilePath && this.cachedModelFirstLine) { + return this.cachedModelFirstLine; + } + + // Otherwise fallback to resource return this.hasAssociatedFilePath ? basenameOrAuthority(this.resource) : this.resource.path; } @@ -278,11 +291,30 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin private createModel(): UntitledTextEditorModel { const model = this._register(this.instantiationService.createInstance(UntitledTextEditorModel, this.preferredMode, this.resource, this.hasAssociatedFilePath, this.initialValue, this.preferredEncoding)); + this.registerModelListeners(model); + + return model; + } + + private registerModelListeners(model: UntitledTextEditorModel): void { + // re-emit some events from the model this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); this._register(model.onDidChangeEncoding(() => this._onDidModelChangeEncoding.fire())); - return model; + // listen for first line change events if we use it for the label + // by checking the contents of the first line has changed + if (!this._hasAssociatedFilePath) { + this._register(model.onDidChangeFirstLine(() => this.onDidChangeFirstLine(model))); + } + } + + private onDidChangeFirstLine(model: UntitledTextEditorModel): void { + const firstLineText = model.textEditorModel?.getValueInRange({ startLineNumber: 1, endLineNumber: 1, startColumn: 1, endColumn: UntitledTextEditorInput.FIRST_LINE_MAX_TITLE_LENGTH }).trim(); + if (firstLineText !== this.cachedModelFirstLine) { + this.cachedModelFirstLine = firstLineText; + this._onDidChangeLabel.fire(); + } } matches(otherInput: unknown): boolean { diff --git a/src/vs/workbench/common/editor/untitledTextEditorModel.ts b/src/vs/workbench/common/editor/untitledTextEditorModel.ts index 36691bbd7ceec786ec5d3fa1aa82db1ed521b62b..600989df4fa647053f8b40207fd72d594a6679e6 100644 --- a/src/vs/workbench/common/editor/untitledTextEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledTextEditorModel.ts @@ -16,6 +16,7 @@ import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { IResolvedTextEditorModel, ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport, IEncodingSupport, IWorkingCopy { } @@ -24,6 +25,9 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt private readonly _onDidChangeContent = this._register(new Emitter()); readonly onDidChangeContent = this._onDidChangeContent.event; + private readonly _onDidChangeFirstLine = this._register(new Emitter()); + readonly onDidChangeFirstLine = this._onDidChangeFirstLine.event; + private readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; @@ -170,15 +174,22 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt const textEditorModel = this.textEditorModel!; // Listen to content changes - this._register(textEditorModel.onDidChangeContent(() => this.onModelContentChanged())); + this._register(textEditorModel.onDidChangeContent(e => this.onModelContentChanged(e))); // Listen to mode changes this._register(textEditorModel.onDidChangeLanguage(() => this.onConfigurationChange())); // mode change can have impact on config + // If we have initial contents, make sure to emit this + // as the appropiate events to the outside. + if (backup || this.initialValue) { + this._onDidChangeContent.fire(); + this._onDidChangeFirstLine.fire(); + } + return this as UntitledTextEditorModel & IResolvedTextEditorModel; } - private onModelContentChanged(): void { + private onModelContentChanged(e: IModelContentChangedEvent): void { if (!this.isResolved()) { return; } @@ -196,8 +207,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt this.setDirty(true); } - // Emit as event + // Emit as general content change event this._onDidChangeContent.fire(); + + // Emit as first line change event depending on actual change + if (e.changes.some(change => change.range.startLineNumber === 1 || change.range.endLineNumber === 1)) { + this._onDidChangeFirstLine.fire(); + } } isReadonly(): boolean { diff --git a/src/vs/workbench/contrib/backup/electron-browser/backupOnShutdown.ts b/src/vs/workbench/contrib/backup/electron-browser/backupOnShutdown.ts index e6e17435bc321aa74ef09c180bc7f7d0228100b3..d4c92a5fe760aad8037d0aaf9079472c2c3ad8aa 100644 --- a/src/vs/workbench/contrib/backup/electron-browser/backupOnShutdown.ts +++ b/src/vs/workbench/contrib/backup/electron-browser/backupOnShutdown.ts @@ -17,7 +17,7 @@ import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/ import { isMacintosh } from 'vs/base/common/platform'; import { HotExitConfiguration } from 'vs/platform/files/common/files'; import { IElectronService } from 'vs/platform/electron/node/electron'; -import type { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; +import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; export class BackupOnShutdown extends Disposable implements IWorkbenchContribution { diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts index eefb63885df8f6abef934061bad17b70b48c42c1..199b8f007d716c47965b894262231084267b7550 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts @@ -32,6 +32,7 @@ import { toResource } from 'vs/base/test/common/utils'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILogService } from 'vs/platform/log/common/log'; +import { INewUntitledTextEditorOptions } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); const backupHome = path.join(userdataDir, 'Backups'); @@ -118,16 +119,17 @@ suite('BackupTracker', () => { return [accessor, part, tracker]; } - test('Track backups (untitled)', async function () { - this.timeout(20000); - + async function untitledBackupTest(options?: INewUntitledTextEditorOptions): Promise { const [accessor, part, tracker] = await createTracker(); - const untitledEditor = accessor.textFileService.untitled.create(); + const untitledEditor = accessor.textFileService.untitled.create(options); await accessor.editorService.openEditor(untitledEditor, { pinned: true }); const untitledModel = await untitledEditor.resolve(); - untitledModel.textEditorModel.setValue('Super Good'); + + if (!options?.initialValue) { + untitledModel.textEditorModel.setValue('Super Good'); + } await accessor.backupFileService.joinBackupResource(); @@ -141,6 +143,18 @@ suite('BackupTracker', () => { part.dispose(); tracker.dispose(); + } + + test('Track backups (untitled)', function () { + this.timeout(20000); + + return untitledBackupTest(); + }); + + test('Track backups (untitled with initial contents)', function () { + this.timeout(20000); + + return untitledBackupTest({ initialValue: 'Foo Bar' }); }); test('Track backups (file)', async function () { diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index e90e51bb2a26761e7eb44fc5094c2c69ca90588d..1267fb549f9926a1ec2b49d805d85b86f1e06c9c 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -16,7 +16,6 @@ import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { basename, isEqual } from 'vs/base/common/resources'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { localize } from 'vs/nls'; import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IResourceEditor, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IVisibleEditor, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -587,11 +586,10 @@ export class EditorService extends Disposable implements EditorServiceImpl { if (resourceSideBySideInput.masterResource && resourceSideBySideInput.detailResource) { const masterInput = this.createInput({ resource: resourceSideBySideInput.masterResource, forceFile: resourceSideBySideInput.forceFile }); const detailInput = this.createInput({ resource: resourceSideBySideInput.detailResource, forceFile: resourceSideBySideInput.forceFile }); - const label = resourceSideBySideInput.label || masterInput.getName() || localize('sideBySideLabels', "{0} - {1}", this.toDiffLabel(masterInput), this.toDiffLabel(detailInput)); return new SideBySideEditorInput( - label, - typeof resourceSideBySideInput.description === 'string' ? resourceSideBySideInput.description : masterInput.getDescription(), + resourceSideBySideInput.label || this.toSideBySideLabel(detailInput, masterInput, '-'), + resourceSideBySideInput.description, detailInput, masterInput ); @@ -602,9 +600,13 @@ export class EditorService extends Disposable implements EditorServiceImpl { if (resourceDiffInput.leftResource && resourceDiffInput.rightResource) { const leftInput = this.createInput({ resource: resourceDiffInput.leftResource, forceFile: resourceDiffInput.forceFile }); const rightInput = this.createInput({ resource: resourceDiffInput.rightResource, forceFile: resourceDiffInput.forceFile }); - const label = resourceDiffInput.label || localize('compareLabels', "{0} ↔ {1}", this.toDiffLabel(leftInput), this.toDiffLabel(rightInput)); - return new DiffEditorInput(label, resourceDiffInput.description, leftInput, rightInput); + return new DiffEditorInput( + resourceDiffInput.label || this.toSideBySideLabel(leftInput, rightInput, '↔'), + resourceDiffInput.description, + leftInput, + rightInput + ); } // Untitled file support @@ -687,19 +689,24 @@ export class EditorService extends Disposable implements EditorServiceImpl { return input; } - private toDiffLabel(input: EditorInput): string | undefined { - const res = input.getResource(); - if (!res) { + private toSideBySideLabel(leftInput: EditorInput, rightInput: EditorInput, divider: string): string | undefined { + const leftResource = leftInput.getResource(); + const rightResource = rightInput.getResource(); + + // Without any resource, do not try to compute a label + if (!leftResource || !rightResource) { return undefined; } - // Do not try to extract any paths from simple untitled text editors - if (res.scheme === Schemas.untitled && !this.untitledTextEditorService.hasAssociatedFilePath(res)) { - return input.getName(); + // If both editors are file inputs, we produce an optimized label + // by adding the relative path of both inputs to the label. This + // makes it easier to understand a file-based comparison. + if (this.fileInputFactory.isFileInput(leftInput) && this.fileInputFactory.isFileInput(rightInput)) { + return `${this.labelService.getUriLabel(leftResource, { relative: true })} ${divider} ${this.labelService.getUriLabel(rightResource, { relative: true })}`; } - // Otherwise: for diff labels prefer to see the path as part of the label - return this.labelService.getUriLabel(res, { relative: true }); + // Signal back that the label should be computed from within the editor + return undefined; } //#endregion diff --git a/src/vs/workbench/test/common/editor/untitledTextEditor.test.ts b/src/vs/workbench/test/common/editor/untitledTextEditor.test.ts index 0367c11ee1dcb02329e86989d9bfc1e38ba0bf36..6a0d6eccb1ff2215a33bf4f40b059e0bdcc9f447 100644 --- a/src/vs/workbench/test/common/editor/untitledTextEditor.test.ts +++ b/src/vs/workbench/test/common/editor/untitledTextEditor.test.ts @@ -16,6 +16,8 @@ import { snapshotToString } from 'vs/workbench/services/textfile/common/textfile import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; +import { Range } from 'vs/editor/common/core/range'; class ServiceAccessor { constructor( @@ -328,6 +330,62 @@ suite('Workbench untitled text editors', () => { model.dispose(); }); + test('onDidChangeFirstLine event and input name', async function () { + const service = accessor.untitledTextEditorService; + const input = service.create(); + + let counter = 0; + + let model = await input.resolve(); + model.onDidChangeFirstLine(() => counter++); + + model.textEditorModel.setValue('foo'); + assert.equal(input.getName(), 'foo'); + + assert.equal(counter, 1); + model.textEditorModel.setValue('bar'); + assert.equal(input.getName(), 'bar'); + + assert.equal(counter, 2); + model.textEditorModel.setValue(''); + assert.equal(input.getName(), 'Untitled-1'); + + assert.equal(counter, 3); + + model.textEditorModel.setValue('Hello\nWorld'); + assert.equal(counter, 4); + + function createSingleEditOp(text: string, positionLineNumber: number, positionColumn: number, selectionLineNumber: number = positionLineNumber, selectionColumn: number = positionColumn): IIdentifiedSingleEditOperation { + let range = new Range( + selectionLineNumber, + selectionColumn, + positionLineNumber, + positionColumn + ); + + return { + identifier: null, + range, + text, + forceMoveMarkers: false + }; + } + + model.textEditorModel.applyEdits([createSingleEditOp('hello', 2, 2)]); + assert.equal(counter, 4); // change was not on first line + + input.dispose(); + model.dispose(); + + const inputWithContents = service.create({ initialValue: 'Foo' }); + model = await inputWithContents.resolve(); + + assert.equal(inputWithContents.getName(), 'Foo'); + + inputWithContents.dispose(); + model.dispose(); + }); + test('onDidChangeDirty event', async function () { const service = accessor.untitledTextEditorService; const input = service.create();