Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 72 additions & 5 deletions src/vs/workbench/contrib/files/browser/views/explorerView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext
import { FileCopiedContext, NEW_FILE_COMMAND_ID, NEW_FOLDER_COMMAND_ID } from '../fileActions.js';
import * as DOM from '../../../../../base/browser/dom.js';
import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js';
import { IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js';
import { isUntitledWorkspace, IWorkspace, IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js';
import { IConfigurationService, IConfigurationChangeEvent } from '../../../../../platform/configuration/common/configuration.js';
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
Expand All @@ -38,7 +38,7 @@ import { IAsyncDataTreeViewState } from '../../../../../base/browser/ui/tree/asy
import { FuzzyScore } from '../../../../../base/common/filters.js';
import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';
import { IFileService, FileSystemProviderCapabilities } from '../../../../../platform/files/common/files.js';
import { IDisposable } from '../../../../../base/common/lifecycle.js';
import { IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
import { Event } from '../../../../../base/common/event.js';
import { IViewDescriptorService } from '../../../../common/views.js';
import { IViewsService } from '../../../../services/views/common/viewsService.js';
Expand All @@ -54,6 +54,7 @@ import { ResourceMap } from '../../../../../base/common/map.js';
import { AbstractTreePart } from '../../../../../base/browser/ui/tree/abstractTree.js';
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';
import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';


function hasExpandedRootChild(tree: WorkbenchCompressibleAsyncDataTree<ExplorerItem | ExplorerItem[], ExplorerItem, FuzzyScore>, treeInput: ExplorerItem[]): boolean {
Expand Down Expand Up @@ -149,7 +150,32 @@ export interface IExplorerViewPaneOptions extends IViewPaneOptions {
delegate: IExplorerViewContainerDelegate;
}

/**
* Marks the Explorer pane header as showing a name the user chose.
*/
export const PRESERVE_WORKSPACE_NAME_CASE_CLASS = 'preserve-workspace-name-case';

/**
* Marks the part hosting the Explorer as showing a name the user chose in its
* merged (single view) title.
*/
export const PRESERVE_MERGED_WORKSPACE_NAME_CASE_CLASS = 'preserve-merged-workspace-name-case';

/**
* Whether the Explorer title shows a name the user provided and therefore has to
* be rendered with its original casing. Untitled workspaces show a generated
* label and empty workbenches show a static label, so both keep the default casing.
*/
export function shouldPreserveWorkspaceNameCase(workbenchState: WorkbenchState, workspace: IWorkspace, environmentService: IEnvironmentService): boolean {
if (workbenchState === WorkbenchState.EMPTY) {
return false;
}

return !workspace.configuration || !isUntitledWorkspace(workspace.configuration, environmentService);
}

export class ExplorerView extends ViewPane implements IExplorerView {

static readonly TREE_VIEW_STATE_STORAGE_KEY: string = 'workbench.explorer.treeViewState';

private tree!: WorkbenchCompressibleAsyncDataTree<ExplorerItem | ExplorerItem[], ExplorerItem, FuzzyScore>;
Expand Down Expand Up @@ -182,6 +208,7 @@ export class ExplorerView extends ViewPane implements IExplorerView {
private dragHandler!: DelayedDragHandler;
private _autoReveal: boolean | 'force' | 'focusNoScroll' = false;
private readonly delegate: IExplorerViewContainerDelegate | undefined;
private workspaceTitleContainer: HTMLElement | undefined;

override get singleViewPaneContainerTitle(): string {
return this.name;
Expand Down Expand Up @@ -211,7 +238,8 @@ export class ExplorerView extends ViewPane implements IExplorerView {
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@ICommandService private readonly commandService: ICommandService,
@IOpenerService openerService: IOpenerService,
@IAccessibilityService private readonly accessibilityService: IAccessibilityService
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
@IEnvironmentService private readonly environmentService: IEnvironmentService
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);

Expand All @@ -231,8 +259,8 @@ export class ExplorerView extends ViewPane implements IExplorerView {
this.viewHasSomeCollapsibleRootItem = ViewHasSomeCollapsibleRootItemContext.bindTo(contextKeyService);
this.viewVisibleContextKey = FoldersViewVisibleContext.bindTo(contextKeyService);


this.explorerService.registerView(this);
this._register(toDisposable(() => this.clearWorkspaceTitleContainer()));
}

get autoReveal() {
Expand All @@ -255,9 +283,24 @@ export class ExplorerView extends ViewPane implements IExplorerView {
// noop
}

override get headerVisible(): boolean {
return super.headerVisible;
}

override set headerVisible(visible: boolean) {
super.headerVisible = visible;
this.updateWorkspaceTitleCase();
}

override setVisible(visible: boolean): void {
if (!visible) {
this.clearWorkspaceTitleContainer();
}
this.viewVisibleContextKey.set(visible);
super.setVisible(visible);
if (visible) {
this.updateWorkspaceTitleContainer();
}
}

@memoize private get fileCopiedContextKey(): IContextKey<boolean> {
Expand Down Expand Up @@ -285,11 +328,35 @@ export class ExplorerView extends ViewPane implements IExplorerView {
titleElement.setAttribute('aria-label', this.ariaHeaderLabel);
};

this._register(this.contextService.onDidChangeWorkspaceName(setHeader));
this._register(this.contextService.onDidChangeWorkspaceName(() => {
setHeader();
this.updateWorkspaceTitleCase();
}));
this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateWorkspaceTitleCase()));
this._register(this.labelService.onDidChangeFormatters(setHeader));
setHeader();
}

private updateWorkspaceTitleContainer(): void {
const workspaceTitleContainer = DOM.findParentWithClass(this.element, 'part') ?? undefined;
if (this.workspaceTitleContainer !== workspaceTitleContainer) {
this.workspaceTitleContainer?.classList.remove(PRESERVE_MERGED_WORKSPACE_NAME_CASE_CLASS);
this.workspaceTitleContainer = workspaceTitleContainer;
}
this.updateWorkspaceTitleCase();
}

private clearWorkspaceTitleContainer(): void {
this.workspaceTitleContainer?.classList.remove(PRESERVE_MERGED_WORKSPACE_NAME_CASE_CLASS);
this.workspaceTitleContainer = undefined;
}

private updateWorkspaceTitleCase(): void {
const preserveWorkspaceNameCase = shouldPreserveWorkspaceNameCase(this.contextService.getWorkbenchState(), this.contextService.getWorkspace(), this.environmentService);
this.element.classList.toggle(PRESERVE_WORKSPACE_NAME_CASE_CLASS, preserveWorkspaceNameCase);
this.workspaceTitleContainer?.classList.toggle(PRESERVE_MERGED_WORKSPACE_NAME_CASE_CLASS, preserveWorkspaceNameCase && this.isVisible() && !this.headerVisible);
}

protected override layoutBody(height: number, width: number): void {
super.layoutBody(height, width);
this.tree.layout(height, width);
Expand Down
27 changes: 26 additions & 1 deletion src/vs/workbench/contrib/files/test/browser/explorerView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import assert from 'assert';
import { Emitter } from '../../../../../base/common/event.js';
import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from '../../../../../base/test/common/utils.js';
import { ExplorerItem } from '../../common/explorerModel.js';
import { getContext } from '../../browser/views/explorerView.js';
import { getContext, shouldPreserveWorkspaceNameCase } from '../../browser/views/explorerView.js';
import { listInvalidItemForeground } from '../../../../../platform/theme/common/colorRegistry.js';
import { CompressedNavigationController } from '../../browser/views/explorerViewer.js';
import * as dom from '../../../../../base/browser/dom.js';
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
import { provideDecorations } from '../../browser/views/explorerDecorationsProvider.js';
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
import { NullFilesConfigurationService, TestFileService } from '../../../../test/common/workbenchTestServices.js';
import { TestEnvironmentService } from '../../../../test/browser/workbenchTestServices.js';
import { URI } from '../../../../../base/common/uri.js';
import { IWorkspace, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js';
import { joinPath } from '../../../../../base/common/resources.js';

suite('Files - ExplorerView', () => {

Expand Down Expand Up @@ -69,6 +73,27 @@ suite('Files - ExplorerView', () => {
});
});

test('preserves workspace name case only for user named workspaces', async function () {
const untitledWorkspacesHome = TestEnvironmentService.untitledWorkspacesHome;
function workspace(configuration: URI | null): IWorkspace {
return { id: 'test', folders: [], configuration };
}

assert.deepStrictEqual({
empty: shouldPreserveWorkspaceNameCase(WorkbenchState.EMPTY, workspace(null), TestEnvironmentService),
folder: shouldPreserveWorkspaceNameCase(WorkbenchState.FOLDER, workspace(null), TestEnvironmentService),
untitled: shouldPreserveWorkspaceNameCase(WorkbenchState.WORKSPACE, workspace(joinPath(untitledWorkspacesHome, '1234', 'workspace.json')), TestEnvironmentService),
untitledDifferentCase: shouldPreserveWorkspaceNameCase(WorkbenchState.WORKSPACE, workspace(joinPath(untitledWorkspacesHome.with({ path: untitledWorkspacesHome.path.toUpperCase() }), '1234', 'workspace.json')), TestEnvironmentService),
named: shouldPreserveWorkspaceNameCase(WorkbenchState.WORKSPACE, workspace(URI.file('/some/path/myWorkspace.code-workspace')), TestEnvironmentService),
}, {
empty: false,
folder: true,
untitled: false,
untitledDifferentCase: false,
named: true,
});
});

test('compressed navigation controller', async function () {
const container = $('.file');
const label = $('.label');
Expand Down
12 changes: 4 additions & 8 deletions src/vs/workbench/contrib/modernUI/browser/media/fontRamp.css
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,11 @@
}

/*
* File Explorer titles — respect the real casing of the workspace / folder name
* (user input). `text-transform: none` overrides both the base `uppercase` rule
* and the `capitalize` override above. System labels such as "Untitled
* (Workspace)" are already title-cased in source, so they still render
* correctly; only user-provided folder names (e.g. "vscode-dev") are left
* untouched.
* Named File Explorer workspace titles preserve the casing supplied by the
* user. Untitled workspaces continue to use the standard capitalized header.
*/
.modern-ui.monaco-workbench .part:not(.editor)[data-active-composite="workbench.view.explorer"] > .title > .title-label h2,
.modern-ui .monaco-pane-view .pane > .pane-header > .icon.codicon-explorer-view-icon + .title {
.modern-ui.monaco-workbench .part.preserve-merged-workspace-name-case > .title > .title-label h2,
.modern-ui .pane.preserve-workspace-name-case > .pane-header > .icon.codicon-explorer-view-icon + .title {
text-transform: none;
}

Expand Down
Loading
Loading