Chat - implement session picker in the chat panel title (#293426)

* refactor layout and layout2d into base common

* support anchored quick pick

* wip: use anchored quick pick in scm

* almost there

* undo scm history view pane

* Chat - implement session picker in the chat panel title

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Port changes again, and clean things up

* Pull request feedback

* missing changes

* cleanup

* fix bad merge

* reduce complexity

* polish

* update title

* Adopt the anchor

* Fix compilation error

* Fix monaco editor check

* Enhance drag-and-drop functionality in QuickInput: add cursor style for no-drag state and enable/disable control

* Fix positioning bug

* fix change of behavior of layout2d

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Benjamin Pasero <benjamin.pasero@microsoft.com>
Co-authored-by: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com>
This commit is contained in:
João Moreno
2026-02-10 20:05:45 +01:00
committed by GitHub
parent 5f41dcfdca
commit 52457c5f1f
10 changed files with 310 additions and 171 deletions

View File

@@ -7,11 +7,13 @@ import { BrowserFeatures } from '../../canIUse.js';
import * as DOM from '../../dom.js';
import { StandardMouseEvent } from '../../mouseEvent.js';
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../common/lifecycle.js';
import { AnchorAlignment, AnchorAxisAlignment, AnchorPosition, IRect, layout2d } from '../../../common/layout.js';
import * as platform from '../../../common/platform.js';
import { Range } from '../../../common/range.js';
import { OmitOptional } from '../../../common/types.js';
import './contextview.css';
export { AnchorAlignment, AnchorAxisAlignment, AnchorPosition } from '../../../common/layout.js';
export const enum ContextViewDOMPosition {
ABSOLUTE = 1,
FIXED,
@@ -31,18 +33,6 @@ export function isAnchor(obj: unknown): obj is IAnchor | OmitOptional<IAnchor> {
return !!anchor && typeof anchor.x === 'number' && typeof anchor.y === 'number';
}
export const enum AnchorAlignment {
LEFT, RIGHT
}
export const enum AnchorPosition {
BELOW, ABOVE
}
export const enum AnchorAxisAlignment {
VERTICAL, HORIZONTAL
}
export interface IDelegate {
/**
* The anchor where to position the context view.
@@ -73,66 +63,40 @@ export interface IContextViewProvider {
layout(): void;
}
export interface IPosition {
top: number;
left: number;
}
export function getAnchorRect(anchor: HTMLElement | StandardMouseEvent | IAnchor): IRect {
// Get the element's position and size (to anchor the view)
if (DOM.isHTMLElement(anchor)) {
const elementPosition = DOM.getDomNodePagePosition(anchor);
export interface ISize {
width: number;
height: number;
}
// In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element
// e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level.
// Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Size Multiplier: 1.5
const zoom = DOM.getDomNodeZoomLevel(anchor);
export interface IView extends IPosition, ISize { }
export const enum LayoutAnchorPosition {
Before,
After
}
export enum LayoutAnchorMode {
AVOID,
ALIGN
}
export interface ILayoutAnchor {
offset: number;
size: number;
mode?: LayoutAnchorMode; // default: AVOID
position: LayoutAnchorPosition;
}
/**
* Lays out a one dimensional view next to an anchor in a viewport.
*
* @returns The view offset within the viewport.
*/
export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAnchor): number {
const layoutAfterAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset : anchor.offset + anchor.size;
const layoutBeforeAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset + anchor.size : anchor.offset;
if (anchor.position === LayoutAnchorPosition.Before) {
if (viewSize <= viewportSize - layoutAfterAnchorBoundary) {
return layoutAfterAnchorBoundary; // happy case, lay it out after the anchor
}
if (viewSize <= layoutBeforeAnchorBoundary) {
return layoutBeforeAnchorBoundary - viewSize; // ok case, lay it out before the anchor
}
return Math.max(viewportSize - viewSize, 0); // sad case, lay it over the anchor
return {
top: elementPosition.top * zoom,
left: elementPosition.left * zoom,
width: elementPosition.width * zoom,
height: elementPosition.height * zoom
};
} else if (isAnchor(anchor)) {
return {
top: anchor.y,
left: anchor.x,
width: anchor.width || 1,
height: anchor.height || 2
};
} else {
if (viewSize <= layoutBeforeAnchorBoundary) {
return layoutBeforeAnchorBoundary - viewSize; // happy case, lay it out before the anchor
}
if (viewSize <= viewportSize - layoutAfterAnchorBoundary && layoutBeforeAnchorBoundary < viewSize / 2) {
return layoutAfterAnchorBoundary; // ok case, lay it out after the anchor
}
return 0; // sad case, lay it over the anchor
return {
top: anchor.posy,
left: anchor.posx,
// We are about to position the context view where the mouse
// cursor is. To prevent the view being exactly under the mouse
// when showing and thus potentially triggering an action within,
// we treat the mouse location like a small sized block element.
width: 2,
height: 2
};
}
}
@@ -270,82 +234,14 @@ export class ContextView extends Disposable {
}
// Get anchor
const anchor = this.delegate!.getAnchor();
// Compute around
let around: IView;
// Get the element's position and size (to anchor the view)
if (DOM.isHTMLElement(anchor)) {
const elementPosition = DOM.getDomNodePagePosition(anchor);
// In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element
// e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level.
// Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Size Multiplier: 1.5
const zoom = DOM.getDomNodeZoomLevel(anchor);
around = {
top: elementPosition.top * zoom,
left: elementPosition.left * zoom,
width: elementPosition.width * zoom,
height: elementPosition.height * zoom
};
} else if (isAnchor(anchor)) {
around = {
top: anchor.y,
left: anchor.x,
width: anchor.width || 1,
height: anchor.height || 2
};
} else {
around = {
top: anchor.posy,
left: anchor.posx,
// We are about to position the context view where the mouse
// cursor is. To prevent the view being exactly under the mouse
// when showing and thus potentially triggering an action within,
// we treat the mouse location like a small sized block element.
width: 2,
height: 2
};
}
const viewSizeWidth = DOM.getTotalWidth(this.view);
const viewSizeHeight = DOM.getTotalHeight(this.view);
const anchorPosition = this.delegate!.anchorPosition ?? AnchorPosition.BELOW;
const anchorAlignment = this.delegate!.anchorAlignment ?? AnchorAlignment.LEFT;
const anchorAxisAlignment = this.delegate!.anchorAxisAlignment ?? AnchorAxisAlignment.VERTICAL;
let top: number;
let left: number;
const anchor = getAnchorRect(this.delegate!.getAnchor());
const activeWindow = DOM.getActiveWindow();
if (anchorAxisAlignment === AnchorAxisAlignment.VERTICAL) {
const verticalAnchor: ILayoutAnchor = { offset: around.top - activeWindow.pageYOffset, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset;
// if view intersects vertically with anchor, we must avoid the anchor
if (Range.intersects({ start: top, end: top + viewSizeHeight }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) {
horizontalAnchor.mode = LayoutAnchorMode.AVOID;
}
left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor);
} else {
const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
const verticalAnchor: ILayoutAnchor = { offset: around.top, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor);
// if view intersects horizontally with anchor, we must avoid the anchor
if (Range.intersects({ start: left, end: left + viewSizeWidth }, { start: horizontalAnchor.offset, end: horizontalAnchor.offset + horizontalAnchor.size })) {
verticalAnchor.mode = LayoutAnchorMode.AVOID;
}
top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset;
}
const viewport = { top: activeWindow.pageYOffset, left: activeWindow.pageXOffset, width: activeWindow.innerWidth, height: activeWindow.innerHeight };
const view = { width: DOM.getTotalWidth(this.view), height: DOM.getTotalHeight(this.view) };
const anchorPosition = this.delegate!.anchorPosition;
const anchorAlignment = this.delegate!.anchorAlignment;
const anchorAxisAlignment = this.delegate!.anchorAxisAlignment;
const { top, left } = layout2d(viewport, view, anchor, { anchorAlignment, anchorPosition, anchorAxisAlignment });
this.view.classList.remove('top', 'bottom', 'left', 'right');
this.view.classList.add(anchorPosition === AnchorPosition.BELOW ? 'bottom' : 'top');

View File

@@ -11,7 +11,6 @@ import { StandardKeyboardEvent } from '../../keyboardEvent.js';
import { StandardMouseEvent } from '../../mouseEvent.js';
import { ActionBar, ActionsOrientation, IActionViewItemProvider } from '../actionbar/actionbar.js';
import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from '../actionbar/actionViewItems.js';
import { AnchorAlignment, layout, LayoutAnchorPosition } from '../contextview/contextview.js';
import { DomScrollableElement } from '../scrollbar/scrollableElement.js';
import { EmptySubmenuAction, IAction, IActionRunner, Separator, SubmenuAction } from '../../../common/actions.js';
import { RunOnceScheduler } from '../../../common/async.js';
@@ -26,6 +25,7 @@ import { DisposableStore } from '../../../common/lifecycle.js';
import { isLinux, isMacintosh } from '../../../common/platform.js';
import { ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js';
import * as strings from '../../../common/strings.js';
import { AnchorAlignment, layout, LayoutAnchorPosition } from '../../../common/layout.js';
export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/;
export const MENU_ESCAPED_MNEMONIC_REGEX = /(&amp;)?(&amp;)([^\s&])/g;
@@ -859,7 +859,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
const ret = { top: 0, left: 0 };
// Start with horizontal
ret.left = layout(windowDimensions.width, submenu.width, { position: expandDirection.horizontal === HorizontalDirection.Right ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, offset: entry.left, size: entry.width });
ret.left = layout(windowDimensions.width, submenu.width, { position: expandDirection.horizontal === HorizontalDirection.Right ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, offset: entry.left, size: entry.width }).position;
// We don't have enough room to layout the menu fully, so we are overlapping the menu
if (ret.left >= entry.left && ret.left < entry.left + entry.width) {
@@ -872,7 +872,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
}
// Now that we have a horizontal position, try layout vertically
ret.top = layout(windowDimensions.height, submenu.height, { position: LayoutAnchorPosition.Before, offset: entry.top, size: 0 });
ret.top = layout(windowDimensions.height, submenu.height, { position: LayoutAnchorPosition.Before, offset: entry.top, size: 0 }).position;
// We didn't have enough room below, but we did above, so we shift down to align the menu
if (ret.top + submenu.height === entry.top && ret.top + entry.height + submenu.height <= windowDimensions.height) {

View File

@@ -0,0 +1,166 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Range } from './range.js';
export interface IAnchor {
x: number;
y: number;
width?: number;
height?: number;
}
export const enum AnchorAlignment {
LEFT, RIGHT
}
export const enum AnchorPosition {
BELOW, ABOVE
}
export const enum AnchorAxisAlignment {
VERTICAL, HORIZONTAL
}
interface IPosition {
readonly top: number;
readonly left: number;
}
interface ISize {
readonly width: number;
readonly height: number;
}
export interface IRect extends IPosition, ISize { }
export const enum LayoutAnchorPosition {
Before,
After
}
export enum LayoutAnchorMode {
AVOID,
ALIGN
}
export interface ILayoutAnchor {
offset: number;
size: number;
mode?: LayoutAnchorMode; // default: AVOID
position: LayoutAnchorPosition;
}
export interface ILayoutResult {
position: number;
result: 'ok' | 'flipped' | 'overlap';
}
/**
* Lays out a one dimensional view next to an anchor in a viewport.
*
* @returns The view offset within the viewport.
*/
export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAnchor): ILayoutResult {
const layoutAfterAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset : anchor.offset + anchor.size;
const layoutBeforeAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset + anchor.size : anchor.offset;
if (anchor.position === LayoutAnchorPosition.Before) {
if (viewSize <= viewportSize - layoutAfterAnchorBoundary) {
return { position: layoutAfterAnchorBoundary, result: 'ok' }; // happy case, lay it out after the anchor
}
if (viewSize <= layoutBeforeAnchorBoundary) {
return { position: layoutBeforeAnchorBoundary - viewSize, result: 'flipped' }; // ok case, lay it out before the anchor
}
return { position: Math.max(viewportSize - viewSize, 0), result: 'overlap' }; // sad case, lay it over the anchor
} else {
if (viewSize <= layoutBeforeAnchorBoundary) {
return { position: layoutBeforeAnchorBoundary - viewSize, result: 'ok' }; // happy case, lay it out before the anchor
}
if (viewSize <= viewportSize - layoutAfterAnchorBoundary && layoutBeforeAnchorBoundary < viewSize / 2) {
return { position: layoutAfterAnchorBoundary, result: 'flipped' }; // ok case, lay it out after the anchor
}
return { position: 0, result: 'overlap' }; // sad case, lay it over the anchor
}
}
interface ILayout2DOptions {
readonly anchorAlignment?: AnchorAlignment; // default: left
readonly anchorPosition?: AnchorPosition; // default: above
readonly anchorAxisAlignment?: AnchorAxisAlignment; // default: vertical
}
export interface ILayout2DResult {
top: number;
left: number;
bottom: number;
right: number;
anchorAlignment: AnchorAlignment;
anchorPosition: AnchorPosition;
}
export function layout2d(viewport: IRect, view: ISize, anchor: IRect, options?: ILayout2DOptions): ILayout2DResult {
let anchorAlignment = options?.anchorAlignment ?? AnchorAlignment.LEFT;
let anchorPosition = options?.anchorPosition ?? AnchorPosition.BELOW;
const anchorAxisAlignment = options?.anchorAxisAlignment ?? AnchorAxisAlignment.VERTICAL;
let top: number;
let left: number;
if (anchorAxisAlignment === AnchorAxisAlignment.VERTICAL) {
const verticalAnchor: ILayoutAnchor = { offset: anchor.top - viewport.top, size: anchor.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
const horizontalAnchor: ILayoutAnchor = { offset: anchor.left, size: anchor.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
const verticalLayoutResult = layout(viewport.height, view.height, verticalAnchor);
top = verticalLayoutResult.position + viewport.top;
if (verticalLayoutResult.result === 'flipped') {
anchorPosition = anchorPosition === AnchorPosition.BELOW ? AnchorPosition.ABOVE : AnchorPosition.BELOW;
}
// if view intersects vertically with anchor, we must avoid the anchor
if (Range.intersects({ start: top, end: top + view.height }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) {
horizontalAnchor.mode = LayoutAnchorMode.AVOID;
}
const horizontalLayoutResult = layout(viewport.width, view.width, horizontalAnchor);
left = horizontalLayoutResult.position;
if (horizontalLayoutResult.result === 'flipped') {
anchorAlignment = anchorAlignment === AnchorAlignment.LEFT ? AnchorAlignment.RIGHT : AnchorAlignment.LEFT;
}
} else {
const horizontalAnchor: ILayoutAnchor = { offset: anchor.left, size: anchor.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
const verticalAnchor: ILayoutAnchor = { offset: anchor.top, size: anchor.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
const horizontalLayoutResult = layout(viewport.width, view.width, horizontalAnchor);
left = horizontalLayoutResult.position;
if (horizontalLayoutResult.result === 'flipped') {
anchorAlignment = anchorAlignment === AnchorAlignment.LEFT ? AnchorAlignment.RIGHT : AnchorAlignment.LEFT;
}
// if view intersects horizontally with anchor, we must avoid the anchor
if (Range.intersects({ start: left, end: left + view.width }, { start: horizontalAnchor.offset, end: horizontalAnchor.offset + horizontalAnchor.size })) {
verticalAnchor.mode = LayoutAnchorMode.AVOID;
}
const verticalLayoutResult = layout(viewport.height, view.height, verticalAnchor);
top = verticalLayoutResult.position + viewport.top;
if (verticalLayoutResult.result === 'flipped') {
anchorPosition = anchorPosition === AnchorPosition.BELOW ? AnchorPosition.ABOVE : AnchorPosition.BELOW;
}
}
const right = viewport.width - (left + view.width);
const bottom = viewport.height - (top + view.height);
return { top, left, bottom, right, anchorAlignment, anchorPosition };
}

View File

@@ -4,27 +4,26 @@
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { layout, LayoutAnchorPosition } from '../../../../browser/ui/contextview/contextview.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../common/utils.js';
import { layout, LayoutAnchorPosition } from '../../common/layout.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js';
suite('Contextview', function () {
suite('Layout', function () {
test('layout', () => {
assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.Before }), 0);
assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.Before }), 50);
assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.Before }), 180);
assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.Before }).position, 0);
assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.Before }).position, 50);
assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.Before }).position, 180);
assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.After }), 0);
assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.After }), 30);
assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.After }), 180);
assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.After }).position, 0);
assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.After }).position, 30);
assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.After }).position, 180);
assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.Before }).position, 50);
assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.Before }).position, 100);
assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.Before }).position, 130);
assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.Before }), 50);
assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.Before }), 100);
assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.Before }), 130);
assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.After }), 50);
assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.After }), 30);
assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.After }), 130);
assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.After }).position, 50);
assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.After }).position, 30);
assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.After }).position, 130);
});
ensureNoDisposablesAreLeakedInTestSuite();

