Archived
1
0

chore(vscode): update to 1.53.2

These conflicts will be resolved in the following commits. We do it this way so
that PR review is possible.
This commit is contained in:
Joe Previte
2021-02-25 11:27:27 -07:00
1900 changed files with 83066 additions and 64589 deletions

View File

@ -110,13 +110,13 @@ export const onDidChangeFullscreen = WindowManager.INSTANCE.onDidChangeFullscree
const userAgent = navigator.userAgent;
export const isEdge = (userAgent.indexOf('Edge/') >= 0);
export const isOpera = (userAgent.indexOf('Opera') >= 0);
export const isEdgeLegacy = (userAgent.indexOf('Edge/') >= 0);
export const isFirefox = (userAgent.indexOf('Firefox') >= 0);
export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0);
export const isChrome = (userAgent.indexOf('Chrome') >= 0);
export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0));
export const isWebkitWebView = (!isChrome && !isSafari && isWebKit);
export const isIPad = (userAgent.indexOf('iPad') >= 0 || (isSafari && navigator.maxTouchPoints > 0));
export const isEdgeWebView = isEdge && (userAgent.indexOf('WebView/') >= 0);
export const isEdgeLegacyWebView = isEdgeLegacy && (userAgent.indexOf('WebView/') >= 0);
export const isElectron = (userAgent.indexOf('Electron/') >= 0);
export const isStandalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches);

View File

