Skip to content

Commit

Permalink
Fixed #4897: supported tab details to disambiguate identical names
Browse files Browse the repository at this point in the history
- Supported additional tab details for tabs with identical titles.
- If there existed multiple tabs with the same name, each tab would have its partial relative path displayed after the title.

Signed-off-by: fangnx <[email protected]>
  • Loading branch information
fangnx committed Aug 15, 2019
1 parent e3dfbea commit 8a6cae4
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 6 deletions.
63 changes: 63 additions & 0 deletions packages/core/src/browser/shell/tab-bars.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/********************************************************************************
* Copyright (C) 2019 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { enableJSDOM } from '../test/jsdom';
let disableJSDOM = enableJSDOM();
import { expect } from 'chai';

import { Title, Widget } from '@phosphor/widgets';
import { TabBarRenderer } from './tab-bars';

disableJSDOM();

describe('tab bar', () => {

before(() => {
disableJSDOM = enableJSDOM();
});

after(() => {
disableJSDOM();
});

it('should disambiguate tabs that have identical names', () => {
const tabBar = new TabBarRenderer();
const owner = new Widget();

const tabLabels: string[] = ['index.ts', 'index.ts', 'index.ts', 'main.ts', 'main.ts', 'main.ts', 'uniqueFile.ts'];
const tabPaths: string[] = [
'root1/src/foo/bar/index.ts',
'root1/lib/foo/bar/index.ts',
'root2/src/foo/goo/bar/index.ts',
'root1/aaa/main.ts',
'root1/aaa/bbb/main.ts',
'root1/aaa/bbb/ccc/main.ts',
'root1/src/foo/bar/uniqueFile.ts'
];
const tabs: Title<Widget>[] = tabLabels.map((label, i) => new Title<Widget>({
owner, label, caption: tabPaths[i]
}));
const pathMap = tabBar.findDuplicateLabels(tabs);

expect(pathMap.get(tabPaths[0])).to.be.equal('.../src/...');
expect(pathMap.get(tabPaths[1])).to.be.equal('.../lib/...');
expect(pathMap.get(tabPaths[2])).to.be.equal('root2/...');
expect(pathMap.get(tabPaths[3])).to.be.equal('root1/aaa');
expect(pathMap.get(tabPaths[4])).to.be.equal('root1/aaa/bbb');
expect(pathMap.get(tabPaths[5])).to.be.equal('.../ccc');
expect(pathMap.get(tabPaths[6])).to.be.equal(undefined);
});
});
111 changes: 105 additions & 6 deletions packages/core/src/browser/shell/tab-bars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,11 @@ export class TabBarRenderer extends TabBar.Renderer {
}

/**
* Render tabs with the default DOM structure, but additionally register a context
* menu listener.
* Render tabs with the default DOM structure, but additionally register a context menu listener.
* @param data {SideBarRenderData} data used to render the tab.
* @param isInSidePanel {boolean} an optional check which determines if the tab is in the side-panel.
*/
renderTab(data: SideBarRenderData): VirtualElement {
renderTab(data: SideBarRenderData, isInSidePanel?: boolean): VirtualElement {
const title = data.title;
const id = this.createTabId(data.title);
const key = this.createTabKey(data);
Expand All @@ -96,7 +97,7 @@ export class TabBarRenderer extends TabBar.Renderer {
ondblclick: this.handleDblClickEvent
},
this.renderIcon(data),
this.renderLabel(data),
this.renderLabel(data, isInSidePanel),
this.renderCloseIcon(data)
);
}
Expand Down Expand Up @@ -131,8 +132,9 @@ export class TabBarRenderer extends TabBar.Renderer {
/**
* If size information is available for the label, set it as inline style. Tab padding
* and icon size are also considered in the `top` position.
* @param isInSidePanel {boolean} an optional check which determines if the tab is in the side-panel.
*/
renderLabel(data: SideBarRenderData): VirtualElement {
renderLabel(data: SideBarRenderData, isInSidePanel?: boolean): VirtualElement {
const labelSize = data.labelSize;
const iconSize = data.iconSize;
let width: string | undefined;
Expand All @@ -152,9 +154,106 @@ export class TabBarRenderer extends TabBar.Renderer {
top = `${paddingTop + iconHeight}px`;
}
const style: ElementInlineStyle = { width, height, top };
// No need to check for duplicate labels if the tab is rendered in the side panel (title is not displayed)
// or if there are less than two files in the tab bar.
if (isInSidePanel || (this.tabBar && this.tabBar.titles.length < 2)) {
return h.div({ className: 'p-TabBar-tabLabel', style }, data.title.label);
}
const originalToDisplayedMap = this.findDuplicateLabels([...this.tabBar!.titles]);
const labelDetails: string | undefined = originalToDisplayedMap.get(data.title.caption);
if (labelDetails) {
return h.div({ className: 'p-TabBar-tabLabelWrapper' },
h.div({ className: 'p-TabBar-tabLabel', style }, data.title.label),
h.div({ className: 'p-TabBar-tabLabelDetails', style }, labelDetails));
}
return h.div({ className: 'p-TabBar-tabLabel', style }, data.title.label);
}

/**
* Find duplicate labels from the currently opened tabs in the tab bar.
* Return the approriate partial paths that can distinguish the identical labels.
*
* E.g., a/p/index.ts => a/..., b/p/index.ts => b/...
*
* To prevent excessively long path displayed, show at maximum three levels from the end by default.
* @param {Title<Widget>[]} titles - array of titles in the current tab bar.
* @returns {Map<string, string>} A map from each tab's original path to its displayed partial path.
*/
findDuplicateLabels(titles: Title<Widget>[]): Map<string, string> {
// Filter from all tabs to group them by the distinct label (file name).
// E.g., 'foo.js' => {0 (index) => 'a/b/foo.js', '2 => a/c/foo.js' },
// 'bar.js' => {1 => 'a/d/bar.js', ...}
const labelGroups = new Map<string, Map<number, string>>();
titles.forEach((title, index) => {
if (!labelGroups.has(title.label)) {
labelGroups.set(title.label, new Map<number, string>());
}
labelGroups.get(title.label)!.set(index, title.caption);
});

const originalToDisplayedMap = new Map<string, string>();
// Parse each group of editors with the same label.
labelGroups.forEach(labelGroup => {
// Filter to get groups that have duplicates.
if (labelGroup.size > 1) {
const paths: string[][] = [];
let maxPathLength = 0;
labelGroup.forEach((pathStr, index) => {
const steps = pathStr.split('/');
maxPathLength = Math.max(maxPathLength, steps.length);
paths[index] = (steps.slice(0, steps.length - 1));
// By default, show at maximum three levels from the end.
let defaultDisplayedPath = steps.slice(-4, -1).join('/');
if (steps.length > 4) {
defaultDisplayedPath = '.../' + defaultDisplayedPath;
}
originalToDisplayedMap.set(pathStr, defaultDisplayedPath);
});

// Iterate through the steps of the path from the left to find the step that can distinguish it.
// E.g., ['root', 'foo', 'c'], ['root', 'bar', 'd'] => 'foo', 'bar'
let i = 0;
while (i < maxPathLength - 1) {
// Store indexes of all paths that have the identical element in each step.
const stepOccurrences = new Map<string, number[]>();
// Compare the current step of all paths
paths.forEach((path, index) => {
const step = path[i];
if (path.length > 0) {
if (i > path.length - 1) {
paths[index] = [];
} else if (!stepOccurrences.has(step)) {
stepOccurrences.set(step, [index]);
} else {
stepOccurrences.get(step)!.push(index);
}
}
});
// Set the displayed path for each tab.
stepOccurrences.forEach((indexArr, displayedPath) => {
if (indexArr.length === 1) {
const originalPath = labelGroup.get(indexArr[0]);
if (originalPath) {
const originalElements = originalPath.split('/');
const displayedElements = displayedPath.split('/');
if (originalElements.slice(-2)[0] !== displayedElements.slice(-1)[0]) {
displayedPath += '/...';
}
if (originalElements[0] !== displayedElements[0]) {
displayedPath = '.../' + displayedPath;
}
originalToDisplayedMap.set(originalPath, displayedPath);
paths[indexArr[0]] = [];
}
}
});
i++;
}
}
});
return originalToDisplayedMap;
}

/**
* If size information is available for the icon, set it as inline style. Tab padding
* is also considered in the `top` position.
Expand Down Expand Up @@ -550,7 +649,7 @@ export class SideTabBar extends ScrollableTabBar {
} else {
rd = { title, current, zIndex };
}
content[i] = renderer.renderTab(rd);
content[i] = renderer.renderTab(rd, true);
}
VirtualDOM.render(content, host);
}
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/browser/style/tabs.css
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,23 @@

.p-TabBar.theia-app-centers .p-TabBar-tabIcon,
.p-TabBar.theia-app-centers .p-TabBar-tabLabel,
.p-TabBar.theia-app-centers .p-TabBar-tabLabelDetails,
.p-TabBar.theia-app-centers .p-TabBar-tabCloseIcon {
display: inline-block;
}

.p-TabBar.theia-app-centers .p-TabBar-tabLabelDetails {
margin-left: 5px;
color: var(--theia-ui-font-color2);
flex: 1 1 auto;
overflow: hidden;
white-space: nowrap;
}

.p-TabBar.theia-app-centers .p-TabBar-tabLabelWrapper {
display: flex;
}

.p-TabBar-tab-secondary-label {
color: var(--theia-brand-color2);
cursor: pointer;
Expand Down

0 comments on commit 8a6cae4

Please sign in to comment.