View File

@@ -20,6 +20,11 @@
border-top-left-radius: 5px;
}
.quick-input-widget.no-drag .quick-input-titlebar,
.quick-input-widget.no-drag .quick-input-title,
.quick-input-widget.no-drag .quick-input-header {
cursor: default;
}
.quick-input-widget .monaco-inputbox .monaco-action-bar {
top: 0;
}

View File

@@ -37,6 +37,8 @@ import { TriStateCheckbox, createToggleActionViewItemProvider } from '../../../b
import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js';
import { QuickInputTreeController } from './tree/quickInputTreeController.js';
import { QuickTree } from './tree/quickTree.js';
import { AnchorAlignment, AnchorPosition, layout2d } from '../../../base/common/layout.js';
import { getAnchorRect } from '../../../base/browser/ui/contextview/contextview.js';
const $ = dom.$;
@@ -541,6 +543,7 @@ export class QuickInputController extends Disposable {
input.quickNavigate = options.quickNavigate;
input.hideInput = !!options.hideInput;
input.contextKey = options.contextKey;
input.anchor = options.anchor;
input.busy = true;
Promise.all([picks, options.activeItem])
.then(([items, _activeItem]) => {
@@ -710,6 +713,7 @@ export class QuickInputController extends Disposable {
ui.container.style.display = '';
this.updateLayout();
this.dndController?.setEnabled(!controller.anchor);
this.dndController?.layoutContainer();
ui.inputBox.setFocus();
this.quickInputTypeContext.set(controller.type);
@@ -861,16 +865,52 @@ export class QuickInputController extends Disposable {
private updateLayout() {
if (this.ui && this.isVisible()) {
const style = this.ui.container.style;
const width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH);
let width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH);
style.width = width + 'px';
let listHeight = this.dimension && this.dimension.height * 0.4;
// Position
style.top = `${this.viewState?.top ? Math.round(this.dimension!.height * this.viewState.top) : this.titleBarOffset}px`;
style.left = `${Math.round((this.dimension!.width * (this.viewState?.left ?? 0.5 /* center */)) - (width / 2))}px`;
if (this.controller?.anchor) {
const container = this.layoutService.getContainer(dom.getActiveWindow()).getBoundingClientRect();
const anchor = getAnchorRect(this.controller.anchor);
width = 380;
listHeight = this.dimension ? Math.min(this.dimension.height * 0.2, 200) : 200;
// Beware:
// We need to add some extra pixels to the height to account for the input and padding.
const containerHeight = Math.floor(listHeight) + 6 + 26 + 16;
const { top, left, right, bottom, anchorAlignment, anchorPosition } = layout2d(container, { width, height: containerHeight }, anchor, { anchorPosition: AnchorPosition.ABOVE });
if (anchorAlignment === AnchorAlignment.RIGHT) {
style.right = `${right}px`;
style.left = 'initial';
} else {
style.left = `${left}px`;
style.right = 'initial';
}
if (anchorPosition === AnchorPosition.ABOVE) {
style.bottom = `${bottom}px`;
style.top = 'initial';
} else {
style.top = `${top}px`;
style.bottom = 'initial';
}
style.width = `${width}px`;
style.height = '';
} else {
style.top = `${this.viewState?.top ? Math.round(this.dimension!.height * this.viewState.top) : this.titleBarOffset}px`;
style.left = `${Math.round((this.dimension!.width * (this.viewState?.left ?? 0.5 /* center */)) - (width / 2))}px`;
style.right = '';
style.bottom = '';
style.height = '';
}
this.ui.inputBox.layout();
this.ui.list.layout(this.dimension && this.dimension.height * 0.4);
this.ui.tree.layout(this.dimension && this.dimension.height * 0.4);
this.ui.list.layout(listHeight);
this.ui.tree.layout(listHeight);
}
}
@@ -965,6 +1005,8 @@ export interface IQuickInputControllerHost extends ILayoutService { }
class QuickInputDragAndDropController extends Disposable {
readonly dndViewState = observableValue<{ top?: number; left?: number; done: boolean } | undefined>(this, undefined);
private _enabled = true;
private readonly _snapThreshold = 20;
private readonly _snapLineHorizontalRatio = 0.25;
@@ -1000,6 +1042,10 @@ class QuickInputDragAndDropController extends Disposable {
}
layoutContainer(dimension = this._layoutService.activeContainerDimension): void {
if (!this._enabled) {
return;
}
const state = this.dndViewState.get();
const dragAreaRect = this._quickInputContainer.getBoundingClientRect();
if (state?.top && state?.left) {
@@ -1011,6 +1057,11 @@ class QuickInputDragAndDropController extends Disposable {
}
}
setEnabled(enabled: boolean): void {
this._enabled = enabled;
this._quickInputContainer.classList.toggle('no-drag', !enabled);
}
setAlignment(alignment: 'top' | 'center' | { top: number; left: number }, done = true): void {
if (alignment === 'top') {
this.dndViewState.set({
@@ -1041,6 +1092,10 @@ class QuickInputDragAndDropController extends Disposable {
// Double click
this._register(dom.addDisposableGenericMouseUpListener(dragArea, (event: MouseEvent) => {
if (!this._enabled) {
return;
}
const originEvent = new StandardMouseEvent(dom.getWindow(dragArea), event);
if (originEvent.detail !== 2) {
return;
@@ -1057,6 +1112,10 @@ class QuickInputDragAndDropController extends Disposable {
// Mouse down
this._register(dom.addDisposableGenericMouseDownListener(dragArea, (e: MouseEvent) => {
if (!this._enabled) {
return;
}
const activeWindow = dom.getWindow(this._layoutService.activeContainer);
const originEvent = new StandardMouseEvent(activeWindow, e);

View File

@@ -197,6 +197,11 @@ export interface IPickOptions<T extends IQuickPickItem> {
*/
activeItem?: Promise<T> | T;
/**
* an optional anchor for the picker
*/
anchor?: HTMLElement | { x: number; y: number };
onKeyMods?: (keyMods: IKeyMods) => void;
onDidFocus?: (entry: T) => void;
onDidTriggerItemButton?: (context: IQuickPickItemButtonContext<T>) => void;
@@ -353,6 +358,11 @@ export interface IQuickInput extends IDisposable {
*/
ignoreFocusOut: boolean;
/**
* An optional anchor for the quick input.
*/
anchor?: HTMLElement | { x: number; y: number };
/**
* Shows the quick input.
*/

View File

@@ -149,7 +149,7 @@ export class PickAgentSessionAction extends Action2 {
async run(accessor: ServicesAccessor): Promise<void> {
const instantiationService = accessor.get(IInstantiationService);
const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker);
const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, undefined);
await agentSessionsPicker.pickAgentSession();
}
}

View File

@@ -67,6 +67,7 @@ export class AgentSessionsPicker {
private readonly sorter = new AgentSessionsSorter();
constructor(
private readonly anchor: HTMLElement | undefined,
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@@ -78,6 +79,7 @@ export class AgentSessionsPicker {
const picker = disposables.add(this.quickInputService.createQuickPick<ISessionPickItem>({ useSeparators: true }));
const filter = disposables.add(this.instantiationService.createInstance(AgentSessionsFilter, {}));
picker.anchor = this.anchor;
picker.items = this.createPickerItems(filter);
picker.canAcceptInBackground = true;
picker.placeholder = localize('chatAgentPickerPlaceholder', "Search agent sessions by name");

View File

@@ -59,6 +59,8 @@ export class ChatViewTitleControl extends Disposable {
}
private registerActions(): void {
const that = this;
this._register(registerAction2(class extends Action2 {
constructor() {
super({
@@ -76,7 +78,7 @@ export class ChatViewTitleControl extends Disposable {
async run(accessor: ServicesAccessor): Promise<void> {
const instantiationService = accessor.get(IInstantiationService);
const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker);
const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, that.titleLabel.value?.element);
await agentSessionsPicker.pickAgentSession();
}
}));