@ -27,7 +27,7 @@ export const BrowserFeatures = {
|| !!(navigator && navigator.clipboard && navigator.clipboard.readText)
),
richText: (() => {
if (browser.isEdge) {
if (browser.isEdgeLegacy) {
let index = navigator.userAgent.indexOf('Edge/');
let version = parseInt(navigator.userAgent.substring(index + 5, navigator.userAgent.indexOf('.', index)), 10);

View File

@ -16,7 +16,7 @@ export interface IContextMenuEvent {
export interface IContextMenuDelegate {
getAnchor(): HTMLElement | { x: number; y: number; width?: number; height?: number; };
getActions(): IAction[];
getActions(): readonly IAction[];
getCheckedActionsRepresentation?(action: IAction): 'radio' | 'checkbox';
getActionViewItem?(action: IAction): IActionViewItem | undefined;
getActionsContext?(event?: IContextMenuEvent): any;

View File

@ -20,18 +20,15 @@ import { KeyCode } from 'vs/base/common/keyCodes';
export function clearNode(node: HTMLElement): void {
while (node.firstChild) {
node.removeChild(node.firstChild);
node.firstChild.remove();
}
}
/**
* @deprecated Use node.isConnected directly
*/
export function isInDOM(node: Node | null): boolean {
while (node) {
if (node === document.body) {
return true;
}
node = node.parentNode || (node as ShadowRoot).host;
}
return false;
return node?.isConnected ?? false;
}
class DomListener implements IDisposable {
@ -451,6 +448,8 @@ export interface IDimension {
export class Dimension implements IDimension {
static readonly None = new Dimension(0, 0);
constructor(
public readonly width: number,
public readonly height: number,
@ -842,7 +841,7 @@ export const EventType = {
MOUSE_OUT: 'mouseout',
MOUSE_ENTER: 'mouseenter',
MOUSE_LEAVE: 'mouseleave',
MOUSE_WHEEL: browser.isEdge ? 'mousewheel' : 'wheel',
MOUSE_WHEEL: browser.isEdgeLegacy ? 'mousewheel' : 'wheel',
POINTER_UP: 'pointerup',
POINTER_DOWN: 'pointerdown',
POINTER_MOVE: 'pointermove',
@ -1002,9 +1001,13 @@ export function after<T extends Node>(sibling: HTMLElement, child: T): T {
return child;
}
export function append<T extends Node>(parent: HTMLElement, ...children: T[]): T {
children.forEach(child => parent.appendChild(child));
return children[children.length - 1];
export function append<T extends Node>(parent: HTMLElement, child: T): T;
export function append<T extends Node>(parent: HTMLElement, ...children: (T | string)[]): void;
export function append<T extends Node>(parent: HTMLElement, ...children: (T | string)[]): T | void {
parent.append(...children);
if (children.length === 1 && typeof children[0] !== 'string') {
return <T>children[0];
}
}
export function prepend<T extends Node>(parent: HTMLElement, child: T): T {
@ -1012,26 +1015,12 @@ export function prepend<T extends Node>(parent: HTMLElement, child: T): T {
return child;
}
/**
* Removes all children from `parent` and appends `children`
*/
export function reset(parent: HTMLElement, ...children: Array<Node | string>): void {
parent.innerText = '';
appendChildren(parent, ...children);
}
/**
* Appends `children` to `parent`
*/
export function appendChildren(parent: HTMLElement, ...children: Array<Node | string>): void {
for (const child of children) {
if (child instanceof Node) {
parent.appendChild(child);
} else if (typeof child === 'string') {
parent.appendChild(document.createTextNode(child));
}
}
append(parent, ...children);
}
const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/;
@ -1085,13 +1074,7 @@ function _$<T extends Element>(namespace: Namespace, description: string, attrs?
}
});
for (const child of children) {
if (child instanceof Node) {
result.appendChild(child);
} else if (typeof child === 'string') {
result.appendChild(document.createTextNode(child));
}
}
result.append(...children);
return result as T;
}
@ -1211,9 +1194,10 @@ export function computeScreenAwareSize(cssPx: number): number {
* See https://mathiasbynens.github.io/rel-noopener/
*/
export function windowOpenNoOpener(url: string): void {
if (platform.isNative || browser.isEdgeWebView) {
if (browser.isElectron || browser.isEdgeLegacyWebView) {
// In VSCode, window.open() always returns null...
// The same is true for a WebView (see https://github.com/microsoft/monaco-editor/issues/628)
// Also call directly window.open in sandboxed Electron (see https://github.com/microsoft/monaco-editor/issues/2220)
window.open(url);
} else {
let newTab = window.open();
@ -1249,7 +1233,7 @@ export function asCSSUrl(uri: URI): string {
export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void {
// If the data is provided as Buffer, we create a
// blog URL out of it to produce a valid link
// blob URL out of it to produce a valid link
let url: string;
if (URI.isUri(dataOrUri)) {
url = dataOrUri.toString(true);
@ -1298,7 +1282,7 @@ export interface IDetectedFullscreen {
mode: DetectedFullscreenMode;
/**
* Wether we know for sure that we are in fullscreen mode or
* Whether we know for sure that we are in fullscreen mode or
* it is a guess.
*/
guess: boolean;
@ -1385,7 +1369,7 @@ export function safeInnerHtml(node: HTMLElement, value: string): void {
}, ['class', 'id', 'role', 'tabindex']);
const html = _ttpSafeInnerHtml?.createHTML(value, options) ?? insane(value, options);
node.innerHTML = html as unknown as string;
node.innerHTML = html as string;
}
/**
@ -1398,7 +1382,12 @@ function toBinary(str: string): string {
for (let i = 0; i < codeUnits.length; i++) {
codeUnits[i] = str.charCodeAt(i);
}
return String.fromCharCode(...new Uint8Array(codeUnits.buffer));
let binary = '';
const uint8array = new Uint8Array(codeUnits.buffer);
for (let i = 0; i < uint8array.length; i++) {
binary += String.fromCharCode(uint8array[i]);
}
return binary;
}
/**
@ -1445,7 +1434,7 @@ export namespace WebFileSystemAccess {
}
export function supported(obj: any & Window): obj is FileSystemAccess {
const candidate = obj as FileSystemAccess;
const candidate = obj as FileSystemAccess | undefined;
if (typeof candidate?.showDirectoryPicker === 'function') {
return true;
}
@ -1483,7 +1472,13 @@ export class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
};
this._subscriptions.add(domEvent(document.body, 'keydown', true)(e => {
const event = new StandardKeyboardEvent(e);
// If Alt-key keydown event is repeated, ignore it #112347
// Only known to be necessary for Alt-Key at the moment #115810
if (event.keyCode === KeyCode.Alt && e.repeat) {
return;
}
if (e.altKey && !this._keyStatus.altKey) {
this._keyStatus.lastKeyPressed = 'alt';
@ -1595,3 +1590,9 @@ export class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
this._subscriptions.dispose();
}
}
export function getCookieValue(name: string): string | undefined {
const match = document.cookie.match('(^|[^;]+)\\s*' + name + '\\s*=\\s*([^;]+)'); // See https://stackoverflow.com/a/25490531
return match ? match.pop() : undefined;
}

View File

@ -10,7 +10,14 @@ export async function sha1Hex(str: string): Promise<string> {
// Prefer to use browser's crypto module
if (globalThis?.crypto?.subtle) {
const hash = await globalThis.crypto.subtle.digest({ name: 'sha-1' }, VSBuffer.fromString(str).buffer);
// Careful to use `dontUseNodeBuffer` when passing the
// buffer to the browser `crypto` API. Users reported
// native crashes in certain cases that we could trace
// back to passing node.js `Buffer` around
// (https://github.com/microsoft/vscode/issues/114227)
const buffer = VSBuffer.fromString(str, { dontUseNodeBuffer: true }).buffer;
const hash = await globalThis.crypto.subtle.digest({ name: 'sha-1' }, buffer);
return toHexString(hash);
}

View File

@ -15,10 +15,10 @@ import { cloneAndChange } from 'vs/base/common/objects';
import { escape } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { FileAccess, Schemas } from 'vs/base/common/network';
import { markdownEscapeEscapedCodicons } from 'vs/base/common/codicons';
import { markdownEscapeEscapedIcons } from 'vs/base/common/iconLabels';
import { resolvePath } from 'vs/base/common/resources';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { renderCodicons } from 'vs/base/browser/codicons';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
import { Event } from 'vs/base/common/event';
import { domEvent } from 'vs/base/browser/event';
@ -156,7 +156,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
};
renderer.paragraph = (text): string => {
if (markdown.supportThemeIcons) {
const elements = renderCodicons(text);
const elements = renderLabelWithIcons(text);
text = elements.map(e => typeof e === 'string' ? e : e.outerHTML).join('');
}
return `<p>${text}</p>`;
@ -218,7 +218,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
// We always pass the output through insane after this so that we don't rely on
// marked for sanitization.
markedOptions.sanitizer = (html: string): string => {
const match = markdown.isTrusted ? html.match(/^(<span[^<]+>)|(<\/\s*span>)$/) : undefined;
const match = markdown.isTrusted ? html.match(/^(<span[^>]+>)|(<\/\s*span>)$/) : undefined;
return match ? html : '';
};
markedOptions.sanitize = true;
@ -233,13 +233,13 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
}
// escape theme icons
if (markdown.supportThemeIcons) {
value = markdownEscapeEscapedCodicons(value);
value = markdownEscapeEscapedIcons(value);
}
const renderedMarkdown = marked.parse(value, markedOptions);
// sanitize with insane
element.innerHTML = sanitizeRenderedMarkdown(markdown, renderedMarkdown);
element.innerHTML = sanitizeRenderedMarkdown(markdown, renderedMarkdown) as string;
// signal that async code blocks can be now be inserted
signalInnerHTML!();
@ -261,13 +261,9 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
function sanitizeRenderedMarkdown(
options: { isTrusted?: boolean },
renderedMarkdown: string,
): string {
): string | TrustedHTML {
const insaneOptions = getInsaneOptions(options);
if (_ttpInsane) {
return _ttpInsane.createHTML(renderedMarkdown, insaneOptions) as unknown as string;
} else {
return insane(renderedMarkdown, insaneOptions);
}
return _ttpInsane?.createHTML(renderedMarkdown, insaneOptions) ?? insane(renderedMarkdown, insaneOptions);
}
function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOptions {
@ -302,12 +298,12 @@ function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOpti
'td': ['align']
},
filter(token: { tag: string; attrs: { readonly [key: string]: string; }; }): boolean {
if (token.tag === 'span' && options.isTrusted && (Object.keys(token.attrs).length === 1)) {
if (token.attrs['style']) {
if (token.tag === 'span' && options.isTrusted) {
if (token.attrs['style'] && (Object.keys(token.attrs).length === 1)) {
return !!token.attrs['style'].match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/);
} else if (token.attrs['class']) {
// The class should match codicon rendering in src\vs\base\common\codicons.ts
return !!token.attrs['class'].match(/^codicon codicon-[a-z\-]+( codicon-animation-[a-z\-]+)?$/);
return !!token.attrs['class'].match(/^codicon codicon-[a-z\-]+( codicon-modifier-[a-z\-]+)?$/);
}
return false;
}

View File

@ -104,7 +104,3 @@
.monaco-action-bar .action-item.action-dropdown-item > .action-label {
margin-right: 1px;
}
.monaco-action-bar .action-item.action-dropdown-item > .monaco-dropdown {
margin-right: 4px;
}

View File

@ -20,27 +20,6 @@ export abstract class BreadcrumbsItem {
abstract render(container: HTMLElement): void;
}
export class SimpleBreadcrumbsItem extends BreadcrumbsItem {
constructor(
readonly text: string,
readonly title: string = text
) {
super();
}
equals(other: this) {
return other === this || other instanceof SimpleBreadcrumbsItem && other.text === this.text && other.title === this.title;
}
render(container: HTMLElement): void {
let node = document.createElement('div');
node.title = this.title;
node.innerText = this.text;
container.appendChild(node);
}
}
export interface IBreadcrumbsWidgetStyles {
breadcrumbsBackground?: Color;
breadcrumbsForeground?: Color;
@ -333,7 +312,12 @@ export class BreadcrumbsWidget {
private _renderItem(item: BreadcrumbsItem, container: HTMLDivElement): void {
dom.clearNode(container);
container.className = '';
item.render(container);
try {
item.render(container);
} catch (err) {
container.innerText = '<<RENDER ERROR>>';
console.error(err);
}
container.tabIndex = -1;
container.setAttribute('role', 'listitem');
container.classList.add('monaco-breadcrumb-item');

View File

@ -11,15 +11,15 @@ import { mixin } from 'vs/base/common/objects';
import { Event as BaseEvent, Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch';
import { renderCodicons } from 'vs/base/browser/codicons';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
import { addDisposableListener, IFocusTracker, EventType, EventHelper, trackFocus, reset, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom';
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
import { IAction, IActionRunner } from 'vs/base/common/actions';
import { Action, IAction, IActionRunner } from 'vs/base/common/actions';
import { CSSIcon, Codicon } from 'vs/base/common/codicons';
export interface IButtonOptions extends IButtonStyles {
readonly title?: boolean | string;
readonly supportCodicons?: boolean;
readonly supportIcons?: boolean;
readonly secondary?: boolean;
}
@ -41,7 +41,7 @@ const defaultOptions: IButtonStyles = {
export interface IButton extends IDisposable {
readonly element: HTMLElement;
readonly onDidClick: BaseEvent<Event>;
readonly onDidClick: BaseEvent<Event | undefined>;
label: string;
icon: CSSIcon;
enabled: boolean;
@ -190,8 +190,8 @@ export class Button extends Disposable implements IButton {
set label(value: string) {
this._element.classList.add('monaco-text-button');
if (this.options.supportCodicons) {
reset(this._element, ...renderCodicons(value));
if (this.options.supportIcons) {
reset(this._element, ...renderLabelWithIcons(value));
} else {
this._element.textContent = value;
}
@ -203,7 +203,7 @@ export class Button extends Disposable implements IButton {
}
set icon(icon: CSSIcon) {
this._element.classList.add(...icon.classNames.split(' '));
this._element.classList.add(...CSSIcon.asClassNameArray(icon));
}
set enabled(value: boolean) {
@ -240,10 +240,12 @@ export interface IButtonWithDropdownOptions extends IButtonOptions {
export class ButtonWithDropdown extends Disposable implements IButton {
private readonly button: Button;
private readonly action: Action;
private readonly dropdownButton: Button;
readonly element: HTMLElement;
readonly onDidClick: BaseEvent<Event>;
private readonly _onDidClick = this._register(new Emitter<Event | undefined>());
readonly onDidClick = this._onDidClick.event;
constructor(container: HTMLElement, options: IButtonWithDropdownOptions) {
super();
@ -253,15 +255,16 @@ export class ButtonWithDropdown extends Disposable implements IButton {
container.appendChild(this.element);
this.button = this._register(new Button(this.element, options));
this.onDidClick = this.button.onDidClick;
this._register(this.button.onDidClick(e => this._onDidClick.fire(e)));
this.action = this._register(new Action('primaryAction', this.button.label, undefined, true, async () => this._onDidClick.fire(undefined)));
this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportCodicons: true }));
this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportIcons: true }));
this.dropdownButton.element.classList.add('monaco-dropdown-button');
this.dropdownButton.icon = Codicon.dropDownButton;
this._register(this.dropdownButton.onDidClick(() => {
this._register(this.dropdownButton.onDidClick(e => {
options.contextMenuProvider.showContextMenu({
getAnchor: () => this.dropdownButton.element,
getActions: () => options.actions,
getActions: () => [this.action, ...options.actions],
actionRunner: options.actionRunner,
onHide: () => this.dropdownButton.element.setAttribute('aria-expanded', 'false')
});
@ -271,6 +274,7 @@ export class ButtonWithDropdown extends Disposable implements IButton {
set label(value: string) {
this.button.label = value;
this.action.label = value;
}
set icon(icon: CSSIcon) {

View File

@ -101,12 +101,10 @@ export class Checkbox extends Widget {
const classes = ['monaco-custom-checkbox'];
if (this._opts.icon) {
classes.push(this._opts.icon.classNames);
} else {
classes.push('codicon'); // todo@aeschli: remove once codicon fully adopted
classes.push(...CSSIcon.asClassNameArray(this._opts.icon));
}
if (this._opts.actionClassName) {
classes.push(this._opts.actionClassName);
classes.push(...this._opts.actionClassName.split(' '));
}
if (this._checked) {
classes.push('checked');
@ -114,7 +112,7 @@ export class Checkbox extends Widget {
this.domNode = document.createElement('div');
this.domNode.title = this._opts.title;
this.domNode.className = classes.join(' ');
this.domNode.classList.add(...classes);
this.domNode.tabIndex = 0;
this.domNode.setAttribute('role', 'checkbox');
this.domNode.setAttribute('aria-checked', String(this._checked));

View File

@ -1,8 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.codicon-wrench-subaction {
opacity: 0.5;
}

View File

@ -3,13 +3,28 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.codicon-wrench-subaction {
opacity: 0.5;
}
@keyframes codicon-spin {
100% {
transform:rotate(360deg);
}
}
.codicon-animation-spin {
.codicon-sync.codicon-modifier-spin, .codicon-loading.codicon-modifier-spin{
/* Use steps to throttle FPS to reduce CPU usage */
animation: codicon-spin 1.5s steps(30) infinite;
}
.codicon-modifier-disabled {
opacity: 0.4;
}
/* custom speed & easing for loading icon */
.codicon-loading,
.codicon-tree-item-loading::before {
animation-duration: 1s !important;
animation-timing-function: cubic-bezier(0.53, 0.21, 0.29, 0.67) !important;
}

View File

@ -4,8 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./codicon/codicon';
import 'vs/css!./codicon/codicon-modifications';
import 'vs/css!./codicon/codicon-animations';
import 'vs/css!./codicon/codicon-modifiers';
import { Codicon } from 'vs/base/common/codicons';

View File

@ -182,7 +182,9 @@ export class Dialog extends Disposable {
button.label = mnemonicButtonLabel(buttonMap[index].label, true);
this._register(button.onDidClick(e => {
EventHelper.stop(e);
if (e) {
EventHelper.stop(e);
}
resolve({
button: buttonMap[index].index,
@ -307,7 +309,9 @@ export class Dialog extends Disposable {
}
}));
this.iconElement.classList.remove(...dialogErrorIcon.classNamesArray, ...dialogWarningIcon.classNamesArray, ...dialogInfoIcon.classNamesArray, ...Codicon.loading.classNamesArray);
const spinModifierClassName = 'codicon-modifier-spin';
this.iconElement.classList.remove(...dialogErrorIcon.classNamesArray, ...dialogWarningIcon.classNamesArray, ...dialogInfoIcon.classNamesArray, ...Codicon.loading.classNamesArray, spinModifierClassName);
switch (this.options.type) {
case 'error':
@ -317,7 +321,7 @@ export class Dialog extends Disposable {
this.iconElement.classList.add(...dialogWarningIcon.classNamesArray);
break;
case 'pending':
this.iconElement.classList.add(...Codicon.loading.classNamesArray, 'codicon-animation-spin');
this.iconElement.classList.add(...Codicon.loading.classNamesArray, spinModifierClassName);
break;
case 'none':
case 'info':

View File

@ -201,7 +201,7 @@ export class Dropdown extends BaseDropdown {
}
export interface IActionProvider {
getActions(): IAction[];
getActions(): readonly IAction[];
}
export interface IDropdownMenuOptions extends IBaseDropdownOptions {
@ -215,7 +215,7 @@ export interface IDropdownMenuOptions extends IBaseDropdownOptions {
export class DropdownMenu extends BaseDropdown {
private _contextMenuProvider: IContextMenuProvider;
private _menuOptions: IMenuOptions | undefined;
private _actions: IAction[] = [];
private _actions: readonly IAction[] = [];
private actionProvider?: IActionProvider;
private menuClassName: string;
private menuAsChild?: boolean;
@ -238,7 +238,7 @@ export class DropdownMenu extends BaseDropdown {
return this._menuOptions;
}
private get actions(): IAction[] {
private get actions(): readonly IAction[] {
if (this.actionProvider) {
return this.actionProvider.getActions();
}
@ -246,7 +246,7 @@ export class DropdownMenu extends BaseDropdown {
return this._actions;
}
private set actions(actions: IAction[]) {
private set actions(actions: readonly IAction[]) {
this._actions = actions;
}

View File

@ -170,7 +170,13 @@ export class ActionWithDropdownActionViewItem extends ActionViewItem {
super.render(container);
if (this.element) {
this.element.classList.add('action-dropdown-item');
this.dropdownMenuActionViewItem = new DropdownMenuActionViewItem(new Action('dropdownAction', undefined), (<IActionWithDropdownActionViewItemOptions>this.options).menuActionsOrProvider, this.contextMenuProvider, { classNames: ['dropdown', ...Codicon.dropDownButton.classNamesArray, ...(<IActionWithDropdownActionViewItemOptions>this.options).menuActionClassNames || []] });
const menuActionsProvider = {
getActions: () => {
const actionsProvider = (<IActionWithDropdownActionViewItemOptions>this.options).menuActionsOrProvider;
return [this._action, ...(Array.isArray(actionsProvider) ? actionsProvider : actionsProvider.getActions())];
}
};
this.dropdownMenuActionViewItem = new DropdownMenuActionViewItem(this._register(new Action('dropdownAction', undefined)), menuActionsProvider, this.contextMenuProvider, { classNames: ['dropdown', ...Codicon.dropDownButton.classNamesArray, ...(<IActionWithDropdownActionViewItemOptions>this.options).menuActionClassNames || []] });
this.dropdownMenuActionViewItem.render(this.element);
}
}

View File

@ -258,6 +258,10 @@ export class FindInput extends Widget {
this.onmousedown(this.inputBox.inputElement, (e) => this._onMouseDown.fire(e));
}
public get onDidChange(): Event<string> {
return this.inputBox.onDidChange;
}
public enable(): void {
this.domNode.classList.remove('disabled');
this.inputBox.enable();

View File

@ -5,7 +5,7 @@
import * as objects from 'vs/base/common/objects';
import * as dom from 'vs/base/browser/dom';
import { renderCodicons } from 'vs/base/browser/codicons';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
export interface IHighlight {
start: number;
@ -21,7 +21,7 @@ export class HighlightedLabel {
private highlights: IHighlight[] = [];
private didEverRender: boolean = false;
constructor(container: HTMLElement, private supportCodicons: boolean) {
constructor(container: HTMLElement, private supportIcons: boolean) {
this.domNode = document.createElement('span');
this.domNode.className = 'monaco-highlighted-label';
@ -61,12 +61,12 @@ export class HighlightedLabel {
}
if (pos < highlight.start) {
const substring = this.text.substring(pos, highlight.start);
children.push(dom.$('span', undefined, ...this.supportCodicons ? renderCodicons(substring) : [substring]));
children.push(dom.$('span', undefined, ...this.supportIcons ? renderLabelWithIcons(substring) : [substring]));
pos = highlight.end;
}
const substring = this.text.substring(highlight.start, highlight.end);
const element = dom.$('span.highlight', undefined, ...this.supportCodicons ? renderCodicons(substring) : [substring]);
const element = dom.$('span.highlight', undefined, ...this.supportIcons ? renderLabelWithIcons(substring) : [substring]);
if (highlight.extraClasses) {
element.classList.add(highlight.extraClasses);
}
@ -76,7 +76,7 @@ export class HighlightedLabel {
if (pos < this.text.length) {
const substring = this.text.substring(pos,);
children.push(dom.$('span', undefined, ...this.supportCodicons ? renderCodicons(substring) : [substring]));
children.push(dom.$('span', undefined, ...this.supportIcons ? renderLabelWithIcons(substring) : [substring]));
}
dom.reset(this.domNode, ...children);

View File

@ -45,10 +45,13 @@
}
.monaco-hover hr {
box-sizing: border-box;
border-left: 0px;
border-right: 0px;
margin-top: 4px;
margin-bottom: -4px;
margin-left: -10px;
margin-right: -10px;
margin-left: -8px;
margin-right: -8px;
height: 1px;
}

View File

@ -27,7 +27,9 @@ export class HoverWidget extends Disposable {
this.contentsDomNode = document.createElement('div');
this.contentsDomNode.className = 'monaco-hover-content';
this._scrollbar = this._register(new DomScrollableElement(this.contentsDomNode, {}));
this._scrollbar = this._register(new DomScrollableElement(this.contentsDomNode, {
consumeMouseWheelIfScrollbarIsNeeded: true
}));
this.containerDomNode.appendChild(this._scrollbar.getDomNode());
}

View File

@ -20,4 +20,5 @@ export interface IHoverDelegateOptions {
export interface IHoverDelegate {
showHover(options: IHoverDelegateOptions): IDisposable | undefined;
hideHover(): void;
}

View File

@ -16,16 +16,18 @@ import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { isFunction, isString } from 'vs/base/common/types';
import { domEvent } from 'vs/base/browser/event';
import { localize } from 'vs/nls';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
export interface IIconLabelCreationOptions {
supportHighlights?: boolean;
supportDescriptionHighlights?: boolean;
supportCodicons?: boolean;
supportIcons?: boolean;
hoverDelegate?: IHoverDelegate;
}
export interface IIconLabelMarkdownString {
markdown: IMarkdownString | string | undefined | (() => Promise<IMarkdownString | string | undefined>);
markdown: IMarkdownString | string | undefined | ((token: CancellationToken) => Promise<IMarkdownString | string | undefined>);
markdownNotSupportedFallback: string | undefined;
}
@ -114,13 +116,13 @@ export class IconLabel extends Disposable {
this.descriptionContainer = this._register(new FastLabelNode(dom.append(this.labelContainer, dom.$('span.monaco-icon-description-container'))));
if (options?.supportHighlights) {
this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportCodicons);
this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportIcons);
} else {
this.nameNode = new Label(nameContainer);
}
if (options?.supportDescriptionHighlights) {
this.descriptionNodeFactory = () => new HighlightedLabel(dom.append(this.descriptionContainer.element, dom.$('span.label-description')), !!options.supportCodicons);
this.descriptionNodeFactory = () => new HighlightedLabel(dom.append(this.descriptionContainer.element, dom.$('span.label-description')), !!options.supportIcons);
} else {
this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description'))));
}
@ -190,29 +192,47 @@ export class IconLabel extends Disposable {
}
}
private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString): void {
htmlElement.removeAttribute('title');
let tooltip: () => Promise<string | IMarkdownString | undefined>;
private static adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean) {
if (hoverOptions && isHovering) {
if (mouseX !== undefined) {
(<IHoverDelegateTarget>hoverOptions.target).x = mouseX + 10;
}
hoverDelegate.showHover(hoverOptions);
}
}
private getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise<string | IMarkdownString | undefined> {
if (isString(markdownTooltip)) {
tooltip = async () => markdownTooltip;
return async () => markdownTooltip;
} else if (isFunction(markdownTooltip.markdown)) {
tooltip = markdownTooltip.markdown;
return markdownTooltip.markdown;
} else {
const markdown = markdownTooltip.markdown;
tooltip = async () => markdown;
return async () => markdown;
}
}
private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString): void {
htmlElement.setAttribute('title', '');
htmlElement.removeAttribute('title');
let tooltip = this.getTooltipForCustom(markdownTooltip);
// Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely.
// On Mac, the delay is 1500.
const hoverDelay = isMacintosh ? 1500 : 500;
let hoverOptions: IHoverDelegateOptions | undefined;
let mouseX: number | undefined;
let isHovering = false;
let tokenSource: CancellationTokenSource;
function mouseOver(this: HTMLElement, e: MouseEvent): any {
if (isHovering) {
return;
}
tokenSource = new CancellationTokenSource();
function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): any {
isHovering = false;
hoverOptions = undefined;
tokenSource.dispose(true);
mouseLeaveDisposable.dispose();
mouseDownDisposable.dispose();
}
@ -232,22 +252,26 @@ export class IconLabel extends Disposable {
targetElements: [this],
dispose: () => { }
};
const resolvedTooltip = await tooltip();
hoverOptions = {
text: localize('iconLabel.loading', "Loading..."),
target,
anchorPosition: AnchorPosition.BELOW
};
IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined);
if (resolvedTooltip) {
hoverOptions = {
text: resolvedTooltip,
target,
anchorPosition: AnchorPosition.BELOW
};
// awaiting the tooltip could take a while. Make sure we're still hovering.
IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
} else {
hoverDelegate.hideHover();
}
}
// awaiting the tooltip could take a while. Make sure we're still hovering.
if (hoverOptions && isHovering) {
if (mouseX !== undefined) {
(<IHoverDelegateTarget>hoverOptions.target).x = mouseX + 10;
}
hoverDelegate.showHover(hoverOptions);
}
}
mouseMoveDisposable.dispose();
}, hoverDelay);
@ -336,7 +360,7 @@ class LabelWithHighlights {
private singleLabel: HighlightedLabel | undefined = undefined;
private options: IIconLabelValueOptions | undefined;
constructor(private container: HTMLElement, private supportCodicons: boolean) { }
constructor(private container: HTMLElement, private supportIcons: boolean) { }
setLabel(label: string | string[], options?: IIconLabelValueOptions): void {
if (this.label === label && equals(this.options, options)) {
@ -350,7 +374,7 @@ class LabelWithHighlights {
if (!this.singleLabel) {
this.container.innerText = '';
this.container.classList.remove('multiple');
this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), this.supportCodicons);
this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), this.supportIcons);
}
this.singleLabel.set(label, options?.matches, undefined, options?.labelEscapeNewLines);
@ -368,7 +392,7 @@ class LabelWithHighlights {
const id = options?.domId && `${options?.domId}_${i}`;
const name = dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' });
const highlightedLabel = new HighlightedLabel(dom.append(this.container, name), this.supportCodicons);
const highlightedLabel = new HighlightedLabel(dom.append(this.container, name), this.supportIcons);
highlightedLabel.set(l, m, undefined, options?.labelEscapeNewLines);
if (i < label.length - 1) {

View File

@ -4,21 +4,22 @@
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { CSSIcon } from 'vs/base/common/codicons';
const renderCodiconsRegex = /(\\)?\$\((([a-z0-9\-]+?)(?:~([a-z0-9\-]*?))?)\)/gi;
const labelWithIconsRegex = /(\\)?\$\(([a-z\-]+(?:~[a-z\-]+)?)\)/gi;
export function renderCodicons(text: string): Array<HTMLSpanElement | string> {
export function renderLabelWithIcons(text: string): Array<HTMLSpanElement | string> {
const elements = new Array<HTMLSpanElement | string>();
let match: RegExpMatchArray | null;
let textStart = 0, textStop = 0;
while ((match = renderCodiconsRegex.exec(text)) !== null) {
while ((match = labelWithIconsRegex.exec(text)) !== null) {
textStop = match.index || 0;
elements.push(text.substring(textStart, textStop));
textStart = (match.index || 0) + match[0].length;
const [, escaped, codicon, name, animation] = match;
elements.push(escaped ? `$(${codicon})` : renderCodicon(name, animation));
const [, escaped, codicon] = match;
elements.push(escaped ? `$(${codicon})` : renderIcon({ id: codicon }));
}
if (textStart < text.length) {
@ -27,6 +28,8 @@ export function renderCodicons(text: string): Array<HTMLSpanElement | string> {
return elements;
}
export function renderCodicon(name: string, animation: string): HTMLSpanElement {
return dom.$(`span.codicon.codicon-${name}${animation ? `.codicon-animation-${animation}` : ''}`);
export function renderIcon(icon: CSSIcon): HTMLSpanElement {
const node = dom.$(`span`);
node.classList.add(...CSSIcon.asClassNameArray(icon));
return node;
}

View File

@ -55,6 +55,10 @@
white-space: pre; /* enable to show labels that include multiple whitespaces */
}
.monaco-icon-label.nowrap > .monaco-icon-label-container > .monaco-icon-description-container > .label-description{
white-space: nowrap
}
.vs .monaco-icon-label > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
opacity: .95;
}
@ -64,6 +68,16 @@
font-style: italic;
}
.monaco-icon-label.deprecated {
text-decoration: line-through;
opacity: 0.66;
}
/* make sure apply italic font style to decorations as well */
.monaco-icon-label.italic::after {
font-style: italic;
}
.monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
.monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
text-decoration: line-through;

View File

@ -4,16 +4,16 @@
*--------------------------------------------------------------------------------------------*/
import { reset } from 'vs/base/browser/dom';
import { renderCodicons } from 'vs/base/browser/codicons';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
export class CodiconLabel {
export class SimpleIconLabel {
constructor(
private readonly _container: HTMLElement
) { }
set text(text: string) {
reset(this._container, ...renderCodicons(text ?? ''));
reset(this._container, ...renderLabelWithIcons(text ?? ''));
}
set title(title: string) {

View File

@ -17,20 +17,20 @@
border: 1px solid transparent;
}
.monaco-inputbox > .wrapper > .input,
.monaco-inputbox > .wrapper > .mirror {
.monaco-inputbox > .ibwrapper > .input,
.monaco-inputbox > .ibwrapper > .mirror {
/* Customizable */
padding: 4px;
}
.monaco-inputbox > .wrapper {
.monaco-inputbox > .ibwrapper {
position: relative;
width: 100%;
height: 100%;
}
.monaco-inputbox > .wrapper > .input {
.monaco-inputbox > .ibwrapper > .input {
display: inline-block;
box-sizing: border-box;
width: 100%;
@ -43,26 +43,26 @@
color: inherit;
}
.monaco-inputbox > .wrapper > input {
.monaco-inputbox > .ibwrapper > input {
text-overflow: ellipsis;
}
.monaco-inputbox > .wrapper > textarea.input {
.monaco-inputbox > .ibwrapper > textarea.input {
display: block;
-ms-overflow-style: none; /* IE 10+: hide scrollbars */
scrollbar-width: none; /* Firefox: hide scrollbars */
outline: none;
}
.monaco-inputbox > .wrapper > textarea.input::-webkit-scrollbar {
.monaco-inputbox > .ibwrapper > textarea.input::-webkit-scrollbar {
display: none; /* Chrome + Safari: hide scrollbar */
}
.monaco-inputbox > .wrapper > textarea.input.empty {
.monaco-inputbox > .ibwrapper > textarea.input.empty {
white-space: nowrap;
}
.monaco-inputbox > .wrapper > .mirror {
.monaco-inputbox > .ibwrapper > .mirror {
position: absolute;
display: inline-block;
width: 100%;

View File

@ -157,7 +157,7 @@ export class InputBox extends Widget {
let tagName = this.options.flexibleHeight ? 'textarea' : 'input';
let wrapper = dom.append(this.element, $('.wrapper'));
let wrapper = dom.append(this.element, $('.ibwrapper'));
this.input = dom.append(wrapper, $(tagName + '.input.empty'));
this.input.setAttribute('autocorrect', 'off');
this.input.setAttribute('autocapitalize', 'off');
@ -398,7 +398,7 @@ export class InputBox extends Widget {
return !!this.validation && !this.validation(this.value);
}
public validate(): boolean {
public validate(): MessageType | undefined {
let errorMsg: IMessage | null = null;
if (this.validation) {
@ -414,7 +414,7 @@ export class InputBox extends Widget {
}
}
return !errorMsg;
return errorMsg?.type;
}
public stylesForType(type: MessageType | undefined): { border: Color | undefined; background: Color | undefined; foreground: Color | undefined } {

View File

@ -33,7 +33,7 @@
.monaco-list-row {
position: absolute;
box-sizing: border-box;
box-sizing: border-box;
overflow: hidden;
width: 100%;
}
@ -49,7 +49,9 @@
}
/* Focus */
.monaco-list.element-focused, .monaco-list.selection-single, .monaco-list.selection-multiple {
.monaco-list.element-focused,
.monaco-list.selection-single,
.monaco-list.selection-multiple {
outline: 0 !important;
}
@ -64,6 +66,7 @@
border-radius: 10px;
font-size: 12px;
position: absolute;
z-index: 1000;
}
/* Type filter */

View File

@ -432,8 +432,24 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
const deleteRange = { start, end: start + deleteCount };
const removeRange = Range.intersect(previousRenderRange, deleteRange);
// try to reuse rows, avoid removing them from DOM
const rowsToDispose = new Map<string, [IRow, T, number, number][]>();
for (let i = removeRange.start; i < removeRange.end; i++) {
this.removeItemFromDOM(i);
const item = this.items[i];
item.dragStartDisposable.dispose();
if (item.row) {
let rows = rowsToDispose.get(item.templateId);
if (!rows) {
rows = [];
rowsToDispose.set(item.templateId, rows);
}
rows.push([item.row, item.element, i, item.size]);
}
item.row = null;
}
const previousRestRange: IRange = { start: start + deleteCount, end: this.items.length };
@ -491,7 +507,34 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
for (const range of insertRanges) {
for (let i = range.start; i < range.end; i++) {
this.insertItemInDOM(i, beforeElement);
const item = this.items[i];
const rows = rowsToDispose.get(item.templateId);
const rowData = rows?.pop();
if (!rowData) {
this.insertItemInDOM(i, beforeElement);
} else {
const [row, element, index, size] = rowData;
const renderer = this.renderers.get(item.templateId);
if (renderer && renderer.disposeElement) {
renderer.disposeElement(element, index, row.templateData, size);
}
this.insertItemInDOM(i, beforeElement, row);
}
}
}
for (const [templateId, rows] of rowsToDispose) {
for (const [row, element, index, size] of rows) {
const renderer = this.renderers.get(templateId);
if (renderer && renderer.disposeElement) {
renderer.disposeElement(element, index, row.templateData, size);
}
this.cache.release(row);
}
}
@ -699,24 +742,26 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
// DOM operations
private insertItemInDOM(index: number, beforeElement: HTMLElement | null): void {
private insertItemInDOM(index: number, beforeElement: HTMLElement | null, row?: IRow): void {
const item = this.items[index];
if (!item.row) {
item.row = this.cache.alloc(item.templateId);
const role = this.accessibilityProvider.getRole(item.element) || 'listitem';
item.row!.domNode!.setAttribute('role', role);
const checked = this.accessibilityProvider.isChecked(item.element);
if (typeof checked !== 'undefined') {
item.row!.domNode!.setAttribute('aria-checked', String(!!checked));
}
item.row = row ?? this.cache.alloc(item.templateId);
}
if (!item.row.domNode!.parentElement) {
const role = this.accessibilityProvider.getRole(item.element) || 'listitem';
item.row.domNode.setAttribute('role', role);
const checked = this.accessibilityProvider.isChecked(item.element);
if (typeof checked !== 'undefined') {
item.row.domNode.setAttribute('aria-checked', String(!!checked));
}
if (!item.row.domNode.parentElement) {
if (beforeElement) {
this.rowsContainer.insertBefore(item.row.domNode!, beforeElement);
this.rowsContainer.insertBefore(item.row.domNode, beforeElement);
} else {
this.rowsContainer.appendChild(item.row.domNode!);
this.rowsContainer.appendChild(item.row.domNode);
}
}
@ -734,10 +779,10 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
const uri = this.dnd.getDragURI(item.element);
item.dragStartDisposable.dispose();
item.row.domNode!.draggable = !!uri;
item.row.domNode.draggable = !!uri;
if (uri) {
const onDragStart = domEvent(item.row.domNode!, 'dragstart');
const onDragStart = domEvent(item.row.domNode, 'dragstart');
item.dragStartDisposable = onDragStart(event => this.onDragStart(item.element, uri, event));
}
@ -768,36 +813,39 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
}
private updateItemInDOM(item: IItem<T>, index: number): void {
item.row!.domNode!.style.top = `${this.elementTop(index)}px`;
item.row!.domNode.style.top = `${this.elementTop(index)}px`;
if (this.setRowHeight) {
item.row!.domNode!.style.height = `${item.size}px`;
item.row!.domNode.style.height = `${item.size}px`;
}
if (this.setRowLineHeight) {
item.row!.domNode!.style.lineHeight = `${item.size}px`;
item.row!.domNode.style.lineHeight = `${item.size}px`;
}
item.row!.domNode!.setAttribute('data-index', `${index}`);
item.row!.domNode!.setAttribute('data-last-element', index === this.length - 1 ? 'true' : 'false');
item.row!.domNode!.setAttribute('aria-setsize', String(this.accessibilityProvider.getSetSize(item.element, index, this.length)));
item.row!.domNode!.setAttribute('aria-posinset', String(this.accessibilityProvider.getPosInSet(item.element, index)));
item.row!.domNode!.setAttribute('id', this.getElementDomId(index));
item.row!.domNode.setAttribute('data-index', `${index}`);
item.row!.domNode.setAttribute('data-last-element', index === this.length - 1 ? 'true' : 'false');
item.row!.domNode.setAttribute('aria-setsize', String(this.accessibilityProvider.getSetSize(item.element, index, this.length)));
item.row!.domNode.setAttribute('aria-posinset', String(this.accessibilityProvider.getPosInSet(item.element, index)));
item.row!.domNode.setAttribute('id', this.getElementDomId(index));
item.row!.domNode!.classList.toggle('drop-target', item.dropTarget);
item.row!.domNode.classList.toggle('drop-target', item.dropTarget);
}
private removeItemFromDOM(index: number): void {
const item = this.items[index];
item.dragStartDisposable.dispose();
const renderer = this.renderers.get(item.templateId);
if (item.row && renderer && renderer.disposeElement) {
renderer.disposeElement(item.element, index, item.row.templateData, item.size);
}
if (item.row) {
const renderer = this.renderers.get(item.templateId);
this.cache.release(item.row!);
item.row = null;
if (renderer && renderer.disposeElement) {
renderer.disposeElement(item.element, index, item.row.templateData, item.size);
}
this.cache.release(item.row);
item.row = null;
}
if (this.horizontalScrolling) {
this.eventuallyUpdateScrollWidth();
@ -809,14 +857,14 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
return scrollPosition.scrollTop;
}
setScrollTop(scrollTop: number): void {
setScrollTop(scrollTop: number, reuseAnimation?: boolean): void {
if (this.scrollableElementUpdateDisposable) {
this.scrollableElementUpdateDisposable.dispose();
this.scrollableElementUpdateDisposable = null;
this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight });
}
this.scrollableElement.setScrollPosition({ scrollTop });
this.scrollableElement.setScrollPosition({ scrollTop, reuseAnimation });
}
getScrollLeft(): number {
@ -895,9 +943,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
this.render(previousRenderRange, e.scrollTop, e.height, e.scrollLeft, e.scrollWidth);
if (this.supportDynamicHeights) {
// Don't update scrollTop from within an scroll event
// so we don't break smooth scrolling. #104144
this._rerender(e.scrollTop, e.height, false);
this._rerender(e.scrollTop, e.height, e.inSmoothScrolling);
}
} catch (err) {
console.error('Got bad scroll event:', e);
@ -1027,7 +1073,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
const item = this.items[index]!;
item.dropTarget = true;
if (item.row && item.row.domNode) {
if (item.row) {
item.row.domNode.classList.add('drop-target');
}
}
@ -1037,7 +1083,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
const item = this.items[index]!;
item.dropTarget = false;
if (item.row && item.row.domNode) {
if (item.row) {
item.row.domNode.classList.remove('drop-target');
}
}
@ -1167,7 +1213,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
* Given a stable rendered state, checks every rendered element whether it needs
* to be probed for dynamic height. Adjusts scroll height and top if necessary.
*/
private _rerender(renderTop: number, renderHeight: number, updateScrollTop: boolean = true): void {
private _rerender(renderTop: number, renderHeight: number, inSmoothScrolling?: boolean): void {
const previousRenderRange = this.getRenderRange(renderTop, renderHeight);
// Let's remember the second element's position, this helps in scrolling up
@ -1233,8 +1279,15 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
}
}
if (updateScrollTop && typeof anchorElementIndex === 'number') {
this.scrollTop = this.elementTop(anchorElementIndex) - anchorElementTopDelta!;
if (typeof anchorElementIndex === 'number') {
// To compute a destination scroll top, we need to take into account the current smooth scrolling
// animation, and then reuse it with a new target (to avoid prolonging the scroll)
// See https://github.com/microsoft/vscode/issues/104144
// See https://github.com/microsoft/vscode/pull/104284
// See https://github.com/microsoft/vscode/issues/107704
const deltaScrollTop = this.scrollable.getFutureScrollPosition().scrollTop - renderTop;
const newScrollTop = this.elementTop(anchorElementIndex) - anchorElementTopDelta! + deltaScrollTop;
this.setScrollTop(newScrollTop, inSmoothScrolling);
}
this._onDidChangeContentHeight.fire(this.contentHeight);
@ -1256,7 +1309,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
const size = item.size;
if (!this.setRowHeight && item.row && item.row.domNode) {
if (!this.setRowHeight && item.row) {
let newSize = item.row.domNode.offsetHeight;
item.size = newSize;
item.lastDynamicHeightWidth = this.renderWidth;
@ -1265,8 +1318,8 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
const row = this.cache.alloc(item.templateId);
row.domNode!.style.height = '';
this.rowsContainer.appendChild(row.domNode!);
row.domNode.style.height = '';
this.rowsContainer.appendChild(row.domNode);
const renderer = this.renderers.get(item.templateId);
if (renderer) {
@ -1277,14 +1330,14 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
}
}
item.size = row.domNode!.offsetHeight;
item.size = row.domNode.offsetHeight;
if (this.virtualDelegate.setDynamicHeight) {
this.virtualDelegate.setDynamicHeight(item.element, item.size);
}
item.lastDynamicHeightWidth = this.renderWidth;
this.rowsContainer.removeChild(row.domNode!);
this.rowsContainer.removeChild(row.domNode);
this.cache.release(row);
return item.size - size;

View File

@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import { $ } from 'vs/base/browser/dom';
export interface IRow {
domNode: HTMLElement | null;
domNode: HTMLElement;
templateId: string;
templateData: any;
}
@ -84,7 +84,6 @@ export class RowCache<T> implements IDisposable {
for (const cachedRow of cachedRows) {
const renderer = this.getRenderer(templateId);
renderer.disposeTemplate(cachedRow.templateData);
cachedRow.domNode = null;
cachedRow.templateData = null;
}
});

View File

@ -18,11 +18,12 @@ import { ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable';
import { Event } from 'vs/base/common/event';
import { AnchorAlignment, layout, LayoutAnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
import { isLinux, isMacintosh } from 'vs/base/common/platform';
import { Codicon, registerCodicon, stripCodicons } from 'vs/base/common/codicons';
import { Codicon, registerCodicon } from 'vs/base/common/codicons';
import { BaseActionViewItem, ActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { formatRule } from 'vs/base/browser/ui/codicons/codiconStyles';
import { isFirefox } from 'vs/base/browser/browser';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { stripIcons } from 'vs/base/common/iconLabels';
export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/;
export const MENU_ESCAPED_MNEMONIC_REGEX = /(&amp;)?(&amp;)([^\s&])/g;
@ -532,7 +533,7 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
if (this.options.label) {
clearNode(this.label);
let label = stripCodicons(this.getAction().label);
let label = stripIcons(this.getAction().label);
if (label) {
const cleanLabel = cleanMnemonic(label);
if (!this.options.enableMnemonics) {

View File

@ -126,9 +126,11 @@ export class MenuBar extends Disposable {
let eventHandled = true;
const key = !!e.key ? e.key.toLocaleLowerCase() : '';
if (event.equals(KeyCode.LeftArrow) || (isMacintosh && event.equals(KeyCode.Tab | KeyMod.Shift))) {
const tabNav = isMacintosh && this.options.compactMode === undefined;
if (event.equals(KeyCode.LeftArrow) || (tabNav && event.equals(KeyCode.Tab | KeyMod.Shift))) {
this.focusPrevious();
} else if (event.equals(KeyCode.RightArrow) || (isMacintosh && event.equals(KeyCode.Tab))) {
} else if (event.equals(KeyCode.RightArrow) || (tabNav && event.equals(KeyCode.Tab))) {
this.focusNext();
} else if (event.equals(KeyCode.Escape) && this.isFocused && !this.isOpen) {
this.setUnfocusedState();

View File

@ -83,7 +83,7 @@ export abstract class AbstractScrollbar extends Widget {
* Creates the dom node for an arrow & adds it to the container
*/
protected _createArrow(opts: ScrollbarArrowOptions): void {
let arrow = this._register(new ScrollbarArrow(opts));
const arrow = this._register(new ScrollbarArrow(opts));
this.domNode.domNode.appendChild(arrow.bgDomNode);
this.domNode.domNode.appendChild(arrow.domNode);
}
@ -186,10 +186,10 @@ export abstract class AbstractScrollbar extends Widget {
}
public delegateMouseDown(e: IMouseEvent): void {
let domTop = this.domNode.domNode.getClientRects()[0].top;
let sliderStart = domTop + this._scrollbarState.getSliderPosition();
let sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize();
let mousePos = this._sliderMousePosition(e);
const domTop = this.domNode.domNode.getClientRects()[0].top;
const sliderStart = domTop + this._scrollbarState.getSliderPosition();
const sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize();
const mousePos = this._sliderMousePosition(e);
if (sliderStart <= mousePos && mousePos <= sliderStop) {
// Act as if it was a mouse down on the slider
if (e.leftButton) {
@ -263,7 +263,7 @@ export abstract class AbstractScrollbar extends Widget {
private _setDesiredScrollPositionNow(_desiredScrollPosition: number): void {
let desiredScrollPosition: INewScrollPosition = {};
const desiredScrollPosition: INewScrollPosition = {};
this.writeScrollPosition(desiredScrollPosition, _desiredScrollPosition);
this._scrollable.setScrollPositionNow(desiredScrollPosition);
@ -278,6 +278,10 @@ export abstract class AbstractScrollbar extends Widget {
}
}
public isNeeded(): boolean {
return this._scrollbarState.isNeeded();
}
// ----------------- Overwrite these
protected abstract _renderDomNode(largeSize: number, smallSize: number): void;

View File

@ -38,8 +38,8 @@ export class HorizontalScrollbar extends AbstractScrollbar {
});
if (options.horizontalHasArrows) {
let arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
let scrollbarDelta = (options.horizontalScrollbarSize - ARROW_IMG_SIZE) / 2;
const arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
const scrollbarDelta = (options.horizontalScrollbarSize - ARROW_IMG_SIZE) / 2;
this._createArrow({
className: 'scra',

View File

@ -187,7 +187,7 @@ export abstract class AbstractScrollableElement extends Widget {
this._onScroll.fire(e);
}));
let scrollbarHost: ScrollbarHost = {
const scrollbarHost: ScrollbarHost = {
onMouseWheel: (mouseWheelEvent: StandardWheelEvent) => this._onMouseWheel(mouseWheelEvent),
onDragStart: () => this._onDragStart(),
onDragEnd: () => this._onDragEnd(),
@ -325,7 +325,7 @@ export abstract class AbstractScrollableElement extends Widget {
// -------------------- mouse wheel scrolling --------------------
private _setListeningToMouseWheel(shouldListen: boolean): void {
let isListening = (this._mouseWheelToDispose.length > 0);
const isListening = (this._mouseWheelToDispose.length > 0);
if (isListening === shouldListen) {
// No change
@ -337,7 +337,7 @@ export abstract class AbstractScrollableElement extends Widget {
// Start listening (if necessary)
if (shouldListen) {
let onMouseWheel = (browserEvent: IMouseWheelEvent) => {
const onMouseWheel = (browserEvent: IMouseWheelEvent) => {
this._onMouseWheel(new StandardWheelEvent(browserEvent));
};
@ -426,7 +426,15 @@ export abstract class AbstractScrollableElement extends Widget {
}
}
if (this._options.alwaysConsumeMouseWheel || didScroll) {
let consumeMouseWheel = didScroll;
if (!consumeMouseWheel && this._options.alwaysConsumeMouseWheel) {
consumeMouseWheel = true;
}
if (!consumeMouseWheel && this._options.consumeMouseWheelIfScrollbarIsNeeded && (this._verticalScrollbar.isNeeded() || this._horizontalScrollbar.isNeeded())) {
consumeMouseWheel = true;
}
if (consumeMouseWheel) {
e.preventDefault();
e.stopPropagation();
}
@ -473,8 +481,8 @@ export abstract class AbstractScrollableElement extends Widget {
if (this._options.useShadows) {
const scrollState = this._scrollable.getCurrentScrollPosition();
let enableTop = scrollState.scrollTop > 0;
let enableLeft = scrollState.scrollLeft > 0;
const enableTop = scrollState.scrollTop > 0;
const enableLeft = scrollState.scrollLeft > 0;
this._leftShadowDomNode!.setClassName('shadow' + (enableLeft ? ' left' : ''));
this._topShadowDomNode!.setClassName('shadow' + (enableTop ? ' top' : ''));
@ -549,8 +557,12 @@ export class SmoothScrollableElement extends AbstractScrollableElement {
super(element, options, scrollable);
}
public setScrollPosition(update: INewScrollPosition): void {
this._scrollable.setScrollPositionNow(update);
public setScrollPosition(update: INewScrollPosition & { reuseAnimation?: boolean }): void {
if (update.reuseAnimation) {
this._scrollable.setScrollPositionSmooth(update, update.reuseAnimation);
} else {
this._scrollable.setScrollPositionNow(update);
}
}
public getScrollPosition(): IScrollPosition {
@ -593,12 +605,13 @@ export class DomScrollableElement extends ScrollableElement {
}
function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableElementResolvedOptions {
let result: ScrollableElementResolvedOptions = {
const result: ScrollableElementResolvedOptions = {
lazyRender: (typeof opts.lazyRender !== 'undefined' ? opts.lazyRender : false),
className: (typeof opts.className !== 'undefined' ? opts.className : ''),
useShadows: (typeof opts.useShadows !== 'undefined' ? opts.useShadows : true),
handleMouseWheel: (typeof opts.handleMouseWheel !== 'undefined' ? opts.handleMouseWheel : true),
flipAxes: (typeof opts.flipAxes !== 'undefined' ? opts.flipAxes : false),
consumeMouseWheelIfScrollbarIsNeeded: (typeof opts.consumeMouseWheelIfScrollbarIsNeeded !== 'undefined' ? opts.consumeMouseWheelIfScrollbarIsNeeded : false),
alwaysConsumeMouseWheel: (typeof opts.alwaysConsumeMouseWheel !== 'undefined' ? opts.alwaysConsumeMouseWheel : false),
scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false),
mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1),

View File

@ -40,6 +40,11 @@ export interface ScrollableElementCreationOptions {
* Defaults to false.
*/
scrollYToX?: boolean;
/**
* Consume all mouse wheel events if a scrollbar is needed (i.e. scrollSize > size).
* Defaults to false.
*/
consumeMouseWheelIfScrollbarIsNeeded?: boolean;
/**
* Always consume mouse wheel events, even when scrolling is no longer possible.
* Defaults to false.
@ -136,6 +141,7 @@ export interface ScrollableElementResolvedOptions {
handleMouseWheel: boolean;
flipAxes: boolean;
scrollYToX: boolean;
consumeMouseWheelIfScrollbarIsNeeded: boolean;
alwaysConsumeMouseWheel: boolean;
mouseWheelScrollSensitivity: number;
fastScrollSensitivity: number;

View File

@ -88,7 +88,7 @@ export class ScrollbarArrow extends Widget {
}
private _arrowMouseDown(e: IMouseEvent): void {
let scheduleRepeater = () => {
const scheduleRepeater = () => {
this._mousedownRepeatTimer.cancelAndSet(() => this._onActivate(), 1000 / 24);
};

View File

@ -85,7 +85,7 @@ export class ScrollbarState {
}
public setVisibleSize(visibleSize: number): boolean {
let iVisibleSize = Math.round(visibleSize);
const iVisibleSize = Math.round(visibleSize);
if (this._visibleSize !== iVisibleSize) {
this._visibleSize = iVisibleSize;
this._refreshComputedValues();
@ -95,7 +95,7 @@ export class ScrollbarState {
}
public setScrollSize(scrollSize: number): boolean {
let iScrollSize = Math.round(scrollSize);
const iScrollSize = Math.round(scrollSize);
if (this._scrollSize !== iScrollSize) {
this._scrollSize = iScrollSize;
this._refreshComputedValues();
@ -105,7 +105,7 @@ export class ScrollbarState {
}
public setScrollPosition(scrollPosition: number): boolean {
let iScrollPosition = Math.round(scrollPosition);
const iScrollPosition = Math.round(scrollPosition);
if (this._scrollPosition !== iScrollPosition) {
this._scrollPosition = iScrollPosition;
this._refreshComputedValues();
@ -198,7 +198,7 @@ export class ScrollbarState {
return 0;
}
let desiredSliderPosition = offset - this._arrowSize - this._computedSliderSize / 2;
const desiredSliderPosition = offset - this._arrowSize - this._computedSliderSize / 2;
return Math.round(desiredSliderPosition / this._computedSliderRatio);
}
@ -214,7 +214,7 @@ export class ScrollbarState {
return 0;
}
let correctedOffset = offset - this._arrowSize; // compensate if has arrows
const correctedOffset = offset - this._arrowSize; // compensate if has arrows
let desiredScrollPosition = this._scrollPosition;
if (correctedOffset < this._computedSliderPosition) {
desiredScrollPosition -= this._visibleSize; // page up/left
@ -233,7 +233,7 @@ export class ScrollbarState {
return 0;
}
let desiredSliderPosition = this._computedSliderPosition + delta;
const desiredSliderPosition = this._computedSliderPosition + delta;
return Math.round(desiredSliderPosition / this._computedSliderRatio);
}
}

View File

@ -43,7 +43,7 @@ export class ScrollbarVisibilityController extends Disposable {
}
public setShouldBeVisible(rawShouldBeVisible: boolean): void {
let shouldBeVisible = this.applyVisibilitySetting(rawShouldBeVisible);
const shouldBeVisible = this.applyVisibilitySetting(rawShouldBeVisible);
if (this._shouldBeVisible !== shouldBeVisible) {
this._shouldBeVisible = shouldBeVisible;
@ -105,4 +105,4 @@ export class ScrollbarVisibilityController extends Disposable {
this._domNode.setClassName(this._invisibleClassName + (withFadeAway ? ' fade' : ''));
}
}
}
}

View File

@ -38,8 +38,8 @@ export class VerticalScrollbar extends AbstractScrollbar {
});
if (options.verticalHasArrows) {
let arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
let scrollbarDelta = (options.verticalScrollbarSize - ARROW_IMG_SIZE) / 2;
const arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
const scrollbarDelta = (options.verticalScrollbarSize - ARROW_IMG_SIZE) / 2;
this._createArrow({
className: 'scra',

View File

@ -73,7 +73,7 @@ export class ToolBar extends Disposable {
actionViewItemProvider: this.options.actionViewItemProvider,
actionRunner: this.actionRunner,
keybindingProvider: this.options.getKeyBinding,
classNames: (options.moreIcon ?? toolBarMoreIcon).classNames,
classNames: CSSIcon.asClassNameArray(options.moreIcon ?? toolBarMoreIcon),
anchorAlignmentProvider: this.options.anchorAlignmentProvider,
menuAsChild: !!this.options.renderDropdownAsChildElement
}

View File

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { ComposedTreeDelegate, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree';
import { ObjectTree, IObjectTreeOptions, CompressibleObjectTree, ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider, ICompressibleObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree';
import { ObjectTree, IObjectTreeOptions, CompressibleObjectTree, ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider, ICompressibleObjectTreeOptions, IObjectTreeSetChildrenOptions } from 'vs/base/browser/ui/tree/objectTree';
import { IListVirtualDelegate, IIdentityProvider, IListDragAndDrop, IListDragOverReaction } from 'vs/base/browser/ui/list/list';
import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeSorter, ICollapseStateChangeEvent, IAsyncDataSource, ITreeDragAndDrop, TreeError, WeakMapper, ITreeFilter, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree';
import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
@ -279,6 +279,7 @@ function asObjectTreeOptions<TInput, T, TFilterData>(options?: IAsyncDataTreeOpt
}
export interface IAsyncDataTreeOptionsUpdate extends IAbstractTreeOptionsUpdate { }
export interface IAsyncDataTreeUpdateChildrenOptions<T> extends IObjectTreeSetChildrenOptions<T> { }
export interface IAsyncDataTreeOptions<T, TFilterData = void> extends IAsyncDataTreeOptionsUpdate, Pick<IAbstractTreeOptions<T, TFilterData>, Exclude<keyof IAbstractTreeOptions<T, TFilterData>, 'collapseByDefault'>> {
readonly collapseByDefault?: { (e: T): boolean; };
@ -499,11 +500,11 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
}
}
async updateChildren(element: TInput | T = this.root.element, recursive = true, rerender = false): Promise<void> {
await this._updateChildren(element, recursive, rerender);
async updateChildren(element: TInput | T = this.root.element, recursive = true, rerender = false, options?: IAsyncDataTreeUpdateChildrenOptions<T>): Promise<void> {
await this._updateChildren(element, recursive, rerender, undefined, options);
}
private async _updateChildren(element: TInput | T = this.root.element, recursive = true, rerender = false, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
private async _updateChildren(element: TInput | T = this.root.element, recursive = true, rerender = false, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>, options?: IAsyncDataTreeUpdateChildrenOptions<T>): Promise<void> {
if (typeof this.root.element === 'undefined') {
throw new TreeError(this.user, 'Tree input not set');
}
@ -514,7 +515,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
}
const node = this.getDataNode(element);
await this.refreshAndRenderNode(node, recursive, viewStateContext);
await this.refreshAndRenderNode(node, recursive, viewStateContext, options);
if (rerender) {
try {
@ -704,9 +705,9 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
return node;
}
private async refreshAndRenderNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
private async refreshAndRenderNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>, options?: IAsyncDataTreeUpdateChildrenOptions<T>): Promise<void> {
await this.refreshNode(node, recursive, viewStateContext);
this.render(node, viewStateContext);
this.render(node, viewStateContext, options);
}
private async refreshNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
@ -768,8 +769,8 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
const children = await childrenPromise;
return this.setChildren(node, children, recursive, viewStateContext);
} catch (err) {
if (node !== this.root) {
this.tree.collapse(node === this.root ? null : node);
if (node !== this.root && this.tree.hasElement(node)) {
this.tree.collapse(node);
}
if (isPromiseCanceledError(err)) {
@ -921,9 +922,18 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
return childrenToRefresh;
}
protected render(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): void {
protected render(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>, options?: IAsyncDataTreeUpdateChildrenOptions<T>): void {
const children = node.children.map(node => this.asTreeElement(node, viewStateContext));
this.tree.setChildren(node === this.root ? null : node, children);
const objectTreeOptions: IObjectTreeSetChildrenOptions<IAsyncDataTreeNode<TInput, T>> | undefined = options && {
...options,
diffIdentityProvider: options!.diffIdentityProvider && {
getId(node: IAsyncDataTreeNode<TInput, T>): { toString(): string; } {
return options!.diffIdentityProvider!.getId(node.element as T);
}
}
};
this.tree.setChildren(node === this.root ? null : node, children, objectTreeOptions);
if (node !== this.root) {
this.tree.setCollapsible(node, node.hasChildren);

View File

@ -6,8 +6,9 @@
import { Iterable } from 'vs/base/common/iterator';
import { Event } from 'vs/base/common/event';
import { ITreeModel, ITreeNode, ITreeElement, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError, TreeFilterResult, TreeVisibility, WeakMapper } from 'vs/base/browser/ui/tree/tree';
import { IObjectTreeModelOptions, ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel';
import { IList } from 'vs/base/browser/ui/tree/indexTreeModel';
import { IObjectTreeModelOptions, ObjectTreeModel, IObjectTreeModel, IObjectTreeModelSetChildrenOptions } from 'vs/base/browser/ui/tree/objectTreeModel';
import { IIndexTreeModelSpliceOptions, IList } from 'vs/base/browser/ui/tree/indexTreeModel';
import { IIdentityProvider } from 'vs/base/browser/ui/list/list';
// Exported only for test reasons, do not use directly
export interface ICompressedTreeElement<T> extends ITreeElement<T> {
@ -108,6 +109,12 @@ interface ICompressedObjectTreeModelOptions<T, TFilterData> extends IObjectTreeM
readonly compressionEnabled?: boolean;
}
const wrapIdentityProvider = <T>(base: IIdentityProvider<T>): IIdentityProvider<ICompressedTreeNode<T>> => ({
getId(node) {
return node.elements.map(e => base.getId(e).toString()).join('\0');
}
});
// Exported only for test reasons, do not use directly
export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> implements ITreeModel<ICompressedTreeNode<T> | null, TFilterData, T | null> {
@ -120,6 +127,7 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
private model: ObjectTreeModel<ICompressedTreeNode<T>, TFilterData>;
private nodes = new Map<T | null, ICompressedTreeNode<T>>();
private enabled: boolean;
private readonly identityProvider?: IIdentityProvider<ICompressedTreeNode<T>>;
get size(): number { return this.nodes.size; }
@ -130,15 +138,21 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
) {
this.model = new ObjectTreeModel(user, list, options);
this.enabled = typeof options.compressionEnabled === 'undefined' ? true : options.compressionEnabled;
this.identityProvider = options.identityProvider;
}
setChildren(
element: T | null,
children: Iterable<ICompressedTreeElement<T>> = Iterable.empty()
children: Iterable<ICompressedTreeElement<T>> = Iterable.empty(),
options: IObjectTreeModelSetChildrenOptions<T, TFilterData>,
): void {
// Diffs must be deem, since the compression can affect nested elements.
// @see https://github.com/microsoft/vscode/pull/114237#issuecomment-759425034
const diffIdentityProvider = options.diffIdentityProvider && wrapIdentityProvider(options.diffIdentityProvider);
if (element === null) {
const compressedChildren = Iterable.map(children, this.enabled ? compress : noCompress);
this._setChildren(null, compressedChildren);
this._setChildren(null, compressedChildren, { diffIdentityProvider, diffDepth: Infinity });
return;
}
@ -159,7 +173,10 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
const parentChildren = parent.children
.map(child => child === node ? recompressedElement : child);
this._setChildren(parent.element, parentChildren);
this._setChildren(parent.element, parentChildren, {
diffIdentityProvider,
diffDepth: node.depth - parent.depth,
});
}
isCompressionEnabled(): boolean {
@ -177,22 +194,29 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
const rootChildren = root.children as ITreeNode<ICompressedTreeNode<T>>[];
const decompressedRootChildren = Iterable.map(rootChildren, decompress);
const recompressedRootChildren = Iterable.map(decompressedRootChildren, enabled ? compress : noCompress);
this._setChildren(null, recompressedRootChildren);
// it should be safe to always use deep diff mode here if an identity
// provider is available, since we know the raw nodes are unchanged.
this._setChildren(null, recompressedRootChildren, {
diffIdentityProvider: this.identityProvider,
diffDepth: Infinity,
});
}
private _setChildren(
node: ICompressedTreeNode<T> | null,
children: Iterable<ITreeElement<ICompressedTreeNode<T>>>
children: Iterable<ITreeElement<ICompressedTreeNode<T>>>,
options: IIndexTreeModelSpliceOptions<ICompressedTreeNode<T>, TFilterData>,
): void {
const insertedElements = new Set<T | null>();
const _onDidCreateNode = (node: ITreeNode<ICompressedTreeNode<T>, TFilterData>) => {
const onDidCreateNode = (node: ITreeNode<ICompressedTreeNode<T>, TFilterData>) => {
for (const element of node.element.elements) {
insertedElements.add(element);
this.nodes.set(element, node.element);
}
};
const _onDidDeleteNode = (node: ITreeNode<ICompressedTreeNode<T>, TFilterData>) => {
const onDidDeleteNode = (node: ITreeNode<ICompressedTreeNode<T>, TFilterData>) => {
for (const element of node.element.elements) {
if (!insertedElements.has(element)) {
this.nodes.delete(element);
@ -200,7 +224,7 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
}
};
this.model.setChildren(node, children, _onDidCreateNode, _onDidDeleteNode);
this.model.setChildren(node, children, { ...options, onDidCreateNode, onDidDeleteNode });
}
has(element: T | null): boolean {
@ -363,16 +387,16 @@ function mapList<T, TFilterData>(nodeMapper: CompressedNodeWeakMapper<T, TFilter
function mapOptions<T, TFilterData>(compressedNodeUnwrapper: CompressedNodeUnwrapper<T>, options: ICompressibleObjectTreeModelOptions<T, TFilterData>): ICompressedObjectTreeModelOptions<T, TFilterData> {
return {
...options,
sorter: options.sorter && {
compare(node: ICompressedTreeNode<T>, otherNode: ICompressedTreeNode<T>): number {
return options.sorter!.compare(node.elements[0], otherNode.elements[0]);
}
},
identityProvider: options.identityProvider && {
getId(node: ICompressedTreeNode<T>): { toString(): string; } {
return options.identityProvider!.getId(compressedNodeUnwrapper(node));
}
},
sorter: options.sorter && {
compare(node: ICompressedTreeNode<T>, otherNode: ICompressedTreeNode<T>): number {
return options.sorter!.compare(node.elements[0], otherNode.elements[0]);
}
},
filter: options.filter && {
filter(node: ICompressedTreeNode<T>, parentVisibility: TreeVisibility): TreeFilterResult<TFilterData> {
return options.filter!.filter(compressedNodeUnwrapper(node), parentVisibility);
@ -424,8 +448,12 @@ export class CompressibleObjectTreeModel<T extends NonNullable<any>, TFilterData
this.model = new CompressedObjectTreeModel(user, mapList(this.nodeMapper, list), mapOptions(compressedNodeUnwrapper, options));
}
setChildren(element: T | null, children: Iterable<ICompressedTreeElement<T>> = Iterable.empty()): void {
this.model.setChildren(element, children);
setChildren(
element: T | null,
children: Iterable<ICompressedTreeElement<T>> = Iterable.empty(),
options: IObjectTreeModelSetChildrenOptions<T, TFilterData> = {},
): void {
this.model.setChildren(element, children, options);
}
isCompressionEnabled(): boolean {

View File

@ -47,13 +47,19 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
return this.input;
}
setInput(input: TInput, viewState?: IDataTreeViewState): void {
setInput(input: TInput | undefined, viewState?: IDataTreeViewState): void {
if (viewState && !this.identityProvider) {
throw new TreeError(this.user, 'Can\'t restore tree view state without an identity provider');
}
this.input = input;
if (!input) {
this.nodesByIdentity.clear();
this.model.setChildren(null, Iterable.empty());
return;
}
if (!viewState) {
this._refresh(input);
return;
@ -155,7 +161,7 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
};
}
this.model.setChildren((element === this.input ? null : element) as T, this.iterate(element, isCollapsed).elements, onDidCreateNode, onDidDeleteNode);
this.model.setChildren((element === this.input ? null : element) as T, this.iterate(element, isCollapsed).elements, { onDidCreateNode, onDidDeleteNode });
}
private iterate(element: TInput | T, isCollapsed?: (el: T) => boolean | undefined): { elements: Iterable<ITreeElement<T>>, size: number } {

View File

@ -3,8 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IIdentityProvider } from 'vs/base/browser/ui/list/list';
import { ICollapseStateChangeEvent, ITreeElement, ITreeFilter, ITreeFilterDataResult, ITreeModel, ITreeNode, TreeVisibility, ITreeModelSpliceEvent, TreeError } from 'vs/base/browser/ui/tree/tree';
import { tail2 } from 'vs/base/common/arrays';
import { LcsDiff } from 'vs/base/common/diff/diff';
import { Emitter, Event, EventBufferer } from 'vs/base/common/event';
import { Iterable } from 'vs/base/common/iterator';
import { ISpliceable } from 'vs/base/common/sequence';
@ -41,6 +43,34 @@ export interface IIndexTreeModelOptions<T, TFilterData> {
readonly autoExpandSingleChildren?: boolean;
}
export interface IIndexTreeModelSpliceOptions<T, TFilterData> {
/**
* If set, child updates will recurse the given number of levels even if
* items in the splice operation are unchanged. `Infinity` is a valid value.
*/
readonly diffDepth?: number;
/**
* Identity provider used to optimize splice() calls in the IndexTree. If
* this is not present, optimized splicing is not enabled.
*
* Warning: if this is present, calls to `setChildren()` will not replace
* or update nodes if their identity is the same, even if the elements are
* different. For this, you should call `rerender()`.
*/
readonly diffIdentityProvider?: IIdentityProvider<T>;
/**
* Callback for when a node is created.
*/
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void;
/**
* Callback for when a node is deleted.
*/
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
}
interface CollapsibleStateUpdate {
readonly collapsible: boolean;
}
@ -110,18 +140,95 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
location: number[],
deleteCount: number,
toInsert: Iterable<ITreeElement<T>> = Iterable.empty(),
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void,
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
options: IIndexTreeModelSpliceOptions<T, TFilterData> = {},
): void {
if (location.length === 0) {
throw new TreeError(this.user, 'Invalid tree location');
}
if (options.diffIdentityProvider) {
this.spliceSmart(options.diffIdentityProvider, location, deleteCount, toInsert, options);
} else {
this.spliceSimple(location, deleteCount, toInsert, options);
}
}
private spliceSmart(
identity: IIdentityProvider<T>,
location: number[],
deleteCount: number,
toInsertIterable: Iterable<ITreeElement<T>> = Iterable.empty(),
options: IIndexTreeModelSpliceOptions<T, TFilterData>,
recurseLevels = options.diffDepth ?? 0,
) {
const { parentNode } = this.getParentNodeWithListIndex(location);
const toInsert = [...toInsertIterable];
const index = location[location.length - 1];
const diff = new LcsDiff(
{ getElements: () => parentNode.children.map(e => identity.getId(e.element).toString()) },
{
getElements: () => [
...parentNode.children.slice(0, index),
...toInsert,
...parentNode.children.slice(index + deleteCount),
].map(e => identity.getId(e.element).toString())
},
).ComputeDiff(false);
// if we were given a 'best effort' diff, use default behavior
if (diff.quitEarly) {
return this.spliceSimple(location, deleteCount, toInsert, options);
}
const locationPrefix = location.slice(0, -1);
const recurseSplice = (fromOriginal: number, fromModified: number, count: number) => {
if (recurseLevels > 0) {
for (let i = 0; i < count; i++) {
fromOriginal--;
fromModified--;
this.spliceSmart(
identity,
[...locationPrefix, fromOriginal, 0],
Number.MAX_SAFE_INTEGER,
toInsert[fromModified].children,
options,
recurseLevels - 1,
);
}
}
};
let lastStartO = Math.min(parentNode.children.length, index + deleteCount);
let lastStartM = toInsert.length;
for (const change of diff.changes.sort((a, b) => b.originalStart - a.originalStart)) {
recurseSplice(lastStartO, lastStartM, lastStartO - (change.originalStart + change.originalLength));
lastStartO = change.originalStart;
lastStartM = change.modifiedStart - index;
this.spliceSimple(
[...locationPrefix, lastStartO],
change.originalLength,
Iterable.slice(toInsert, lastStartM, lastStartM + change.modifiedLength),
options,
);
}
// at this point, startO === startM === count since any remaining prefix should match
recurseSplice(lastStartO, lastStartM, lastStartO);
}
private spliceSimple(
location: number[],
deleteCount: number,
toInsert: Iterable<ITreeElement<T>> = Iterable.empty(),
{ onDidCreateNode, onDidDeleteNode }: IIndexTreeModelSpliceOptions<T, TFilterData>,
) {
const { parentNode, listIndex, revealed, visible } = this.getParentNodeWithListIndex(location);
const treeListElementsToInsert: ITreeNode<T, TFilterData>[] = [];
const nodesToInsertIterator = Iterable.map(toInsert, el => this.createTreeNode(el, parentNode, parentNode.visible ? TreeVisibility.Visible : TreeVisibility.Hidden, revealed, treeListElementsToInsert, onDidCreateNode));
const lastIndex = location[location.length - 1];
const lastHadChildren = parentNode.children.length > 0;
// figure out what's the visible child start index right before the
// splice point
@ -190,6 +297,11 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
deletedNodes.forEach(visit);
}
const currentlyHasChildren = parentNode.children.length > 0;
if (lastHadChildren !== currentlyHasChildren) {
this.setCollapsible(location.slice(0, -1), currentlyHasChildren);
}
this._onDidSplice.fire({ insertedNodes: nodesToInsert, deletedNodes });
let node: IIndexTreeNode<T, TFilterData> | undefined = parentNode;

View File

@ -7,7 +7,7 @@ import { Iterable } from 'vs/base/common/iterator';
import { AbstractTree, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree';
import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, ICollapseStateChangeEvent } from 'vs/base/browser/ui/tree/tree';
import { ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel';
import { IListVirtualDelegate, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list';
import { IListVirtualDelegate, IKeyboardNavigationLabelProvider, IIdentityProvider } from 'vs/base/browser/ui/list/list';
import { Event } from 'vs/base/common/event';
import { CompressibleObjectTreeModel, ElementMapper, ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
import { memoize } from 'vs/base/common/decorators';
@ -17,6 +17,25 @@ export interface IObjectTreeOptions<T, TFilterData = void> extends IAbstractTree
readonly sorter?: ITreeSorter<T>;
}
export interface IObjectTreeSetChildrenOptions<T> {
/**
* If set, child updates will recurse the given number of levels even if
* items in the splice operation are unchanged. `Infinity` is a valid value.
*/
readonly diffDepth?: number;
/**
* Identity provider used to optimize splice() calls in the IndexTree. If
* this is not present, optimized splicing is not enabled.
*
* Warning: if this is present, calls to `setChildren()` will not replace
* or update nodes if their identity is the same, even if the elements are
* different. For this, you should call `rerender()`.
*/
readonly diffIdentityProvider?: IIdentityProvider<T>;
}
export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends AbstractTree<T | null, TFilterData, T | null> {
protected model!: IObjectTreeModel<T, TFilterData>;
@ -33,8 +52,8 @@ export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends
super(user, container, delegate, renderers, options as IObjectTreeOptions<T | null, TFilterData>);
}
setChildren(element: T | null, children: Iterable<ITreeElement<T>> = Iterable.empty()): void {
this.model.setChildren(element, children);
setChildren(element: T | null, children: Iterable<ITreeElement<T>> = Iterable.empty(), options?: IObjectTreeSetChildrenOptions<T>): void {
this.model.setChildren(element, children, options);
}
rerender(element?: T): void {
@ -189,8 +208,8 @@ export class CompressibleObjectTree<T extends NonNullable<any>, TFilterData = vo
super(user, container, delegate, compressibleRenderers, asObjectTreeOptions<T, TFilterData>(compressedTreeNodeProvider, options));
}
setChildren(element: T | null, children: Iterable<ICompressedTreeElement<T>> = Iterable.empty()): void {
this.model.setChildren(element, children);
setChildren(element: T | null, children: Iterable<ICompressedTreeElement<T>> = Iterable.empty(), options?: IObjectTreeSetChildrenOptions<T>): void {
this.model.setChildren(element, children, options);
}
protected createModel(user: string, view: IList<ITreeNode<T, TFilterData>>, options: ICompressibleObjectTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {

View File

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Iterable } from 'vs/base/common/iterator';
import { IndexTreeModel, IIndexTreeModelOptions, IList } from 'vs/base/browser/ui/tree/indexTreeModel';
import { IndexTreeModel, IIndexTreeModelOptions, IList, IIndexTreeModelSpliceOptions } from 'vs/base/browser/ui/tree/indexTreeModel';
import { Event } from 'vs/base/common/event';
import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError } from 'vs/base/browser/ui/tree/tree';
import { IIdentityProvider } from 'vs/base/browser/ui/list/list';
@ -13,11 +13,14 @@ import { mergeSort } from 'vs/base/common/arrays';
export type ITreeNodeCallback<T, TFilterData> = (node: ITreeNode<T, TFilterData>) => void;
export interface IObjectTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> extends ITreeModel<T | null, TFilterData, T | null> {
setChildren(element: T | null, children: Iterable<ITreeElement<T>> | undefined): void;
setChildren(element: T | null, children: Iterable<ITreeElement<T>> | undefined, options?: IObjectTreeModelSetChildrenOptions<T, TFilterData>): void;
resort(element?: T | null, recursive?: boolean): void;
updateElementHeight(element: T, height: number): void;
}
export interface IObjectTreeModelSetChildrenOptions<T, TFilterData> extends IIndexTreeModelSpliceOptions<T, TFilterData> {
}
export interface IObjectTreeModelOptions<T, TFilterData> extends IIndexTreeModelOptions<T, TFilterData> {
readonly sorter?: ITreeSorter<T>;
readonly identityProvider?: IIdentityProvider<T>;
@ -63,23 +66,21 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
setChildren(
element: T | null,
children: Iterable<ITreeElement<T>> = Iterable.empty(),
onDidCreateNode?: ITreeNodeCallback<T, TFilterData>,
onDidDeleteNode?: ITreeNodeCallback<T, TFilterData>
options: IObjectTreeModelSetChildrenOptions<T, TFilterData> = {},
): void {
const location = this.getElementLocation(element);
this._setChildren(location, this.preserveCollapseState(children), onDidCreateNode, onDidDeleteNode);
this._setChildren(location, this.preserveCollapseState(children), options);
}
private _setChildren(
location: number[],
children: Iterable<ITreeElement<T>> = Iterable.empty(),
onDidCreateNode?: ITreeNodeCallback<T, TFilterData>,
onDidDeleteNode?: ITreeNodeCallback<T, TFilterData>
options: IObjectTreeModelSetChildrenOptions<T, TFilterData>,
): void {
const insertedElements = new Set<T | null>();
const insertedElementIds = new Set<string>();
const _onDidCreateNode = (node: ITreeNode<T | null, TFilterData>) => {
const onDidCreateNode = (node: ITreeNode<T | null, TFilterData>) => {
if (node.element === null) {
return;
}
@ -95,12 +96,10 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
this.nodesByIdentity.set(id, tnode);
}
if (onDidCreateNode) {
onDidCreateNode(tnode);
}
options.onDidCreateNode?.(tnode);
};
const _onDidDeleteNode = (node: ITreeNode<T | null, TFilterData>) => {
const onDidDeleteNode = (node: ITreeNode<T | null, TFilterData>) => {
if (node.element === null) {
return;
}
@ -118,17 +117,14 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
}
}
if (onDidDeleteNode) {
onDidDeleteNode(tnode);
}
options.onDidDeleteNode?.(tnode);
};
this.model.splice(
[...location, 0],
Number.MAX_VALUE,
children,
_onDidCreateNode,
_onDidDeleteNode
{ ...options, onDidCreateNode, onDidDeleteNode }
);
}
@ -182,7 +178,7 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
const location = this.getElementLocation(element);
const node = this.model.getNode(location);
this._setChildren(location, this.resortChildren(node, recursive));
this._setChildren(location, this.resortChildren(node, recursive), {});
}
private resortChildren(node: ITreeNode<T | null, TFilterData>, recursive: boolean, first = true): Iterable<ITreeElement<T>> {