mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 20:13:32 +01:00
feat: implement SplitView for changes panel and enhance CI status widget layout
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import { IObjectTreeElement, ITreeNode } from '../../../../base/browser/ui/tree/
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { Iterable } from '../../../../base/common/iterator.js';
|
||||
import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { Event } from '../../../../base/common/event.js';
|
||||
import { autorun, constObservable, derived, derivedOpts, IObservable, IObservableWithChange, ISettableObservable, ObservablePromise, observableSignalFromEvent, observableValue, runOnChange } from '../../../../base/common/observable.js';
|
||||
import { basename, dirname } from '../../../../base/common/path.js';
|
||||
import { extUriBiasedIgnorePathCase, isEqual } from '../../../../base/common/resources.js';
|
||||
@@ -62,6 +63,10 @@ import { IGitHubService } from '../../github/browser/githubService.js';
|
||||
import { CIStatusWidget } from './ciStatusWidget.js';
|
||||
import { arrayEqualsC } from '../../../../base/common/equals.js';
|
||||
import { GITHUB_REMOTE_FILE_SCHEME } from '../../sessions/common/sessionData.js';
|
||||
import { Orientation } from '../../../../base/browser/ui/sash/sash.js';
|
||||
import { IView, Sizing, SplitView } from '../../../../base/browser/ui/splitview/splitview.js';
|
||||
import { Color } from '../../../../base/common/color.js';
|
||||
import { PANEL_SECTION_BORDER } from '../../../../workbench/common/theme.js';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
@@ -302,6 +307,8 @@ export class ChangesViewPane extends ViewPane {
|
||||
|
||||
private bodyContainer: HTMLElement | undefined;
|
||||
private welcomeContainer: HTMLElement | undefined;
|
||||
private filesHeaderNode: HTMLElement | undefined;
|
||||
private filesCountBadge: HTMLElement | undefined;
|
||||
private contentContainer: HTMLElement | undefined;
|
||||
private overviewContainer: HTMLElement | undefined;
|
||||
private summaryContainer: HTMLElement | undefined;
|
||||
@@ -311,6 +318,8 @@ export class ChangesViewPane extends ViewPane {
|
||||
|
||||
private tree: WorkbenchCompressibleObjectTree<ChangesTreeElement> | undefined;
|
||||
private ciStatusWidget: CIStatusWidget | undefined;
|
||||
private splitView: SplitView | undefined;
|
||||
private splitViewContainer: HTMLElement | undefined;
|
||||
|
||||
private readonly renderDisposables = this._register(new DisposableStore());
|
||||
|
||||
@@ -339,7 +348,7 @@ export class ChangesViewPane extends ViewPane {
|
||||
@ICodeReviewService private readonly codeReviewService: ICodeReviewService,
|
||||
@IGitHubService private readonly gitHubService: IGitHubService,
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
|
||||
super({ ...options, titleMenuId: MenuId.ChatEditingSessionTitleToolbar }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
|
||||
|
||||
this.viewModel = this.instantiationService.createInstance(ChangesViewModel);
|
||||
this._register(this.viewModel);
|
||||
@@ -394,8 +403,11 @@ export class ChangesViewPane extends ViewPane {
|
||||
// Actions container - positioned outside and above the card
|
||||
this.actionsContainer = dom.append(this.bodyContainer, $('.chat-editing-session-actions.outside-card'));
|
||||
|
||||
// Main container with file icons support (the "card")
|
||||
this.contentContainer = dom.append(this.bodyContainer, $('.chat-editing-session-container.show-file-icons'));
|
||||
// SplitView container for resizable file tree / CI checks split
|
||||
this.splitViewContainer = dom.append(this.bodyContainer, $('.changes-splitview-container'));
|
||||
|
||||
// Main container with file icons support (the "card") — top pane
|
||||
this.contentContainer = dom.append(this.splitViewContainer, $('.chat-editing-session-container.show-file-icons'));
|
||||
this._register(createFileIconThemableTreeContainerScope(this.contentContainer, this.themeService));
|
||||
|
||||
// Toggle class based on whether the file icon theme has file icons
|
||||
@@ -405,6 +417,12 @@ export class ChangesViewPane extends ViewPane {
|
||||
updateHasFileIcons();
|
||||
this._register(this.themeService.onDidFileIconThemeChange(updateHasFileIcons));
|
||||
|
||||
// Files header
|
||||
this.filesHeaderNode = dom.append(this.contentContainer, $('.changes-files-header'));
|
||||
const filesTitle = dom.append(this.filesHeaderNode, $('.changes-files-title'));
|
||||
filesTitle.textContent = localize('changesView.filesTitle', "Changed Files");
|
||||
this.filesCountBadge = dom.append(this.filesHeaderNode, $('.changes-files-count'));
|
||||
|
||||
// Overview section (header with summary only - actions moved outside card)
|
||||
this.overviewContainer = dom.append(this.contentContainer, $('.chat-editing-session-overview'));
|
||||
this.summaryContainer = dom.append(this.overviewContainer, $('.changes-summary'));
|
||||
@@ -412,9 +430,73 @@ export class ChangesViewPane extends ViewPane {
|
||||
// List container
|
||||
this.listContainer = dom.append(this.contentContainer, $('.chat-editing-session-list'));
|
||||
|
||||
// CI Status widget beneath the card
|
||||
this.ciStatusWidget = this._register(this.instantiationService.createInstance(CIStatusWidget, this.bodyContainer));
|
||||
this._register(this.ciStatusWidget.onDidChangeHeight(() => this.layoutTree()));
|
||||
// CI Status widget — bottom pane
|
||||
this.ciStatusWidget = this._register(this.instantiationService.createInstance(CIStatusWidget, this.splitViewContainer));
|
||||
|
||||
// Create SplitView
|
||||
this.splitView = this._register(new SplitView(this.splitViewContainer, {
|
||||
orientation: Orientation.VERTICAL,
|
||||
proportionalLayout: false,
|
||||
}));
|
||||
|
||||
// Shared constants for pane sizing
|
||||
const ciMarginTop = 8; // matches margin-top on .ci-status-widget
|
||||
const ciMinHeight = CIStatusWidget.HEADER_HEIGHT + CIStatusWidget.MIN_BODY_HEIGHT + ciMarginTop;
|
||||
|
||||
// Top pane: file tree
|
||||
const self = this;
|
||||
const treePane: IView = {
|
||||
element: this.contentContainer,
|
||||
minimumSize: ciMinHeight,
|
||||
maximumSize: Number.POSITIVE_INFINITY,
|
||||
onDidChange: Event.None,
|
||||
layout: (height) => {
|
||||
self.contentContainer!.style.height = `${height}px`;
|
||||
self._layoutTreeInPane(height);
|
||||
},
|
||||
};
|
||||
|
||||
// Bottom pane: CI checks
|
||||
const ciElement = this.ciStatusWidget.element;
|
||||
const ciWidget = this.ciStatusWidget;
|
||||
const ciPane: IView = {
|
||||
element: ciElement,
|
||||
minimumSize: ciMinHeight,
|
||||
maximumSize: Number.POSITIVE_INFINITY,
|
||||
onDidChange: Event.map(this.ciStatusWidget.onDidChangeHeight, () => undefined),
|
||||
layout: (height) => {
|
||||
const innerHeight = Math.max(0, height - ciMarginTop);
|
||||
ciElement.style.height = `${innerHeight}px`;
|
||||
const bodyHeight = Math.max(0, innerHeight - CIStatusWidget.HEADER_HEIGHT);
|
||||
ciWidget.layout(bodyHeight);
|
||||
},
|
||||
};
|
||||
|
||||
this.splitView.addView(treePane, Sizing.Distribute, 0, true);
|
||||
this.splitView.addView(ciPane, CIStatusWidget.HEADER_HEIGHT + CIStatusWidget.MIN_BODY_HEIGHT + ciMarginTop, 1, true);
|
||||
|
||||
// Style the sash as a visible separator between sections
|
||||
const updateSplitViewStyles = () => {
|
||||
const borderColor = this.themeService.getColorTheme().getColor(PANEL_SECTION_BORDER);
|
||||
this.splitView!.style({ separatorBorder: borderColor ?? Color.transparent });
|
||||
};
|
||||
updateSplitViewStyles();
|
||||
this._register(this.themeService.onDidColorThemeChange(updateSplitViewStyles));
|
||||
|
||||
// Initially hide CI pane until checks arrive
|
||||
this.splitView.setViewVisible(1, false);
|
||||
|
||||
this._register(this.ciStatusWidget.onDidChangeHeight(() => {
|
||||
if (!this.splitView || !this.ciStatusWidget) {
|
||||
return;
|
||||
}
|
||||
const visible = this.ciStatusWidget.visible;
|
||||
const isCurrentlyVisible = this.splitView.isViewVisible(1);
|
||||
if (visible !== isCurrentlyVisible) {
|
||||
this.splitView.setViewVisible(1, visible);
|
||||
}
|
||||
this.layoutSplitView();
|
||||
}));
|
||||
|
||||
this._register(this.onDidChangeBodyVisibility(visible => {
|
||||
if (visible) {
|
||||
@@ -731,14 +813,19 @@ export class ChangesViewPane extends ViewPane {
|
||||
}));
|
||||
}
|
||||
|
||||
// Update visibility based on entries
|
||||
// Update visibility and file count badge based on entries
|
||||
this.renderDisposables.add(autorun(reader => {
|
||||
const { files } = topLevelStats.read(reader);
|
||||
const hasEntries = files > 0;
|
||||
|
||||
dom.setVisibility(hasEntries, this.contentContainer!);
|
||||
dom.setVisibility(hasEntries, this.actionsContainer!);
|
||||
dom.setVisibility(hasEntries, this.splitViewContainer!);
|
||||
dom.setVisibility(!hasEntries, this.welcomeContainer!);
|
||||
|
||||
if (this.filesCountBadge) {
|
||||
this.filesCountBadge.textContent = `${files}`;
|
||||
}
|
||||
}));
|
||||
|
||||
// Update summary text (line counts only, file count is shown in badge)
|
||||
@@ -814,7 +901,7 @@ export class ChangesViewPane extends ViewPane {
|
||||
const tree = this.tree;
|
||||
|
||||
// Re-layout when collapse state changes so the card height adjusts
|
||||
this.renderDisposables.add(tree.onDidChangeContentHeight(() => this.layoutTree()));
|
||||
this.renderDisposables.add(tree.onDidChangeContentHeight(() => this.layoutSplitView()));
|
||||
|
||||
const openFileItem = (item: IChangesFileItem, items: IChangesFileItem[], sideBySide: boolean) => {
|
||||
const { uri: modifiedFileUri, originalUri, isDeletion } = item;
|
||||
@@ -921,85 +1008,45 @@ export class ChangesViewPane extends ViewPane {
|
||||
this.tree.setChildren(null, listChildren);
|
||||
}
|
||||
|
||||
this.layoutTree();
|
||||
this.layoutSplitView();
|
||||
}));
|
||||
}
|
||||
|
||||
private layoutTree(): void {
|
||||
if (!this.tree || !this.listContainer) {
|
||||
/** Layout the tree within its SplitView pane. */
|
||||
private _layoutTreeInPane(paneHeight: number): void {
|
||||
if (!this.tree) {
|
||||
return;
|
||||
}
|
||||
// Subtract overview/padding within the content container
|
||||
const overviewHeight = this.overviewContainer?.offsetHeight ?? 0;
|
||||
const filesHeaderHeight = this.filesHeaderNode?.offsetHeight ?? 0;
|
||||
const treeHeight = Math.max(0, paneHeight - filesHeaderHeight - overviewHeight);
|
||||
this.tree.layout(treeHeight, this.currentBodyWidth);
|
||||
this.tree.getHTMLElement().style.height = `${treeHeight}px`;
|
||||
}
|
||||
|
||||
// Calculate remaining height for the tree by subtracting other elements
|
||||
/** Layout the SplitView to fill available body space. */
|
||||
private layoutSplitView(): void {
|
||||
if (!this.splitView) {
|
||||
return;
|
||||
}
|
||||
const bodyHeight = this.currentBodyHeight;
|
||||
if (bodyHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Measure non-list elements height (padding, actions, overview)
|
||||
const bodyPadding = 16; // 8px top + 8px bottom from .changes-view-body
|
||||
const actionsHeight = this.actionsContainer?.offsetHeight ?? 0;
|
||||
const actionsMargin = actionsHeight > 0 ? 8 : 0; // margin-bottom on actions container
|
||||
const overviewHeight = this.overviewContainer?.offsetHeight ?? 0;
|
||||
const containerPadding = 8; // 4px top + 4px bottom from .chat-editing-session-container
|
||||
const containerBorder = 2; // 1px top + 1px bottom border
|
||||
|
||||
const fixedUsed = bodyPadding + actionsHeight + actionsMargin + overviewHeight + containerPadding + containerBorder;
|
||||
|
||||
// Determine CI widget space needs
|
||||
const ciWidget = this.ciStatusWidget;
|
||||
const ciVisible = ciWidget?.visible ?? false;
|
||||
const ciHeaderHeight = ciVisible ? CIStatusWidget.HEADER_HEIGHT : 0;
|
||||
const ciMargin = ciVisible ? 8 : 0; // margin-top on CI widget
|
||||
const ciDesiredHeight = ciWidget?.desiredHeight ?? 0;
|
||||
|
||||
const spaceForTreeAndCI = Math.max(0, bodyHeight - fixedUsed - ciMargin);
|
||||
|
||||
// Give the tree priority, then CI gets the rest (with min/max on CI body)
|
||||
const treeContentHeight = this.tree.contentHeight;
|
||||
let treeHeight: number;
|
||||
let ciBodyHeight = 0;
|
||||
|
||||
if (!ciVisible) {
|
||||
treeHeight = Math.min(spaceForTreeAndCI, treeContentHeight);
|
||||
} else {
|
||||
// Reserve space for the CI header
|
||||
const spaceAfterCIHeader = Math.max(0, spaceForTreeAndCI - ciHeaderHeight);
|
||||
|
||||
// Give the tree what it needs first, up to available space
|
||||
treeHeight = Math.min(spaceAfterCIHeader, treeContentHeight);
|
||||
|
||||
// Remaining goes to CI body
|
||||
const remainingForCIBody = Math.max(0, spaceAfterCIHeader - treeHeight);
|
||||
const ciDesiredBodyHeight = Math.max(0, ciDesiredHeight - ciHeaderHeight);
|
||||
|
||||
ciBodyHeight = Math.min(ciDesiredBodyHeight, remainingForCIBody);
|
||||
|
||||
// Ensure CI body gets at least MIN_BODY_HEIGHT if there's content
|
||||
if (ciDesiredBodyHeight > 0 && ciBodyHeight < CIStatusWidget.MIN_BODY_HEIGHT) {
|
||||
const minCIBody = Math.min(CIStatusWidget.MIN_BODY_HEIGHT, ciDesiredBodyHeight);
|
||||
const needed = minCIBody - ciBodyHeight;
|
||||
const canTake = Math.max(0, treeHeight - 0); // tree can shrink to 0
|
||||
const taken = Math.min(needed, canTake);
|
||||
treeHeight -= taken;
|
||||
ciBodyHeight += taken;
|
||||
}
|
||||
|
||||
// Cap CI body at MAX_BODY_HEIGHT
|
||||
ciBodyHeight = Math.min(ciBodyHeight, CIStatusWidget.MAX_BODY_HEIGHT);
|
||||
|
||||
ciWidget!.layout(ciBodyHeight);
|
||||
}
|
||||
|
||||
this.tree.layout(treeHeight, this.currentBodyWidth);
|
||||
this.tree.getHTMLElement().style.height = `${treeHeight}px`;
|
||||
const actionsMargin = actionsHeight > 0 ? 8 : 0;
|
||||
const availableHeight = Math.max(0, bodyHeight - bodyPadding - actionsHeight - actionsMargin);
|
||||
this.splitViewContainer!.style.height = `${availableHeight}px`;
|
||||
this.splitView.layout(availableHeight);
|
||||
}
|
||||
|
||||
protected override layoutBody(height: number, width: number): void {
|
||||
super.layoutBody(height, width);
|
||||
this.currentBodyHeight = height;
|
||||
this.currentBodyWidth = width;
|
||||
this.layoutTree();
|
||||
this.layoutSplitView();
|
||||
}
|
||||
|
||||
override focus(): void {
|
||||
|
||||
Reference in New Issue
Block a user