From 7f0836534e5ed0665ff311a8d6d6f45b533807c7 Mon Sep 17 00:00:00 2001 From: fangnx Date: Thu, 18 Jul 2019 16:35:43 -0400 Subject: [PATCH] Fixed #4897: supported tab details to disambiguate identical names - 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 --- packages/core/src/browser/shell/tab-bars.ts | 101 +++++++++++++++++++- packages/core/src/browser/style/tabs.css | 13 +++ 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts index 68e9632fa7c94..9dea9ca084993 100644 --- a/packages/core/src/browser/shell/tab-bars.ts +++ b/packages/core/src/browser/shell/tab-bars.ts @@ -81,8 +81,9 @@ export class TabBarRenderer extends TabBar.Renderer { /** * Render tabs with the default DOM structure, but additionally register a context * menu listener. + * @param {boolean} [isInSidePanel] - Whether the label 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); @@ -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) ); } @@ -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 {boolean} [isInSidePanel] - Whether the label 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; @@ -152,7 +154,96 @@ export class TabBarRenderer extends TabBar.Renderer { top = `${paddingTop + iconHeight}px`; } const style: ElementInlineStyle = { width, height, top }; - return h.div({ className: 'p-TabBar-tabLabel', style }, data.title.label); + if (isInSidePanel || (this.tabBar && this.tabBar.titles.length < 2)) { + return h.div({ className: 'p-TabBar-tabLabel', style }, data.title.label); + } + const originalToDisplayedMap = this.findDuplicateLabels(); + const labelDetails: string | undefined = originalToDisplayedMap.get(data.title.caption); + return (labelDetails) + ? h.div({ className: 'p-TabBar-tabLabelWrapper' }, + h.div({ className: 'p-TabBar-tabLabel', style }, data.title.label), + h.div({ className: 'p-TabBar-tabLabelDetails', style }, labelDetails)) + : 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/... + * @returns {Map} A map from each tab's original path to its displayed partial path. + */ + findDuplicateLabels(): Map { + const titles = this.tabBar!.titles; + // Filter from all tabs to group them by the distinct label (file name). + const labelGroups = new Map>(); + titles.forEach((title, index) => { + if (!labelGroups.has(title.label)) { + labelGroups.set(title.label, new Map()); + } + labelGroups.get(title.label)!.set(index, title.caption); + }); + + const originalToDisplayedMap = new Map(); + // 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); + }); + + let i = 0; + while (i < maxPathLength - 1) { + // Store indexes of all paths that have the identical element in each step. + const stepOccurrences = new Map(); + // 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, new Array()); + stepOccurrences.get(step)!.push(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; } /** @@ -550,7 +641,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); } diff --git a/packages/core/src/browser/style/tabs.css b/packages/core/src/browser/style/tabs.css index 2317d8567b7d0..ae26d8831aa05 100644 --- a/packages/core/src/browser/style/tabs.css +++ b/packages/core/src/browser/style/tabs.css @@ -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;