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:
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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) {
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
Binary file not shown.
@ -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';
|
||||
|
||||
|
@ -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':
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -20,4 +20,5 @@ export interface IHoverDelegateOptions {
|
||||
|
||||
export interface IHoverDelegate {
|
||||
showHover(options: IHoverDelegateOptions): IDisposable | undefined;
|
||||
hideHover(): void;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
@ -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) {
|
@ -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%;
|
||||
|
@ -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 } {
|
||||
|
@ -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 */
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
@ -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 = /(&)?(&)([^\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) {
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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' : ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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 } {
|
||||
|
@ -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;
|
||||
|
@ -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> {
|
||||
|
@ -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>> {
|
||||
|
Reference in New Issue
Block a user