提交 3d6f59a3 编写于 作者: T Tony Valderrama 提交者: Benjamin Pasero

Add template system for tab descriptions. (#12965) (#33815)

* Add template system for tab descriptions. (#12965)

* Add a boolean flag to onConfigurationChanged to prevent updates during initialization.

* - Change configuration key to 'workbench.editor.tabSubtitle'.
- Change configuration type to the enum ['default', 'short', 'medium', 'long']
- Move path logic into FileEditorInput.
- Move configuration handling into GroupService/ITabOptions.
- Refactor duplicate path shortening logic.

* Don't consider empty-string descriptions during de-duplication. Don't show subtitle when all descriptions are identical.

* Minor fixes and documentation.
上级 d941eb71
......@@ -204,7 +204,7 @@ export interface IEditorInput extends IDisposable {
/**
* Returns the display description of this input.
*/
getDescription(verbose?: boolean): string;
getDescription(verbosity?: Verbosity): string;
/**
* Returns the display title of this input.
......
......@@ -159,7 +159,8 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
previewEditors: editorConfig.enablePreview,
showIcons: editorConfig.showIcons,
showTabs: editorConfig.showTabs,
tabCloseButton: editorConfig.tabCloseButton
tabCloseButton: editorConfig.tabCloseButton,
tabSubtitleStyle: editorConfig.tabSubtitle,
};
this.revealIfOpen = editorConfig.revealIfOpen;
......@@ -170,7 +171,8 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
previewEditors: true,
showIcons: false,
showTabs: true,
tabCloseButton: 'right'
tabCloseButton: 'right',
tabSubtitleStyle: 'default',
};
this.revealIfOpen = false;
......@@ -221,7 +223,8 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
previewEditors: newPreviewEditors,
showIcons: editorConfig.showIcons,
tabCloseButton: editorConfig.tabCloseButton,
showTabs: this.forceHideTabs ? false : editorConfig.showTabs
showTabs: this.forceHideTabs ? false : editorConfig.showTabs,
tabSubtitleStyle: editorConfig.tabSubtitle,
};
if (!this.doNotFireTabOptionsChanged && !objects.equals(oldTabOptions, this.tabOptions)) {
......
......@@ -47,7 +47,6 @@ import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
interface IEditorInputLabel {
name: string;
hasAmbiguousName?: boolean;
description?: string;
title?: string;
}
......@@ -264,7 +263,7 @@ export class TabsTitleControl extends TitleControl {
// Compute labels and protect against duplicates
const editorsOfGroup = this.context.getEditors();
const labels = this.getUniqueTabLabels(editorsOfGroup);
const labels = this.getTabLabels(editorsOfGroup);
// Tab label and styles
editorsOfGroup.forEach((editor, index) => {
......@@ -276,7 +275,7 @@ export class TabsTitleControl extends TitleControl {
const label = labels[index];
const name = label.name;
const description = label.hasAmbiguousName && label.description ? label.description : '';
const description = label.description || '';
const title = label.title || '';
// Container
......@@ -338,56 +337,100 @@ export class TabsTitleControl extends TitleControl {
this.layout();
}
private getUniqueTabLabels(editors: IEditorInput[]): IEditorInputLabel[] {
const labels: IEditorInputLabel[] = [];
const mapLabelToDuplicates = new Map<string, IEditorInputLabel[]>();
const mapLabelAndDescriptionToDuplicates = new Map<string, IEditorInputLabel[]>();
private getTabLabels(editors: IEditorInput[]): IEditorInputLabel[] {
const tabSubtitleStyle = this.editorGroupService.getTabOptions().tabSubtitleStyle;
const { verbosity, shortenDuplicates } = this.getSubtitleConfigFlags(tabSubtitleStyle);
// Build labels and descriptions for each editor
editors.forEach(editor => {
const name = editor.getName();
let description = editor.getDescription();
if (mapLabelAndDescriptionToDuplicates.has(`${name}${description}`)) {
description = editor.getDescription(true); // try verbose description if name+description already exists
}
type AugmentedLabel = IEditorInputLabel & { editor: IEditorInput };
const labels = editors.map(editor => ({
editor,
name: editor.getName(),
description: editor.getDescription(verbosity),
title: editor.getTitle(Verbosity.LONG)
}));
const item: IEditorInputLabel = {
name,
description,
title: editor.getTitle(Verbosity.LONG)
};
labels.push(item);
if (shortenDuplicates) {
// gather duplicate titles, while filtering out invalid descriptions
const mapTitleToDuplicates = new Map<string, AugmentedLabel[]>();
for (const label of labels) {
if (typeof label.description === 'string' && label.description) {
getOrSet(mapTitleToDuplicates, label.name, []).push(label);
} else {
label.description = '';
}
}
getOrSet(mapLabelToDuplicates, item.name, []).push(item);
// identify duplicate titles and shorten descriptions
mapTitleToDuplicates.forEach(duplicateTitles => {
// remove description if the title isn't duplicated
if (duplicateTitles.length === 1) {
duplicateTitles[0].description = '';
return;
}
if (typeof description === 'string') {
getOrSet(mapLabelAndDescriptionToDuplicates, `${item.name}${item.description}`, []).push(item);
}
});
// identify duplicate descriptions
const mapDescriptionToDuplicates = new Map<string, AugmentedLabel[]>();
for (const label of duplicateTitles) {
getOrSet(mapDescriptionToDuplicates, label.description, []).push(label);
}
// Mark duplicates and shorten their descriptions
mapLabelToDuplicates.forEach(duplicates => {
if (duplicates.length > 1) {
duplicates = duplicates.filter(d => {
// we could have items with equal label and description. in that case it does not make much
// sense to produce a shortened version of the label, so we ignore those kind of items
return typeof d.description === 'string' && mapLabelAndDescriptionToDuplicates.get(`${d.name}${d.description}`).length === 1;
// for editors with duplicate descriptions, check whether any long descriptions differ
let useLongDescriptions = false;
mapDescriptionToDuplicates.forEach((duplicateDescriptions, name) => {
if (!useLongDescriptions && duplicateDescriptions.length > 1) {
const [first, ...rest] = duplicateDescriptions.map(({ editor }) => editor.getDescription(Verbosity.LONG));
useLongDescriptions = rest.some(description => description !== first);
}
});
if (duplicates.length > 1) {
const shortenedDescriptions = shorten(duplicates.map(duplicate => duplicate.description));
duplicates.forEach((duplicate, i) => {
duplicate.description = shortenedDescriptions[i];
duplicate.hasAmbiguousName = true;
// if so, replace all descriptions with long descriptions
if (useLongDescriptions) {
mapDescriptionToDuplicates.clear();
duplicateTitles.forEach(label => {
label.description = label.editor.getDescription(Verbosity.LONG);
getOrSet(mapDescriptionToDuplicates, label.description, []).push(label);
});
}
}
});
// obtain final set of descriptions
const descriptions: string[] = [];
mapDescriptionToDuplicates.forEach((_, description) => descriptions.push(description));
// remove description if all descriptions are identical
if (descriptions.length === 1) {
for (const label of mapDescriptionToDuplicates.get(descriptions[0])) {
label.description = '';
}
return;
}
// shorten descriptions
const shortenedDescriptions = shorten(descriptions);
descriptions.forEach((description, i) => {
for (const label of mapDescriptionToDuplicates.get(description)) {
label.description = shortenedDescriptions[i];
}
});
});
}
return labels;
}
private getSubtitleConfigFlags(value: string) {
switch (value) {
case 'short':
return { verbosity: Verbosity.SHORT, shortenDuplicates: false };
case 'medium':
return { verbosity: Verbosity.MEDIUM, shortenDuplicates: false };
case 'long':
return { verbosity: Verbosity.LONG, shortenDuplicates: false };
default:
return { verbosity: Verbosity.MEDIUM, shortenDuplicates: true };
}
}
protected doRefresh(): void {
const group = this.context;
const editor = group && group.activeEditor;
......
......@@ -176,7 +176,7 @@ export abstract class EditorInput implements IEditorInput {
* Returns the description of this input that can be shown to the user. Examples include showing the description of
* the input above the editor area to the side of the name of the input.
*/
public getDescription(): string {
public getDescription(verbosity?: Verbosity): string {
return null;
}
......@@ -806,7 +806,8 @@ export interface IWorkbenchEditorConfiguration {
closeOnFileDelete: boolean;
openPositioning: 'left' | 'right' | 'first' | 'last';
revealIfOpen: boolean;
swipeToNavigate: boolean
swipeToNavigate: boolean,
tabSubtitle: 'default' | 'short' | 'medium' | 'long';
}
};
}
......
......@@ -129,6 +129,23 @@ let workbenchProperties: { [path: string]: IJSONSchema; } = {
'description': nls.localize('showEditorTabs', "Controls if opened editors should show in tabs or not."),
'default': true
},
'workbench.editor.tabSubtitle': {
'type': 'string',
'enum': ['default', 'short', 'medium', 'long'],
'enumDescriptions': [
nls.localize('workbench.editor.tabSubtitle.default', "When two files have the same name, shows the distinguinshing sections of each file's path."),
nls.localize('workbench.editor.tabSubtitle.short', "Always shows the directory which contains the file."),
nls.localize('workbench.editor.tabSubtitle.medium', "Always shows the file's path relative to the workspace root."),
nls.localize('workbench.editor.tabSubtitle.long', "Always shows the file's absolute path.")
],
'default': 'default',
'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by parenthesis are not to be translated.'], key: 'tabDescription' },
`Controls the format of the subtitle for an editor tab. Subtitles show up next to the file name depending on this setting and make it easier to understand the location of a file:
- short: 'parent'
- medium: 'workspace/src/parent'
- long: '/home/user/workspace/src/parent'
- default: '.../parent', when another tab shares the same title`),
},
'workbench.editor.tabCloseButton': {
'type': 'string',
'enum': ['left', 'right', 'off'],
......
......@@ -32,8 +32,10 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
private textModelReference: TPromise<IReference<ITextEditorModel>>;
private name: string;
private description: string;
private verboseDescription: string;
private shortDescription: string;
private mediumDescription: string;
private longDescription: string;
private shortTitle: string;
private mediumTitle: string;
......@@ -128,18 +130,25 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
return this.decorateOrphanedFiles(this.name);
}
public getDescription(verbose?: boolean): string {
if (verbose) {
if (!this.verboseDescription) {
this.verboseDescription = labels.getPathLabel(paths.dirname(this.resource.fsPath), void 0, this.environmentService);
}
} else {
if (!this.description) {
this.description = labels.getPathLabel(paths.dirname(this.resource.fsPath), this.contextService, this.environmentService);
}
public getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string {
switch (verbosity) {
case Verbosity.SHORT:
if (!this.shortDescription) {
this.shortDescription = paths.basename(labels.getPathLabel(paths.dirname(this.resource.fsPath), void 0, this.environmentService));
}
return this.shortDescription;
case Verbosity.LONG:
if (!this.longDescription) {
this.longDescription = labels.getPathLabel(paths.dirname(this.resource.fsPath), void 0, this.environmentService);
}
return this.longDescription;
case Verbosity.MEDIUM:
default:
if (!this.mediumDescription) {
this.mediumDescription = labels.getPathLabel(paths.dirname(this.resource.fsPath), this.contextService, this.environmentService);
}
return this.mediumDescription;
}
return verbose ? this.verboseDescription : this.description;
}
public getTitle(verbosity: Verbosity): string {
......@@ -274,4 +283,4 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
return false;
}
}
}
\ No newline at end of file
......@@ -24,6 +24,7 @@ export interface ITabOptions {
tabCloseButton?: 'left' | 'right' | 'off';
showIcons?: boolean;
previewEditors?: boolean;
tabSubtitleStyle?: 'default' | 'short' | 'medium' | 'long';
}
export interface IMoveOptions {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册