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

Unsaved tab labels should preview text instead of "Untitled-#" (fixes #37414)

上级 c09fa216
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
import * as nls from 'vs/nls'; import * as nls from 'vs/nls';
import { Action } from 'vs/base/common/actions'; import { Action } from 'vs/base/common/actions';
import { mixin } from 'vs/base/common/objects'; 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 { QuickOpenEntryGroup } from 'vs/base/parts/quickopen/browser/quickOpenModel';
import { EditorQuickOpenEntry, EditorQuickOpenEntryGroup, IEditorQuickOpenEntry, QuickOpenAction } from 'vs/workbench/browser/quickopen'; import { EditorQuickOpenEntry, EditorQuickOpenEntryGroup, IEditorQuickOpenEntry, QuickOpenAction } from 'vs/workbench/browser/quickopen';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
...@@ -23,7 +23,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; ...@@ -23,7 +23,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle';
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; 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 { export class ExecuteCommandAction extends Action {
...@@ -640,23 +640,24 @@ export abstract class BaseCloseAllAction extends Action { ...@@ -640,23 +640,24 @@ export abstract class BaseCloseAllAction extends Action {
return undefined; return undefined;
})); }));
const dirtyEditorsToConfirmByName = new Set<string>(); const dirtyEditorsToConfirm = new Set<string>();
const dirtyEditorsToConfirmByResource = new ResourceMap();
for (const editor of this.editorService.editors) { for (const editor of this.editorService.editors) {
if (!editor.isDirty() || editor.isSaving()) { if (!editor.isDirty() || editor.isSaving()) {
continue; // only interested in dirty editors (unless in the process of saving) continue; // only interested in dirty editors (unless in the process of saving)
} }
const resource = editor.getResource(); let name: string;
if (resource) { if (editor instanceof SideBySideEditorInput) {
dirtyEditorsToConfirmByResource.set(resource, true); name = editor.master.getName(); // prefer shorter names by using master's name in this case
} else { } 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) { if (confirm === ConfirmResult.CANCEL) {
return; return;
} }
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
import 'vs/css!./media/editorgroupview'; import 'vs/css!./media/editorgroupview';
import { EditorGroup, IEditorOpenOptions, EditorCloseEvent, ISerializedEditorGroup, isSerializedEditorGroup } from 'vs/workbench/common/editor/editorGroup'; 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 { Event, Emitter, Relay } from 'vs/base/common/event';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; 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'; 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 { ...@@ -1306,8 +1306,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
// Switch to editor that we want to handle and confirm to save/revert // Switch to editor that we want to handle and confirm to save/revert
await this.openEditor(editor); await this.openEditor(editor);
const editorResource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); let name: string;
const res = await this.fileDialogService.showSaveConfirm(editorResource ? [editorResource] : [editor.getName()]); 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 // 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. // again to see if anything needs to happen before closing for good.
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * 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 { Event, Emitter } from 'vs/base/common/event';
import { assign } from 'vs/base/common/objects'; import { assign } from 'vs/base/common/objects';
import { withNullAsUndefined, assertIsDefined } from 'vs/base/common/types'; import { withNullAsUndefined, assertIsDefined } from 'vs/base/common/types';
...@@ -682,7 +683,7 @@ export class SideBySideEditorInput extends EditorInput { ...@@ -682,7 +683,7 @@ export class SideBySideEditorInput extends EditorInput {
static readonly ID: string = 'workbench.editorinputs.sidebysideEditorInput'; static readonly ID: string = 'workbench.editorinputs.sidebysideEditorInput';
constructor( constructor(
private readonly name: string, protected readonly name: string | undefined,
private readonly description: string | undefined, private readonly description: string | undefined,
private readonly _details: EditorInput, private readonly _details: EditorInput,
private readonly _master: EditorInput private readonly _master: EditorInput
...@@ -700,6 +701,22 @@ export class SideBySideEditorInput extends EditorInput { ...@@ -700,6 +701,22 @@ export class SideBySideEditorInput extends EditorInput {
return this._details; 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 { isReadonly(): boolean {
return this.master.isReadonly(); return this.master.isReadonly();
} }
...@@ -760,18 +777,6 @@ export class SideBySideEditorInput extends EditorInput { ...@@ -760,18 +777,6 @@ export class SideBySideEditorInput extends EditorInput {
return null; return null;
} }
getTypeId(): string {
return SideBySideEditorInput.ID;
}
getName(): string {
return this.name;
}
getDescription(): string | undefined {
return this.description;
}
matches(otherInput: unknown): boolean { matches(otherInput: unknown): boolean {
if (super.matches(otherInput) === true) { if (super.matches(otherInput) === true) {
return true; return true;
......
...@@ -7,6 +7,7 @@ import { EditorModel, EditorInput, SideBySideEditorInput, TEXT_DIFF_EDITOR_ID, B ...@@ -7,6 +7,7 @@ import { EditorModel, EditorInput, SideBySideEditorInput, TEXT_DIFF_EDITOR_ID, B
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
import { DiffEditorModel } from 'vs/workbench/common/editor/diffEditorModel'; import { DiffEditorModel } from 'vs/workbench/common/editor/diffEditorModel';
import { TextDiffEditorModel } from 'vs/workbench/common/editor/textDiffEditorModel'; 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 * 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 { ...@@ -19,33 +20,25 @@ export class DiffEditorInput extends SideBySideEditorInput {
private cachedModel: DiffEditorModel | null = null; private cachedModel: DiffEditorModel | null = null;
constructor( constructor(
name: string, protected name: string | undefined,
description: string | undefined, description: string | undefined,
original: EditorInput, public readonly originalInput: EditorInput,
modified: EditorInput, public readonly modifiedInput: EditorInput,
private readonly forceOpenAsBinary?: boolean private readonly forceOpenAsBinary?: boolean
) { ) {
super(name, description, original, modified); super(name, description, originalInput, modifiedInput);
}
matches(otherInput: unknown): boolean {
if (!super.matches(otherInput)) {
return false;
}
return otherInput instanceof DiffEditorInput && otherInput.forceOpenAsBinary === this.forceOpenAsBinary;
} }
getTypeId(): string { getTypeId(): string {
return DiffEditorInput.ID; return DiffEditorInput.ID;
} }
get originalInput(): EditorInput { getName(): string {
return this.details; if (!this.name) {
return localize('sideBySideLabels', "{0} ↔ {1}", this.originalInput.getName(), this.modifiedInput.getName());
} }
get modifiedInput(): EditorInput { return this.name;
return this.master;
} }
async resolve(): Promise<EditorModel> { async resolve(): Promise<EditorModel> {
...@@ -88,6 +81,14 @@ export class DiffEditorInput extends SideBySideEditorInput { ...@@ -88,6 +81,14 @@ export class DiffEditorInput extends SideBySideEditorInput {
return new DiffEditorModel(originalEditorModel, modifiedEditorModel); 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 { dispose(): void {
// Free the diff editor model but do not propagate the dispose() call to the two inputs // Free the diff editor model but do not propagate the dispose() call to the two inputs
......
...@@ -28,10 +28,14 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin ...@@ -28,10 +28,14 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin
private static readonly MEMOIZER = createMemoizer(); private static readonly MEMOIZER = createMemoizer();
private static readonly FIRST_LINE_MAX_TITLE_LENGTH = 50;
private readonly _onDidModelChangeEncoding = this._register(new Emitter<void>()); private readonly _onDidModelChangeEncoding = this._register(new Emitter<void>());
readonly onDidModelChangeEncoding = this._onDidModelChangeEncoding.event; readonly onDidModelChangeEncoding = this._onDidModelChangeEncoding.event;
private cachedModel: UntitledTextEditorModel | null = null; private cachedModel: UntitledTextEditorModel | null = null;
private cachedModelFirstLine: string | undefined = undefined;
private modelResolve: Promise<UntitledTextEditorModel & IResolvedTextEditorModel> | null = null; private modelResolve: Promise<UntitledTextEditorModel & IResolvedTextEditorModel> | null = null;
private preferredMode: string | undefined; private preferredMode: string | undefined;
...@@ -71,6 +75,15 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin ...@@ -71,6 +75,15 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin
} }
getName(): string { 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; return this.hasAssociatedFilePath ? basenameOrAuthority(this.resource) : this.resource.path;
} }
...@@ -278,11 +291,30 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin ...@@ -278,11 +291,30 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin
private createModel(): UntitledTextEditorModel { private createModel(): UntitledTextEditorModel {
const model = this._register(this.instantiationService.createInstance(UntitledTextEditorModel, this.preferredMode, this.resource, this.hasAssociatedFilePath, this.initialValue, this.preferredEncoding)); 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 // re-emit some events from the model
this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire()));
this._register(model.onDidChangeEncoding(() => this._onDidModelChangeEncoding.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 { matches(otherInput: unknown): boolean {
......
...@@ -16,6 +16,7 @@ import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; ...@@ -16,6 +16,7 @@ import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
import { IResolvedTextEditorModel, ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IResolvedTextEditorModel, ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; 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 { } export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport, IEncodingSupport, IWorkingCopy { }
...@@ -24,6 +25,9 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt ...@@ -24,6 +25,9 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt
private readonly _onDidChangeContent = this._register(new Emitter<void>()); private readonly _onDidChangeContent = this._register(new Emitter<void>());
readonly onDidChangeContent = this._onDidChangeContent.event; readonly onDidChangeContent = this._onDidChangeContent.event;
private readonly _onDidChangeFirstLine = this._register(new Emitter<void>());
readonly onDidChangeFirstLine = this._onDidChangeFirstLine.event;
private readonly _onDidChangeDirty = this._register(new Emitter<void>()); private readonly _onDidChangeDirty = this._register(new Emitter<void>());
readonly onDidChangeDirty = this._onDidChangeDirty.event; readonly onDidChangeDirty = this._onDidChangeDirty.event;
...@@ -170,15 +174,22 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt ...@@ -170,15 +174,22 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt
const textEditorModel = this.textEditorModel!; const textEditorModel = this.textEditorModel!;
// Listen to content changes // Listen to content changes
this._register(textEditorModel.onDidChangeContent(() => this.onModelContentChanged())); this._register(textEditorModel.onDidChangeContent(e => this.onModelContentChanged(e)));
// Listen to mode changes // Listen to mode changes
this._register(textEditorModel.onDidChangeLanguage(() => this.onConfigurationChange())); // mode change can have impact on config 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; return this as UntitledTextEditorModel & IResolvedTextEditorModel;
} }
private onModelContentChanged(): void { private onModelContentChanged(e: IModelContentChangedEvent): void {
if (!this.isResolved()) { if (!this.isResolved()) {
return; return;
} }
...@@ -196,8 +207,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt ...@@ -196,8 +207,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt
this.setDirty(true); this.setDirty(true);
} }
// Emit as event // Emit as general content change event
this._onDidChangeContent.fire(); 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 { isReadonly(): boolean {
......
...@@ -17,7 +17,7 @@ import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/ ...@@ -17,7 +17,7 @@ import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/
import { isMacintosh } from 'vs/base/common/platform'; import { isMacintosh } from 'vs/base/common/platform';
import { HotExitConfiguration } from 'vs/platform/files/common/files'; import { HotExitConfiguration } from 'vs/platform/files/common/files';
import { IElectronService } from 'vs/platform/electron/node/electron'; 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 { export class BackupOnShutdown extends Disposable implements IWorkbenchContribution {
......
...@@ -32,6 +32,7 @@ import { toResource } from 'vs/base/test/common/utils'; ...@@ -32,6 +32,7 @@ import { toResource } from 'vs/base/test/common/utils';
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { ILogService } from 'vs/platform/log/common/log'; 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 userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer');
const backupHome = path.join(userdataDir, 'Backups'); const backupHome = path.join(userdataDir, 'Backups');
...@@ -118,16 +119,17 @@ suite('BackupTracker', () => { ...@@ -118,16 +119,17 @@ suite('BackupTracker', () => {
return [accessor, part, tracker]; return [accessor, part, tracker];
} }
test('Track backups (untitled)', async function () { async function untitledBackupTest(options?: INewUntitledTextEditorOptions): Promise<void> {
this.timeout(20000);
const [accessor, part, tracker] = await createTracker(); 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 }); await accessor.editorService.openEditor(untitledEditor, { pinned: true });
const untitledModel = await untitledEditor.resolve(); const untitledModel = await untitledEditor.resolve();
if (!options?.initialValue) {
untitledModel.textEditorModel.setValue('Super Good'); untitledModel.textEditorModel.setValue('Super Good');
}
await accessor.backupFileService.joinBackupResource(); await accessor.backupFileService.joinBackupResource();
...@@ -141,6 +143,18 @@ suite('BackupTracker', () => { ...@@ -141,6 +143,18 @@ suite('BackupTracker', () => {
part.dispose(); part.dispose();
tracker.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 () { test('Track backups (file)', async function () {
......
...@@ -16,7 +16,6 @@ import { Event, Emitter } from 'vs/base/common/event'; ...@@ -16,7 +16,6 @@ import { Event, Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { basename, isEqual } from 'vs/base/common/resources'; import { basename, isEqual } from 'vs/base/common/resources';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; 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 { 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 { 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'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
...@@ -587,11 +586,10 @@ export class EditorService extends Disposable implements EditorServiceImpl { ...@@ -587,11 +586,10 @@ export class EditorService extends Disposable implements EditorServiceImpl {
if (resourceSideBySideInput.masterResource && resourceSideBySideInput.detailResource) { if (resourceSideBySideInput.masterResource && resourceSideBySideInput.detailResource) {
const masterInput = this.createInput({ resource: resourceSideBySideInput.masterResource, forceFile: resourceSideBySideInput.forceFile }); const masterInput = this.createInput({ resource: resourceSideBySideInput.masterResource, forceFile: resourceSideBySideInput.forceFile });
const detailInput = this.createInput({ resource: resourceSideBySideInput.detailResource, 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( return new SideBySideEditorInput(
label, resourceSideBySideInput.label || this.toSideBySideLabel(detailInput, masterInput, '-'),
typeof resourceSideBySideInput.description === 'string' ? resourceSideBySideInput.description : masterInput.getDescription(), resourceSideBySideInput.description,
detailInput, detailInput,
masterInput masterInput
); );
...@@ -602,9 +600,13 @@ export class EditorService extends Disposable implements EditorServiceImpl { ...@@ -602,9 +600,13 @@ export class EditorService extends Disposable implements EditorServiceImpl {
if (resourceDiffInput.leftResource && resourceDiffInput.rightResource) { if (resourceDiffInput.leftResource && resourceDiffInput.rightResource) {
const leftInput = this.createInput({ resource: resourceDiffInput.leftResource, forceFile: resourceDiffInput.forceFile }); const leftInput = this.createInput({ resource: resourceDiffInput.leftResource, forceFile: resourceDiffInput.forceFile });
const rightInput = this.createInput({ resource: resourceDiffInput.rightResource, 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 // Untitled file support
...@@ -687,19 +689,24 @@ export class EditorService extends Disposable implements EditorServiceImpl { ...@@ -687,19 +689,24 @@ export class EditorService extends Disposable implements EditorServiceImpl {
return input; return input;
} }
private toDiffLabel(input: EditorInput): string | undefined { private toSideBySideLabel(leftInput: EditorInput, rightInput: EditorInput, divider: string): string | undefined {
const res = input.getResource(); const leftResource = leftInput.getResource();
if (!res) { const rightResource = rightInput.getResource();
// Without any resource, do not try to compute a label
if (!leftResource || !rightResource) {
return undefined; return undefined;
} }
// Do not try to extract any paths from simple untitled text editors // If both editors are file inputs, we produce an optimized label
if (res.scheme === Schemas.untitled && !this.untitledTextEditorService.hasAssociatedFilePath(res)) { // by adding the relative path of both inputs to the label. This
return input.getName(); // 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 // Signal back that the label should be computed from within the editor
return this.labelService.getUriLabel(res, { relative: true }); return undefined;
} }
//#endregion //#endregion
......
...@@ -16,6 +16,8 @@ import { snapshotToString } from 'vs/workbench/services/textfile/common/textfile ...@@ -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 { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; 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 { class ServiceAccessor {
constructor( constructor(
...@@ -328,6 +330,62 @@ suite('Workbench untitled text editors', () => { ...@@ -328,6 +330,62 @@ suite('Workbench untitled text editors', () => {
model.dispose(); 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 () { test('onDidChangeDirty event', async function () {
const service = accessor.untitledTextEditorService; const service = accessor.untitledTextEditorService;
const input = service.create(); const input = service.create();
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册