Merge from vscode 64980ea1f3f532c82bb6c28d27bba9ef2c5b4463 (#7206)

* Merge from vscode 64980ea1f3f532c82bb6c28d27bba9ef2c5b4463

* fix config changes

* fix strictnull checks
This commit is contained in:
Anthony Dresser
2019-09-15 22:38:26 -07:00
committed by GitHub
parent fa6c52699e
commit ea0f9e6ce9
1226 changed files with 21541 additions and 17633 deletions

View File

@@ -111,4 +111,4 @@ export interface IStaticDND {
export const StaticDND: IStaticDND = {
CurrentDragAndDropData: undefined
};
};

View File

@@ -1193,7 +1193,7 @@ export function asDomUri(uri: URI): URI {
return uri;
}
if (Schemas.vscodeRemote === uri.scheme) {
return RemoteAuthorities.rewrite(uri.authority, uri.path);
return RemoteAuthorities.rewrite(uri);
}
return uri;
}

View File

@@ -14,6 +14,7 @@ import { parse } from 'vs/base/common/marshalling';
import { cloneAndChange } from 'vs/base/common/objects';
import { escape } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
export interface MarkdownRenderOptions extends FormattedTextRenderOptions {
codeBlockRenderer?: (modeId: string, value: string) => Promise<string>;
@@ -186,9 +187,9 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
renderer
};
const allowedSchemes = ['http', 'https', 'mailto', 'data'];
const allowedSchemes = [Schemas.http, Schemas.https, Schemas.mailto, Schemas.data, Schemas.file, Schemas.vscodeRemote];
if (markdown.isTrusted) {
allowedSchemes.push('command');
allowedSchemes.push(Schemas.command);
}
const renderedMarkdown = marked.parse(markdown.value, markedOptions);

View File

@@ -55,13 +55,17 @@ export class Dialog extends Disposable {
private buttonGroup: ButtonGroup | undefined;
private styles: IDialogStyles | undefined;
private focusToReturn: HTMLElement | undefined;
private checkboxHasFocus: boolean = false;
private buttons: string[];
constructor(private container: HTMLElement, private message: string, private buttons: string[], private options: IDialogOptions) {
constructor(private container: HTMLElement, private message: string, buttons: string[], private options: IDialogOptions) {
super();
this.modal = this.container.appendChild($(`.dialog-modal-block${options.type === 'pending' ? '.dimmed' : ''}`));
this.element = this.modal.appendChild($('.dialog-box'));
hide(this.element);
// If no button is provided, default to OK
this.buttons = buttons.length ? buttons : [nls.localize('ok', "OK")];
const buttonsRowElement = this.element.appendChild($('.dialog-buttons-row'));
this.buttonsContainer = buttonsRowElement.appendChild($('.dialog-buttons'));
@@ -88,8 +92,6 @@ export class Dialog extends Disposable {
checkboxMessageElement.innerText = this.options.checkboxLabel;
}
const toolbarRowElement = this.element.appendChild($('.dialog-toolbar-row'));
this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar'));
}
@@ -139,14 +141,27 @@ export class Dialog extends Disposable {
let eventHandled = false;
if (evt.equals(KeyMod.Shift | KeyCode.Tab) || evt.equals(KeyCode.LeftArrow)) {
focusedButton = focusedButton + buttonGroup.buttons.length - 1;
focusedButton = focusedButton % buttonGroup.buttons.length;
buttonGroup.buttons[focusedButton].focus();
if (!this.checkboxHasFocus && focusedButton === 0) {
this.checkbox!.domNode.focus();
this.checkboxHasFocus = true;
} else {
focusedButton = (this.checkboxHasFocus ? 0 : focusedButton) + buttonGroup.buttons.length - 1;
focusedButton = focusedButton % buttonGroup.buttons.length;
buttonGroup.buttons[focusedButton].focus();
this.checkboxHasFocus = false;
}
eventHandled = true;
} else if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow)) {
focusedButton++;
focusedButton = focusedButton % buttonGroup.buttons.length;
buttonGroup.buttons[focusedButton].focus();
if (!this.checkboxHasFocus && focusedButton === buttonGroup.buttons.length - 1) {
this.checkbox!.domNode.focus();
this.checkboxHasFocus = true;
} else {
focusedButton = this.checkboxHasFocus ? 0 : focusedButton + 1;
focusedButton = focusedButton % buttonGroup.buttons.length;
buttonGroup.buttons[focusedButton].focus();
this.checkboxHasFocus = false;
}
eventHandled = true;
}

View File

@@ -596,8 +596,8 @@ export class SerializableGrid<T extends ISerializableView> extends Grid<T> {
super.layout(width, height);
if (this.initialLayoutContext) {
this.gridview.trySet2x2();
this.initialLayoutContext = false;
this.gridview.trySet2x2();
}
}
}

View File

@@ -250,6 +250,9 @@ class BranchNode implements ISplitView, IDisposable {
const onDidSashReset = Event.map(this.splitview.onDidSashReset, i => [i]);
this.splitviewSashResetDisposable = onDidSashReset(this._onDidSashReset.fire, this._onDidSashReset);
const onDidChildrenChange = Event.map(Event.any(...this.children.map(c => c.onDidChange)), () => undefined);
this.childrenChangeDisposable = onDidChildrenChange(this._onDidChange.fire, this._onDidChange);
const onDidChildrenSashReset = Event.any(...this.children.map((c, i) => Event.map(c.onDidSashReset, location => [i, ...location])));
this.childrenSashResetDisposable = onDidChildrenSashReset(this._onDidSashReset.fire, this._onDidSashReset);
}
@@ -422,7 +425,6 @@ class BranchNode implements ISplitView, IDisposable {
}
this.splitview.setViewVisible(index, visible);
this._onDidChange.fire(undefined);
}
getChildCachedVisibleSize(index: number): number | undefined {

View File

@@ -27,11 +27,11 @@ export interface IIconLabelValueOptions {
}
class FastLabelNode {
private disposed: boolean;
private _textContent: string;
private _className: string;
private _title: string;
private _empty: boolean;
private disposed: boolean | undefined;
private _textContent: string | undefined;
private _className: string | undefined;
private _title: string | undefined;
private _empty: boolean | undefined;
constructor(private _element: HTMLElement) {
}
@@ -89,7 +89,7 @@ export class IconLabel extends Disposable {
private domNode: FastLabelNode;
private labelDescriptionContainer: FastLabelNode;
private labelNode: FastLabelNode | HighlightedLabel;
private descriptionNode: FastLabelNode | HighlightedLabel;
private descriptionNode: FastLabelNode | HighlightedLabel | undefined;
private descriptionNodeFactory: () => FastLabelNode | HighlightedLabel;
constructor(container: HTMLElement, options?: IIconLabelCreationOptions) {

View File

@@ -64,6 +64,10 @@
outline: none;
}
.monaco-inputbox > .wrapper > textarea.input.empty {
white-space: nowrap;
}
.monaco-inputbox > .wrapper > textarea.input::-webkit-scrollbar {
display: none;
}

View File

@@ -171,7 +171,7 @@ export class InputBox extends Widget {
let tagName = this.options.flexibleHeight ? 'textarea' : 'input';
let wrapper = dom.append(this.element, $('.wrapper'));
this.input = dom.append(wrapper, $(tagName + '.input'));
this.input = dom.append(wrapper, $(tagName + '.input.empty'));
this.input.setAttribute('autocorrect', 'off');
this.input.setAttribute('autocapitalize', 'off');
this.input.setAttribute('spellcheck', 'false');
@@ -242,13 +242,7 @@ export class InputBox extends Widget {
});
}
setTimeout(() => {
if (!this.input) {
return;
}
this.updateMirror();
}, 0);
setTimeout(() => this.updateMirror(), 0);
// Support actions
if (this.options.actions) {
@@ -270,21 +264,18 @@ export class InputBox extends Widget {
}
public setPlaceHolder(placeHolder: string): void {
if (this.input) {
this.input.setAttribute('placeholder', placeHolder);
this.input.title = placeHolder;
}
this.placeholder = placeHolder;
this.input.setAttribute('placeholder', placeHolder);
this.input.title = placeHolder;
}
public setAriaLabel(label: string): void {
this.ariaLabel = label;
if (this.input) {
if (label) {
this.input.setAttribute('aria-label', this.ariaLabel);
} else {
this.input.removeAttribute('aria-label');
}
if (label) {
this.input.setAttribute('aria-label', this.ariaLabel);
} else {
this.input.removeAttribute('aria-label');
}
}
@@ -550,6 +541,7 @@ export class InputBox extends Widget {
this.validate();
this.updateMirror();
dom.toggleClass(this.input, 'empty', !this.value);
if (this.state === 'open' && this.contextViewProvider) {
this.contextViewProvider.layout();
@@ -561,7 +553,7 @@ export class InputBox extends Widget {
return;
}
const value = this.value || this.placeholder;
const value = this.value;
const lastCharCode = value.charCodeAt(value.length - 1);
const suffix = lastCharCode === 10 ? ' ' : '';
const mirrorTextContent = value + suffix;
@@ -594,20 +586,18 @@ export class InputBox extends Widget {
}
protected applyStyles(): void {
if (this.element) {
const background = this.inputBackground ? this.inputBackground.toString() : null;
const foreground = this.inputForeground ? this.inputForeground.toString() : null;
const border = this.inputBorder ? this.inputBorder.toString() : null;
const background = this.inputBackground ? this.inputBackground.toString() : null;
const foreground = this.inputForeground ? this.inputForeground.toString() : null;
const border = this.inputBorder ? this.inputBorder.toString() : null;
this.element.style.backgroundColor = background;
this.element.style.color = foreground;
this.input.style.backgroundColor = background;
this.input.style.color = foreground;
this.element.style.backgroundColor = background;
this.element.style.color = foreground;
this.input.style.backgroundColor = background;
this.input.style.color = foreground;
this.element.style.borderWidth = border ? '1px' : null;
this.element.style.borderStyle = border ? 'solid' : null;
this.element.style.borderColor = border;
}
this.element.style.borderWidth = border ? '1px' : null;
this.element.style.borderStyle = border ? 'solid' : null;
this.element.style.borderColor = border;
}
public layout(): void {
@@ -625,16 +615,27 @@ export class InputBox extends Widget {
}
}
public insertAtCursor(text: string): void {
const inputElement = this.inputElement;
const start = inputElement.selectionStart;
const end = inputElement.selectionEnd;
const content = inputElement.value;
if (start !== null && end !== null) {
this.value = content.substr(0, start) + text + content.substr(end);
inputElement.setSelectionRange(start + 1, start + 1);
this.layout();
}
}
public dispose(): void {
this._hideMessage();
this.element = null!; // StrictNullOverride: nulling out ok in dispose
this.input = null!; // StrictNullOverride: nulling out ok in dispose
this.contextViewProvider = undefined;
this.message = null;
this.validation = undefined;
this.state = null!; // StrictNullOverride: nulling out ok in dispose
this.actionbar = undefined;
if (this.actionbar) {
this.actionbar.dispose();
}
super.dispose();
}

View File

@@ -105,3 +105,10 @@ export interface IListDragAndDrop<T> {
onDragOver(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction;
drop(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void;
}
export class ListError extends Error {
constructor(user: string, message: string) {
super(`ListError [${user}] ${message}`);
}
}

View File

@@ -76,13 +76,14 @@ export class PagedList<T> implements IDisposable {
private _model!: IPagedModel<T>;
constructor(
user: string,
container: HTMLElement,
virtualDelegate: IListVirtualDelegate<number>,
renderers: IPagedRenderer<T, any>[],
options: IListOptions<any> = {}
) {
const pagedRenderers = renderers.map(r => new PagedRenderer<T, ITemplateData<T>>(r, () => this.model));
this.list = new List(container, virtualDelegate, pagedRenderers, options);
this.list = new List(user, container, virtualDelegate, pagedRenderers, options);
}
getHTMLElement(): HTMLElement {

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { getOrDefault } from 'vs/base/common/objects';
import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IDisposable, dispose, Disposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Gesture, EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch';
import * as DOM from 'vs/base/browser/dom';
import { Event, Emitter } from 'vs/base/common/event';
@@ -188,7 +188,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
private currentDragFeedbackDisposable: IDisposable = Disposable.None;
private onDragLeaveTimeout: IDisposable = Disposable.None;
private disposables: IDisposable[];
private readonly disposables: DisposableStore = new DisposableStore();
private _onDidChangeContentHeight = new Emitter<number>();
readonly onDidChangeContentHeight: Event<number> = Event.latch(this._onDidChangeContentHeight.event);
@@ -214,7 +214,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
this.renderers.set(renderer.templateId, renderer);
}
this.cache = new RowCache(this.renderers);
this.cache = this.disposables.add(new RowCache(this.renderers));
this.lastRenderTop = 0;
this.lastRenderHeight = 0;
@@ -238,18 +238,16 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
this.rowsContainer.className = 'monaco-list-rows';
Gesture.addTarget(this.rowsContainer);
this.scrollableElement = new ScrollableElement(this.rowsContainer, {
this.scrollableElement = this.disposables.add(new ScrollableElement(this.rowsContainer, {
alwaysConsumeMouseWheel: true,
horizontal: this.horizontalScrolling ? ScrollbarVisibility.Auto : ScrollbarVisibility.Hidden,
vertical: getOrDefault(options, o => o.verticalScrollMode, DefaultOptions.verticalScrollMode),
useShadows: getOrDefault(options, o => o.useShadows, DefaultOptions.useShadows)
});
}));
this.domNode.appendChild(this.scrollableElement.getDomNode());
container.appendChild(this.domNode);
this.disposables = [this.rangeMap, this.scrollableElement, this.cache];
this.scrollableElement.onScroll(this.onScroll, this, this.disposables);
domEvent(this.rowsContainer, TouchEventType.Change)(this.onTouchChange, this, this.disposables);
@@ -1178,6 +1176,6 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
this.domNode.parentNode.removeChild(this.domNode);
}
this.disposables = dispose(this.disposables);
dispose(this.disposables);
}
}

View File

@@ -16,7 +16,7 @@ import { KeyCode } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { Event, Emitter, EventBufferer } from 'vs/base/common/event';
import { domEvent } from 'vs/base/browser/event';
import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, ListAriaRootRole } from './list';
import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, ListAriaRootRole, ListError } from './list';
import { ListView, IListViewOptions, IListViewDragAndDrop, IAriaProvider } from './listView';
import { Color } from 'vs/base/common/color';
import { mixin } from 'vs/base/common/objects';
@@ -862,7 +862,7 @@ export interface IListStyles {
}
const defaultStyles: IListStyles = {
listFocusBackground: Color.fromHex('#073655'),
listFocusBackground: Color.fromHex('#7FB0D0'),
listActiveSelectionBackground: Color.fromHex('#0E639C'),
listActiveSelectionForeground: Color.fromHex('#FFFFFF'),
listFocusAndSelectionBackground: Color.fromHex('#094771'),
@@ -1170,6 +1170,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
readonly onDidDispose: Event<void> = this._onDidDispose.event;
constructor(
private user: string,
container: HTMLElement,
virtualDelegate: IListVirtualDelegate<T>,
renderers: IListRenderer<any /* TODO@joao */, any>[],
@@ -1261,11 +1262,11 @@ export class List<T> implements ISpliceable<T>, IDisposable {
splice(start: number, deleteCount: number, elements: T[] = []): void {
if (start < 0 || start > this.view.length) {
throw new Error(`Invalid start index: ${start}`);
throw new ListError(this.user, `Invalid start index: ${start}`);
}
if (deleteCount < 0) {
throw new Error(`Invalid delete count: ${deleteCount}`);
throw new ListError(this.user, `Invalid delete count: ${deleteCount}`);
}
if (deleteCount === 0 && elements.length === 0) {
@@ -1348,7 +1349,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
setSelection(indexes: number[], browserEvent?: UIEvent): void {
for (const index of indexes) {
if (index < 0 || index >= this.length) {
throw new Error(`Invalid index ${index}`);
throw new ListError(this.user, `Invalid index ${index}`);
}
}
@@ -1366,7 +1367,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
setFocus(indexes: number[], browserEvent?: UIEvent): void {
for (const index of indexes) {
if (index < 0 || index >= this.length) {
throw new Error(`Invalid index ${index}`);
throw new ListError(this.user, `Invalid index ${index}`);
}
}
@@ -1518,7 +1519,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
reveal(index: number, relativeTop?: number): void {
if (index < 0 || index >= this.length) {
throw new Error(`Invalid index ${index}`);
throw new ListError(this.user, `Invalid index ${index}`);
}
const scrollTop = this.view.getScrollTop();
@@ -1547,7 +1548,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
*/
getRelativeTop(index: number): number | null {
if (index < 0 || index >= this.length) {
throw new Error(`Invalid index ${index}`);
throw new ListError(this.user, `Invalid index ${index}`);
}
const scrollTop = this.view.getScrollTop();
@@ -1574,7 +1575,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
open(indexes: number[], browserEvent?: UIEvent): void {
for (const index of indexes) {
if (index < 0 || index >= this.length) {
throw new Error(`Invalid index ${index}`);
throw new ListError(this.user, `Invalid index ${index}`);
}
}
@@ -1584,7 +1585,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
pin(indexes: number[]): void {
for (const index of indexes) {
if (index < 0 || index >= this.length) {
throw new Error(`Invalid index ${index}`);
throw new ListError(this.user, `Invalid index ${index}`);
}
}

View File

@@ -186,8 +186,4 @@ export class RangeMap {
return -1;
}
dispose() {
this.groups = null!; // StrictNullOverride: nulling out ok in dispose
}
}

View File

@@ -79,11 +79,7 @@ export class RowCache<T> implements IDisposable {
return result;
}
private garbageCollect(): void {
if (!this.renderers) {
return;
}
dispose(): void {
this.cache.forEach((cachedRows, templateId) => {
for (const cachedRow of cachedRows) {
const renderer = this.getRenderer(templateId);
@@ -96,12 +92,6 @@ export class RowCache<T> implements IDisposable {
this.cache.clear();
}
dispose(): void {
this.garbageCollect();
this.cache.clear();
this.renderers = null!; // StrictNullOverride: nulling out ok in dispose
}
private getRenderer(templateId: string): IListRenderer<T, any> {
const renderer = this.renderers.get(templateId);
if (!renderer) {
@@ -109,4 +99,4 @@ export class RowCache<T> implements IDisposable {
}
return renderer;
}
}
}

View File

@@ -758,7 +758,7 @@ export class MenuBar extends Disposable {
if (menuBarMenu.titleElement.children.length) {
let child = menuBarMenu.titleElement.children.item(0) as HTMLElement;
if (child) {
child.style.textDecoration = (this.options.alwaysOnMnemonics || visible) ? 'underline' : null;
child.style.textDecoration = (this.options.alwaysOnMnemonics || visible) ? 'underline' : '';
}
}
});

View File

@@ -10,5 +10,5 @@
}
.octicon-animation-spin {
animation: octicon-spin 2s linear infinite;
animation: octicon-spin 1.5s linear infinite;
}

View File

@@ -10,10 +10,6 @@ body {
font-family: var(--version) !important;
}
body[data-octicons-update="enabled"] .monaco-workbench .part.statusbar > .items-container > .statusbar-item span.octicon {
font-size: 16px;
}
body[data-octicons-update="enabled"] .monaco-workbench .part.statusbar > .items-container > .statusbar-item > a {
display: flex;
align-items: center;

View File

@@ -1,7 +1,6 @@
@font-face {
font-family: "octicons2";
src: url("./octicons2.ttf?81972c7f658ef56eeefe672e514d35a3") format("truetype"),
url("./octicons2.svg?81972c7f658ef56eeefe672e514d35a3#octicons2") format("svg");
src: url("./octicons2.ttf?90586c9ac0aa804395e9c9c0d15d1094") format("truetype");
}
.octicon, .mega-octicon {
@@ -247,5 +246,6 @@ body[data-octicons-update="enabled"] .octicon-warning:before { content: "\f02d"
body[data-octicons-update="enabled"] .octicon-controls:before { content: "\26ad" }
body[data-octicons-update="enabled"] .octicon-event:before { content: "\26ae" }
body[data-octicons-update="enabled"] .octicon-record-keys:before { content: "\26af" }
body[data-octicons-update="enabled"] .octicon-archive:before { content: "\f101" }
body[data-octicons-update="enabled"] .octicon-arrow-both:before { content: "\f102" }
body[data-octicons-update="enabled"] .octicon-Vector:before { content: "\f101" }
body[data-octicons-update="enabled"] .octicon-archive:before { content: "\f102" }
body[data-octicons-update="enabled"] .octicon-arrow-both:before { content: "\f103" }

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 227 KiB

View File

@@ -36,8 +36,8 @@ const defaultOpts = {
export class ProgressBar extends Disposable {
private options: IProgressBarOptions;
private workedVal: number;
private element: HTMLElement;
private bit: HTMLElement;
private element!: HTMLElement;
private bit!: HTMLElement;
private totalWork: number | undefined;
private progressBarBackground: Color | undefined;
private showDelayedScheduler: RunOnceScheduler;
@@ -232,4 +232,4 @@ export class ProgressBar extends Disposable {
this.bit.style.backgroundColor = background;
}
}
}
}

View File

@@ -143,7 +143,7 @@ export class Sash extends Disposable {
this._register(domEvent(this.el, EventType.Start)(this.onTouchStart, this));
if (isIPad) {
// see also http://ux.stackexchange.com/questions/39023/what-is-the-optimum-button-size-of-touch-screen-applications
// see also https://ux.stackexchange.com/questions/39023/what-is-the-optimum-button-size-of-touch-screen-applications
addClass(this.el, 'touch');
}
@@ -179,27 +179,8 @@ export class Sash extends Disposable {
let isMultisashResize = false;
if (this.linkedSash && !(e as any).__linkedSashEvent) {
(e as any).__linkedSashEvent = true;
this.linkedSash.onMouseDown(e);
}
if (!(e as any).__orthogonalSashEvent) {
let orthogonalSash: Sash | undefined;
if (this.orientation === Orientation.VERTICAL) {
if (e.offsetY <= 4) {
orthogonalSash = this.orthogonalStartSash;
} else if (e.offsetY >= this.el.clientHeight - 4) {
orthogonalSash = this.orthogonalEndSash;
}
} else {
if (e.offsetX <= 4) {
orthogonalSash = this.orthogonalStartSash;
} else if (e.offsetX >= this.el.clientWidth - 4) {
orthogonalSash = this.orthogonalEndSash;
}
}
const orthogonalSash = this.getOrthogonalSash(e);
if (orthogonalSash) {
isMultisashResize = true;
@@ -208,6 +189,11 @@ export class Sash extends Disposable {
}
}
if (this.linkedSash && !(e as any).__linkedSashEvent) {
(e as any).__linkedSashEvent = true;
this.linkedSash.onMouseDown(e);
}
if (!this.state) {
return;
}
@@ -295,7 +281,17 @@ export class Sash extends Disposable {
domEvent(window, 'mouseup')(onMouseUp, null, disposables);
}
private onMouseDoubleClick(event: MouseEvent): void {
private onMouseDoubleClick(e: MouseEvent): void {
const orthogonalSash = this.getOrthogonalSash(e);
if (orthogonalSash) {
orthogonalSash._onDidReset.fire();
}
if (this.linkedSash) {
this.linkedSash._onDidReset.fire();
}
this._onDidReset.fire();
}
@@ -386,13 +382,26 @@ export class Sash extends Disposable {
toggleClass(this.el, 'orthogonal-end', state !== SashState.Disabled);
}
dispose(): void {
super.dispose();
if (this.el && this.el.parentElement) {
this.el.parentElement.removeChild(this.el);
private getOrthogonalSash(e: MouseEvent): Sash | undefined {
if (this.orientation === Orientation.VERTICAL) {
if (e.offsetY <= 4) {
return this.orthogonalStartSash;
} else if (e.offsetY >= this.el.clientHeight - 4) {
return this.orthogonalEndSash;
}
} else {
if (e.offsetX <= 4) {
return this.orthogonalStartSash;
} else if (e.offsetX >= this.el.clientWidth - 4) {
return this.orthogonalEndSash;
}
}
this.el = null!; // StrictNullOverride: nulling out ok in dispose
return undefined;
}
dispose(): void {
super.dispose();
this.el.remove();
}
}

View File

@@ -737,7 +737,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
this.listRenderer = new SelectListRenderer();
this.selectList = new List(this.selectDropDownListContainer, this, [this.listRenderer], {
this.selectList = new List('SelectBoxCustom', this.selectDropDownListContainer, this, [this.listRenderer], {
ariaLabel: this.selectBoxOptions.ariaLabel,
useShadows: false,
verticalScrollMode: ScrollbarVisibility.Visible,

View File

@@ -21,24 +21,15 @@
}
.monaco-split-view2 > .split-view-container {
display: flex;
width: 100%;
height: 100%;
white-space: nowrap;
}
.monaco-split-view2.vertical > .split-view-container {
flex-direction: column;
}
.monaco-split-view2.horizontal > .split-view-container {
flex-direction: row;
position: relative;
}
.monaco-split-view2 > .split-view-container > .split-view-view {
white-space: initial;
flex: none;
position: relative;
position: absolute;
}
.monaco-split-view2 > .split-view-container > .split-view-view:not(.visible) {
@@ -51,7 +42,6 @@
.monaco-split-view2.horizontal > .split-view-container > .split-view-view {
height: 100%;
display: inline-block;
}
.monaco-split-view2.separator-border > .split-view-container > .split-view-view:not(:first-child)::before {

View File

@@ -125,15 +125,13 @@ abstract class ViewItem {
}
}
layout(_orthogonalSize: number | undefined): void {
this.container.scrollTop = 0;
this.container.scrollLeft = 0;
}
layoutView(orthogonalSize: number | undefined): void {
layout(position: number, orthogonalSize: number | undefined): void {
this.layoutContainer(position);
this.view.layout(this.size, orthogonalSize);
}
abstract layoutContainer(position: number): void;
dispose(): IView {
this.disposable.dispose();
return this.view;
@@ -142,19 +140,17 @@ abstract class ViewItem {
class VerticalViewItem extends ViewItem {
layout(orthogonalSize: number | undefined): void {
super.layout(orthogonalSize);
layoutContainer(position: number): void {
this.container.style.top = `${position}px`;
this.container.style.height = `${this.size}px`;
this.layoutView(orthogonalSize);
}
}
class HorizontalViewItem extends ViewItem {
layout(orthogonalSize: number | undefined): void {
super.layout(orthogonalSize);
layoutContainer(position: number): void {
this.container.style.left = `${position}px`;
this.container.style.width = `${this.size}px`;
this.layoutView(orthogonalSize);
}
}
@@ -853,7 +849,12 @@ export class SplitView extends Disposable {
this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
// Layout views
this.viewItems.forEach(item => item.layout(this.orthogonalSize));
let position = 0;
for (const viewItem of this.viewItems) {
viewItem.layout(position, this.orthogonalSize);
position += viewItem.size;
}
// Layout sashes
this.sashItems.forEach(item => item.sash.layout());
@@ -910,7 +911,7 @@ export class SplitView extends Disposable {
position += this.viewItems[i].size;
if (this.sashItems[i].sash === sash) {
return position;
return Math.min(position, this.contentSize - 2);
}
}

View File

@@ -98,9 +98,11 @@ class TreeNodeListDragAndDrop<T, TFilterData, TRef> implements IListDragAndDrop<
}
if (result.bubble === TreeDragOverBubble.Up) {
const parentNode = targetNode.parent;
const model = this.modelProvider();
const parentIndex = parentNode && model.getListIndex(model.getNodeLocation(parentNode));
const ref = model.getNodeLocation(targetNode);
const parentRef = model.getParentNodeLocation(ref);
const parentNode = model.getNode(parentRef);
const parentIndex = parentRef && model.getListIndex(parentRef);
return this.onDragOver(data, parentNode, parentIndex, originalEvent, false);
}
@@ -155,7 +157,12 @@ function asListOptions<T, TFilterData, TRef>(modelProvider: () => ITreeModel<T,
enableKeyboardNavigation: options.simpleKeyboardNavigation,
ariaProvider: {
getSetSize(node) {
return node.parent!.visibleChildrenCount;
const model = modelProvider();
const ref = model.getNodeLocation(node);
const parentRef = model.getParentNodeLocation(ref);
const parentNode = model.getNode(parentRef);
return parentNode.visibleChildrenCount;
},
getPosInSet(node) {
return node.visibleChildIndex + 1;
@@ -233,7 +240,7 @@ class EventCollection<T> implements Collection<T> {
}
}
class TreeRenderer<T, TFilterData, TTemplateData> implements IListRenderer<ITreeNode<T, TFilterData>, ITreeListTemplateData<TTemplateData>> {
class TreeRenderer<T, TFilterData, TRef, TTemplateData> implements IListRenderer<ITreeNode<T, TFilterData>, ITreeListTemplateData<TTemplateData>> {
private static DefaultIndent = 8;
@@ -251,6 +258,7 @@ class TreeRenderer<T, TFilterData, TTemplateData> implements IListRenderer<ITree
constructor(
private renderer: ITreeRenderer<T, TFilterData, TTemplateData>,
private modelProvider: () => ITreeModel<T, TFilterData, TRef>,
onDidChangeCollapseState: Event<ICollapseStateChangeEvent<T, TFilterData>>,
private activeNodes: Collection<ITreeNode<T, TFilterData>>,
options: ITreeRendererOptions = {}
@@ -381,10 +389,19 @@ class TreeRenderer<T, TFilterData, TTemplateData> implements IListRenderer<ITree
}
const disposableStore = new DisposableStore();
const model = this.modelProvider();
let node = target;
while (node.parent && node.parent.parent) {
const parent = node.parent;
while (true) {
const ref = model.getNodeLocation(node);
const parentRef = model.getParentNodeLocation(ref);
if (!parentRef) {
break;
}
const parent = model.getNode(parentRef);
const guide = $<HTMLDivElement>('.indent-guide', { style: `width: ${this.indent}px` });
if (this.activeIndentNodes.has(parent)) {
@@ -412,12 +429,16 @@ class TreeRenderer<T, TFilterData, TTemplateData> implements IListRenderer<ITree
}
const set = new Set<ITreeNode<T, TFilterData>>();
const model = this.modelProvider();
nodes.forEach(node => {
const ref = model.getNodeLocation(node);
const parentRef = model.getParentNodeLocation(ref);
if (node.collapsible && node.children.length > 0 && !node.collapsed) {
set.add(node);
} else if (node.parent) {
set.add(node.parent);
} else if (parentRef) {
set.add(model.getNode(parentRef));
}
});
@@ -1053,13 +1074,15 @@ class TreeNodeListMouseController<T, TFilterData, TRef> extends MouseController<
return super.onPointer(e);
}
const model = ((this.tree as any).model as ITreeModel<T, TFilterData, TRef>); // internal
const location = model.getNodeLocation(node);
const recursive = e.browserEvent.altKey;
model.setCollapsed(location, undefined, recursive);
if (node.collapsible) {
const model = ((this.tree as any).model as ITreeModel<T, TFilterData, TRef>); // internal
const location = model.getNodeLocation(node);
const recursive = e.browserEvent.altKey;
model.setCollapsed(location, undefined, recursive);
if (expandOnlyOnTwistieClick && onTwistie) {
return;
if (expandOnlyOnTwistieClick && onTwistie) {
return;
}
}
super.onPointer(e);
@@ -1087,6 +1110,7 @@ interface ITreeNodeListOptions<T, TFilterData, TRef> extends IListOptions<ITreeN
class TreeNodeList<T, TFilterData, TRef> extends List<ITreeNode<T, TFilterData>> {
constructor(
user: string,
container: HTMLElement,
virtualDelegate: IListVirtualDelegate<ITreeNode<T, TFilterData>>,
renderers: IListRenderer<any /* TODO@joao */, any>[],
@@ -1094,7 +1118,7 @@ class TreeNodeList<T, TFilterData, TRef> extends List<ITreeNode<T, TFilterData>>
private selectionTrait: Trait<T>,
options: ITreeNodeListOptions<T, TFilterData, TRef>
) {
super(container, virtualDelegate, renderers, options);
super(user, container, virtualDelegate, renderers, options);
}
protected createMouseController(options: ITreeNodeListOptions<T, TFilterData, TRef>): MouseController<ITreeNode<T, TFilterData>> {
@@ -1150,7 +1174,7 @@ class TreeNodeList<T, TFilterData, TRef> extends List<ITreeNode<T, TFilterData>>
export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable {
protected view: TreeNodeList<T, TFilterData, TRef>;
private renderers: TreeRenderer<T, TFilterData, any>[];
private renderers: TreeRenderer<T, TFilterData, TRef, any>[];
protected model: ITreeModel<T, TFilterData, TRef>;
private focus: Trait<T>;
private selection: Trait<T>;
@@ -1195,6 +1219,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
get onDidDispose(): Event<void> { return this.view.onDidDispose; }
constructor(
user: string,
container: HTMLElement,
delegate: IListVirtualDelegate<T>,
renderers: ITreeRenderer<T, TFilterData, any>[],
@@ -1207,7 +1232,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
const activeNodes = new EventCollection(onDidChangeActiveNodes.event);
this.disposables.push(activeNodes);
this.renderers = renderers.map(r => new TreeRenderer<T, TFilterData, any>(r, onDidChangeCollapseStateRelay.event, activeNodes, _options));
this.renderers = renderers.map(r => new TreeRenderer<T, TFilterData, TRef, any>(r, () => this.model, onDidChangeCollapseStateRelay.event, activeNodes, _options));
this.disposables.push(...this.renderers);
let filter: TypeFilter<T> | undefined;
@@ -1220,9 +1245,9 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
this.focus = new Trait(_options.identityProvider);
this.selection = new Trait(_options.identityProvider);
this.view = new TreeNodeList(container, treeDelegate, this.renderers, this.focus, this.selection, { ...asListOptions(() => this.model, _options), tree: this });
this.view = new TreeNodeList(user, container, treeDelegate, this.renderers, this.focus, this.selection, { ...asListOptions(() => this.model, _options), tree: this });
this.model = this.createModel(this.view, _options);
this.model = this.createModel(user, this.view, _options);
onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState;
this.model.onDidSplice(e => {
@@ -1379,7 +1404,9 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
// Tree navigation
getParentElement(location: TRef): T {
return this.model.getParentElement(location);
const parentRef = this.model.getParentNodeLocation(location);
const parentNode = this.model.getNode(parentRef);
return parentNode.element;
}
getFirstElementChild(location: TRef): T | undefined {
@@ -1416,6 +1443,10 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
return this.model.isCollapsible(location);
}
setCollapsible(location: TRef, collapsible?: boolean): boolean {
return this.model.setCollapsible(location, collapsible);
}
isCollapsed(location: TRef): boolean {
return this.model.isCollapsed(location);
}
@@ -1531,7 +1562,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
if (!didChange) {
const parentLocation = this.model.getParentNodeLocation(location);
if (parentLocation === null) {
if (!parentLocation) {
return;
}
@@ -1586,7 +1617,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
this.model.setCollapsed(location, undefined, recursive);
}
protected abstract createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IAbstractTreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, TRef>;
protected abstract createModel(user: string, view: ISpliceable<ITreeNode<T, TFilterData>>, options: IAbstractTreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, TRef>;
navigate(start?: TRef): ITreeNavigator<T> {
return new TreeNavigator(this.view, this.model, start);
@@ -1633,22 +1664,6 @@ class TreeNavigator<T extends NonNullable<any>, TFilterData, TRef> implements IT
return this.current();
}
parent(): T | null {
if (this.index < 0 || this.index >= this.view.length) {
return null;
}
const node = this.view.element(this.index);
if (!node.parent) {
this.index = -1;
return this.current();
}
this.index = this.model.getListIndex(this.model.getNodeLocation(node.parent));
return this.current();
}
first(): T | null {
this.index = 0;
return this.current();

View File

@@ -4,9 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import { ComposedTreeDelegate, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree';
import { ObjectTree, IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree';
import { ObjectTree, IObjectTreeOptions, CompressibleObjectTree, ICompressibleTreeRenderer } 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 } from 'vs/base/browser/ui/tree/tree';
import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeSorter, ICollapseStateChangeEvent, IAsyncDataSource, ITreeDragAndDrop, TreeError, WeakMapper } from 'vs/base/browser/ui/tree/tree';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { timeout, CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
@@ -18,13 +18,14 @@ import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors
import { toggleClass } from 'vs/base/browser/dom';
import { values } from 'vs/base/common/map';
import { ScrollEvent } from 'vs/base/common/scrollable';
import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
interface IAsyncDataTreeNode<TInput, T> {
element: TInput | T;
readonly parent: IAsyncDataTreeNode<TInput, T> | null;
readonly children: IAsyncDataTreeNode<TInput, T>[];
readonly id?: string | null;
loading: boolean;
refreshPromise: Promise<void> | undefined;
hasChildren: boolean;
stale: boolean;
slow: boolean;
@@ -41,7 +42,7 @@ function createAsyncDataTreeNode<TInput, T>(props: IAsyncDataTreeNodeRequiredPro
return {
...props,
children: [],
loading: false,
refreshPromise: undefined,
stale: true,
slow: false,
collapsedByDefault: undefined
@@ -66,10 +67,11 @@ interface IDataTreeListTemplateData<T> {
templateData: T;
}
type AsyncDataTreeNodeMapper<TInput, T, TFilterData> = WeakMapper<ITreeNode<IAsyncDataTreeNode<TInput, T> | null, TFilterData>, ITreeNode<TInput | T, TFilterData>>;
class AsyncDataTreeNodeWrapper<TInput, T, TFilterData> implements ITreeNode<TInput | T, TFilterData> {
get element(): T { return this.node.element!.element as T; }
get parent(): ITreeNode<T, TFilterData> | undefined { return this.node.parent && new AsyncDataTreeNodeWrapper(this.node.parent); }
get children(): ITreeNode<T, TFilterData>[] { return this.node.children.map(node => new AsyncDataTreeNodeWrapper(node)); }
get depth(): number { return this.node.depth; }
get visibleChildrenCount(): number { return this.node.visibleChildrenCount; }
@@ -82,14 +84,15 @@ class AsyncDataTreeNodeWrapper<TInput, T, TFilterData> implements ITreeNode<TInp
constructor(private node: ITreeNode<IAsyncDataTreeNode<TInput, T> | null, TFilterData>) { }
}
class DataTreeRenderer<TInput, T, TFilterData, TTemplateData> implements ITreeRenderer<IAsyncDataTreeNode<TInput, T>, TFilterData, IDataTreeListTemplateData<TTemplateData>> {
class AsyncDataTreeRenderer<TInput, T, TFilterData, TTemplateData> implements ITreeRenderer<IAsyncDataTreeNode<TInput, T>, TFilterData, IDataTreeListTemplateData<TTemplateData>> {
readonly templateId: string;
private renderedNodes = new Map<IAsyncDataTreeNode<TInput, T>, IDataTreeListTemplateData<TTemplateData>>();
private disposables: IDisposable[] = [];
constructor(
private renderer: ITreeRenderer<T, TFilterData, TTemplateData>,
protected renderer: ITreeRenderer<T, TFilterData, TTemplateData>,
protected nodeMapper: AsyncDataTreeNodeMapper<TInput, T, TFilterData>,
readonly onDidChangeTwistieState: Event<IAsyncDataTreeNode<TInput, T>>
) {
this.templateId = renderer.templateId;
@@ -101,7 +104,7 @@ class DataTreeRenderer<TInput, T, TFilterData, TTemplateData> implements ITreeRe
}
renderElement(node: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>, height: number | undefined): void {
this.renderer.renderElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData, height);
this.renderer.renderElement(this.nodeMapper.map(node) as ITreeNode<T, TFilterData>, index, templateData.templateData, height);
}
renderTwistie(element: IAsyncDataTreeNode<TInput, T>, twistieElement: HTMLElement): boolean {
@@ -111,7 +114,7 @@ class DataTreeRenderer<TInput, T, TFilterData, TTemplateData> implements ITreeRe
disposeElement(node: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>, height: number | undefined): void {
if (this.renderer.disposeElement) {
this.renderer.disposeElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData, height);
this.renderer.disposeElement(this.nodeMapper.map(node) as ITreeNode<T, TFilterData>, index, templateData.templateData, height);
}
}
@@ -243,25 +246,6 @@ function asObjectTreeOptions<TInput, T, TFilterData>(options?: IAsyncDataTreeOpt
};
}
function asTreeElement<TInput, T>(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): ITreeElement<IAsyncDataTreeNode<TInput, T>> {
let collapsed: boolean | undefined;
if (viewStateContext && viewStateContext.viewState.expanded && node.id && viewStateContext.viewState.expanded.indexOf(node.id) > -1) {
collapsed = false;
} else {
collapsed = node.collapsedByDefault;
}
node.collapsedByDefault = undefined;
return {
element: node,
children: node.hasChildren ? Iterator.map(Iterator.fromArray(node.children), child => asTreeElement(child, viewStateContext)) : [],
collapsible: node.hasChildren,
collapsed
};
}
export interface IAsyncDataTreeOptionsUpdate extends IAbstractTreeOptionsUpdate { }
export interface IAsyncDataTreeOptions<T, TFilterData = void> extends IAsyncDataTreeOptionsUpdate, Pick<IAbstractTreeOptions<T, TFilterData>, Exclude<keyof IAbstractTreeOptions<T, TFilterData>, 'collapseByDefault'>> {
@@ -304,7 +288,9 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
private readonly autoExpandSingleChildren: boolean;
private readonly _onDidRender = new Emitter<void>();
private readonly _onDidChangeNodeSlowState = new Emitter<IAsyncDataTreeNode<TInput, T>>();
protected readonly _onDidChangeNodeSlowState = new Emitter<IAsyncDataTreeNode<TInput, T>>();
protected readonly nodeMapper: AsyncDataTreeNodeMapper<TInput, T, TFilterData> = new WeakMapper(node => new AsyncDataTreeNodeWrapper(node));
protected readonly disposables: IDisposable[] = [];
@@ -339,6 +325,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
get onDidDispose(): Event<void> { return this.tree.onDidDispose; }
constructor(
private user: string,
container: HTMLElement,
delegate: IListVirtualDelegate<T>,
renderers: ITreeRenderer<T, TFilterData, any>[],
@@ -350,11 +337,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
this.sorter = options.sorter;
this.collapseByDefault = options.collapseByDefault;
const objectTreeDelegate = new ComposedTreeDelegate<TInput | T, IAsyncDataTreeNode<TInput, T>>(delegate);
const objectTreeRenderers = renderers.map(r => new DataTreeRenderer(r, this._onDidChangeNodeSlowState.event));
const objectTreeOptions = asObjectTreeOptions<TInput, T, TFilterData>(options) || {};
this.tree = new ObjectTree(container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions);
this.tree = this.createTree(user, container, delegate, renderers, options);
this.root = createAsyncDataTreeNode({
element: undefined!,
@@ -374,6 +357,20 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
this.tree.onDidChangeCollapseState(this._onDidChangeCollapseState, this, this.disposables);
}
protected createTree(
user: string,
container: HTMLElement,
delegate: IListVirtualDelegate<T>,
renderers: ITreeRenderer<T, TFilterData, any>[],
options: IAsyncDataTreeOptions<T, TFilterData>
): ObjectTree<IAsyncDataTreeNode<TInput, T>, TFilterData> {
const objectTreeDelegate = new ComposedTreeDelegate<TInput | T, IAsyncDataTreeNode<TInput, T>>(delegate);
const objectTreeRenderers = renderers.map(r => new AsyncDataTreeRenderer(r, this.nodeMapper, this._onDidChangeNodeSlowState.event));
const objectTreeOptions = asObjectTreeOptions<TInput, T, TFilterData>(options) || {};
return new ObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions);
}
updateOptions(options: IAsyncDataTreeOptionsUpdate = {}): void {
this.tree.updateOptions(options);
}
@@ -450,7 +447,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
const viewStateContext = viewState && { viewState, focus: [], selection: [] } as IAsyncDataTreeViewStateContext<TInput, T>;
await this.updateChildren(input, true, viewStateContext);
await this._updateChildren(input, true, viewStateContext);
if (viewStateContext) {
this.tree.setFocus(viewStateContext.focus);
@@ -462,13 +459,17 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
}
}
async updateChildren(element: TInput | T = this.root.element, recursive = true, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
async updateChildren(element: TInput | T = this.root.element, recursive = true): Promise<void> {
await this._updateChildren(element, recursive);
}
private async _updateChildren(element: TInput | T = this.root.element, recursive = true, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
if (typeof this.root.element === 'undefined') {
throw new Error('Tree input not set');
throw new TreeError(this.user, 'Tree input not set');
}
if (this.root.loading) {
await this.subTreeRefreshPromises.get(this.root)!;
if (this.root.refreshPromise) {
await this.root.refreshPromise;
await Event.toPromise(this._onDidRender.event);
}
@@ -505,7 +506,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
getNode(element: TInput | T = this.root.element): ITreeNode<TInput | T, TFilterData> {
const dataNode = this.getDataNode(element);
const node = this.tree.getNode(dataNode === this.root ? null : dataNode);
return new AsyncDataTreeNodeWrapper<TInput, T, TFilterData>(node);
return this.nodeMapper.map(node);
}
collapse(element: T, recursive: boolean = false): boolean {
@@ -515,24 +516,29 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
async expand(element: T, recursive: boolean = false): Promise<boolean> {
if (typeof this.root.element === 'undefined') {
throw new Error('Tree input not set');
throw new TreeError(this.user, 'Tree input not set');
}
if (this.root.loading) {
await this.subTreeRefreshPromises.get(this.root)!;
if (this.root.refreshPromise) {
await this.root.refreshPromise;
await Event.toPromise(this._onDidRender.event);
}
const node = this.getDataNode(element);
if (node !== this.root && !node.loading && !this.tree.isCollapsed(node)) {
if (node.refreshPromise) {
await this.root.refreshPromise;
await Event.toPromise(this._onDidRender.event);
}
if (node !== this.root && !node.refreshPromise && !this.tree.isCollapsed(node)) {
return false;
}
const result = this.tree.expand(node === this.root ? null : node, recursive);
if (node.loading) {
await this.subTreeRefreshPromises.get(node)!;
if (node.refreshPromise) {
await this.root.refreshPromise;
await Event.toPromise(this._onDidRender.event);
}
@@ -643,7 +649,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
const node: IAsyncDataTreeNode<TInput, T> | undefined = this.nodes.get((element === this.root.element ? null : element) as T);
if (!node) {
throw new Error(`Data tree node not found: ${element}`);
throw new TreeError(this.user, `Data tree node not found: ${element}`);
}
return node;
@@ -676,18 +682,18 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
return result;
}
result = this.doRefreshSubTree(node, recursive, viewStateContext);
this.subTreeRefreshPromises.set(node, result);
try {
await result;
} finally {
this.subTreeRefreshPromises.delete(node);
}
return this.doRefreshSubTree(node, recursive, viewStateContext);
}
private async doRefreshSubTree(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
node.loading = true;
let done: () => void;
node.refreshPromise = new Promise(c => done = c);
this.subTreeRefreshPromises.set(node, node.refreshPromise);
node.refreshPromise.finally(() => {
node.refreshPromise = undefined;
this.subTreeRefreshPromises.delete(node);
});
try {
const childrenToRefresh = await this.doRefreshNode(node, recursive, viewStateContext);
@@ -695,7 +701,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
await Promise.all(childrenToRefresh.map(child => this.doRefreshSubTree(child, recursive, viewStateContext)));
} finally {
node.loading = false;
done!();
}
}
@@ -866,16 +872,40 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
}
private render(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): void {
const children = node.children.map(c => asTreeElement(c, viewStateContext));
const children = node.children.map(node => this.asTreeElement(node, viewStateContext));
this.tree.setChildren(node === this.root ? null : node, children);
if (node !== this.root) {
this.tree.setCollapsible(node, node.hasChildren);
}
this._onDidRender.fire();
}
protected asTreeElement(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): ITreeElement<IAsyncDataTreeNode<TInput, T>> {
let collapsed: boolean | undefined;
if (viewStateContext && viewStateContext.viewState.expanded && node.id && viewStateContext.viewState.expanded.indexOf(node.id) > -1) {
collapsed = false;
} else {
collapsed = node.collapsedByDefault;
}
node.collapsedByDefault = undefined;
return {
element: node,
children: node.hasChildren ? Iterator.map(Iterator.fromArray(node.children), child => this.asTreeElement(child, viewStateContext)) : [],
collapsible: node.hasChildren,
collapsed
};
}
// view state
getViewState(): IAsyncDataTreeViewState {
if (!this.identityProvider) {
throw new Error('Can\'t get tree view state without an identity provider');
throw new TreeError(this.user, 'Can\'t get tree view state without an identity provider');
}
const getId = (element: T) => this.identityProvider!.getId(element).toString();
@@ -903,3 +933,117 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
dispose(this.disposables);
}
}
type CompressibleAsyncDataTreeNodeMapper<TInput, T, TFilterData> = WeakMapper<ITreeNode<ICompressedTreeNode<IAsyncDataTreeNode<TInput, T>>, TFilterData>, ITreeNode<ICompressedTreeNode<TInput | T>, TFilterData>>;
class CompressibleAsyncDataTreeNodeWrapper<TInput, T, TFilterData> implements ITreeNode<ICompressedTreeNode<TInput | T>, TFilterData> {
get element(): ICompressedTreeNode<TInput | T> {
return {
elements: this.node.element.elements.map(e => e.element),
incompressible: this.node.element.incompressible
};
}
get children(): ITreeNode<ICompressedTreeNode<TInput | T>, TFilterData>[] { return this.node.children.map(node => new CompressibleAsyncDataTreeNodeWrapper(node)); }
get depth(): number { return this.node.depth; }
get visibleChildrenCount(): number { return this.node.visibleChildrenCount; }
get visibleChildIndex(): number { return this.node.visibleChildIndex; }
get collapsible(): boolean { return this.node.collapsible; }
get collapsed(): boolean { return this.node.collapsed; }
get visible(): boolean { return this.node.visible; }
get filterData(): TFilterData | undefined { return this.node.filterData; }
constructor(private node: ITreeNode<ICompressedTreeNode<IAsyncDataTreeNode<TInput, T>>, TFilterData>) { }
}
class CompressibleAsyncDataTreeRenderer<TInput, T, TFilterData, TTemplateData> implements ICompressibleTreeRenderer<IAsyncDataTreeNode<TInput, T>, TFilterData, IDataTreeListTemplateData<TTemplateData>> {
readonly templateId: string;
private renderedNodes = new Map<IAsyncDataTreeNode<TInput, T>, IDataTreeListTemplateData<TTemplateData>>();
private disposables: IDisposable[] = [];
constructor(
protected renderer: ICompressibleTreeRenderer<T, TFilterData, TTemplateData>,
protected nodeMapper: AsyncDataTreeNodeMapper<TInput, T, TFilterData>,
private compressibleNodeMapperProvider: () => CompressibleAsyncDataTreeNodeMapper<TInput, T, TFilterData>,
readonly onDidChangeTwistieState: Event<IAsyncDataTreeNode<TInput, T>>
) {
this.templateId = renderer.templateId;
}
renderTemplate(container: HTMLElement): IDataTreeListTemplateData<TTemplateData> {
const templateData = this.renderer.renderTemplate(container);
return { templateData };
}
renderElement(node: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>, height: number | undefined): void {
this.renderer.renderElement(this.nodeMapper.map(node) as ITreeNode<T, TFilterData>, index, templateData.templateData, height);
}
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<IAsyncDataTreeNode<TInput, T>>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>, height: number | undefined): void {
this.renderer.renderCompressedElements(this.compressibleNodeMapperProvider().map(node) as ITreeNode<ICompressedTreeNode<T>, TFilterData>, index, templateData.templateData, height);
}
renderTwistie(element: IAsyncDataTreeNode<TInput, T>, twistieElement: HTMLElement): boolean {
toggleClass(twistieElement, 'loading', element.slow);
return false;
}
disposeElement(node: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>, height: number | undefined): void {
if (this.renderer.disposeElement) {
this.renderer.disposeElement(this.nodeMapper.map(node) as ITreeNode<T, TFilterData>, index, templateData.templateData, height);
}
}
disposeTemplate(templateData: IDataTreeListTemplateData<TTemplateData>): void {
this.renderer.disposeTemplate(templateData.templateData);
}
dispose(): void {
this.renderedNodes.clear();
this.disposables = dispose(this.disposables);
}
}
export interface ITreeCompressionDelegate<T> {
isIncompressible(element: T): boolean;
}
export class CompressibleAsyncDataTree<TInput, T, TFilterData = void> extends AsyncDataTree<TInput, T, TFilterData> {
protected readonly compressibleNodeMapper: CompressibleAsyncDataTreeNodeMapper<TInput, T, TFilterData> = new WeakMapper(node => new CompressibleAsyncDataTreeNodeWrapper(node));
constructor(
user: string,
container: HTMLElement,
virtualDelegate: IListVirtualDelegate<T>,
private compressionDelegate: ITreeCompressionDelegate<T>,
renderers: ICompressibleTreeRenderer<T, TFilterData, any>[],
dataSource: IAsyncDataSource<TInput, T>,
options: IAsyncDataTreeOptions<T, TFilterData> = {}
) {
super(user, container, virtualDelegate, renderers, dataSource, options);
}
protected createTree(
user: string,
container: HTMLElement,
delegate: IListVirtualDelegate<T>,
renderers: ICompressibleTreeRenderer<T, TFilterData, any>[],
options: IAsyncDataTreeOptions<T, TFilterData>
): ObjectTree<IAsyncDataTreeNode<TInput, T>, TFilterData> {
const objectTreeDelegate = new ComposedTreeDelegate<TInput | T, IAsyncDataTreeNode<TInput, T>>(delegate);
const objectTreeRenderers = renderers.map(r => new CompressibleAsyncDataTreeRenderer(r, this.nodeMapper, () => this.compressibleNodeMapper, this._onDidChangeNodeSlowState.event));
const objectTreeOptions = asObjectTreeOptions<TInput, T, TFilterData>(options) || {};
return new CompressibleObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions);
}
protected asTreeElement(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): ICompressedTreeElement<IAsyncDataTreeNode<TInput, T>> {
return {
incompressible: this.compressionDelegate.isIncompressible(node.element as T),
...super.asTreeElement(node, viewStateContext)
};
}
}

View File

@@ -1,56 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Iterator, ISequence } from 'vs/base/common/iterator';
import { AbstractTree, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abstractTree';
import { ISpliceable } from 'vs/base/common/sequence';
import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, ICollapseStateChangeEvent } from 'vs/base/browser/ui/tree/tree';
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { Event } from 'vs/base/common/event';
import { CompressedTreeModel, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
export interface IObjectTreeOptions<T, TFilterData = void> extends IAbstractTreeOptions<T, TFilterData> {
sorter?: ITreeSorter<T>;
}
export class CompressedObjectTree<T extends NonNullable<any>, TFilterData = void> extends AbstractTree<ICompressedTreeNode<T> | null, TFilterData, T | null> {
protected model!: CompressedTreeModel<T, TFilterData>;
get onDidChangeCollapseState(): Event<ICollapseStateChangeEvent<ICompressedTreeNode<T> | null, TFilterData>> { return this.model.onDidChangeCollapseState; }
constructor(
container: HTMLElement,
delegate: IListVirtualDelegate<ICompressedTreeNode<T>>,
renderers: ITreeRenderer<ICompressedTreeNode<T>, TFilterData, any>[],
options: IObjectTreeOptions<ICompressedTreeNode<T>, TFilterData> = {}
) {
super(container, delegate, renderers, options);
}
setChildren(
element: T | null,
children?: ISequence<ITreeElement<T>>
): Iterator<ITreeElement<T | null>> {
return this.model.setChildren(element, children);
}
rerender(element?: T): void {
if (element === undefined) {
this.view.rerender();
return;
}
this.model.rerender(element);
}
resort(element: T, recursive = true): void {
this.model.resort(element, recursive);
}
protected createModel(view: ISpliceable<ITreeNode<ICompressedTreeNode<T>, TFilterData>>, options: IObjectTreeOptions<ICompressedTreeNode<T>, TFilterData>): ITreeModel<ICompressedTreeNode<T> | null, TFilterData, T | null> {
return new CompressedTreeModel(view, options);
}
}

View File

@@ -6,19 +6,32 @@
import { ISpliceable } from 'vs/base/common/sequence';
import { Iterator, ISequence } from 'vs/base/common/iterator';
import { Event } from 'vs/base/common/event';
import { ITreeModel, ITreeNode, ITreeElement, ICollapseStateChangeEvent, ITreeModelSpliceEvent } from 'vs/base/browser/ui/tree/tree';
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';
// Exported only for test reasons, do not use directly
export interface ICompressedTreeElement<T> extends ITreeElement<T> {
readonly children?: Iterator<ICompressedTreeElement<T>> | ICompressedTreeElement<T>[];
readonly incompressible?: boolean;
}
// Exported only for test reasons, do not use directly
export interface ICompressedTreeNode<T> {
readonly elements: T[];
readonly incompressible: boolean;
}
function noCompress<T>(element: ICompressedTreeElement<T>): ITreeElement<ICompressedTreeNode<T>> {
const elements = [element.element];
const incompressible = element.incompressible || false;
return {
element: { elements, incompressible },
children: Iterator.map(Iterator.from(element.children), noCompress)
};
}
// Exported only for test reasons, do not use directly
export function compress<T>(element: ICompressedTreeElement<T>): ITreeElement<ICompressedTreeNode<T>> {
const elements = [element.element];
const incompressible = element.incompressible || false;
@@ -49,7 +62,7 @@ export function compress<T>(element: ICompressedTreeElement<T>): ITreeElement<IC
};
}
export function _decompress<T>(element: ITreeElement<ICompressedTreeNode<T>>, index = 0): ICompressedTreeElement<T> {
function _decompress<T>(element: ITreeElement<ICompressedTreeNode<T>>, index = 0): ICompressedTreeElement<T> {
let children: Iterator<ICompressedTreeElement<T>>;
if (index < element.element.elements.length - 1) {
@@ -65,11 +78,12 @@ export function _decompress<T>(element: ITreeElement<ICompressedTreeNode<T>>, in
return { element: element.element.elements[index], children };
}
// Exported only for test reasons, do not use directly
export function decompress<T>(element: ITreeElement<ICompressedTreeNode<T>>): ICompressedTreeElement<T> {
return _decompress(element, 0);
}
export function splice<T>(treeElement: ICompressedTreeElement<T>, element: T, children: Iterator<ICompressedTreeElement<T>>): ICompressedTreeElement<T> {
function splice<T>(treeElement: ICompressedTreeElement<T>, element: T, children: Iterator<ICompressedTreeElement<T>>): ICompressedTreeElement<T> {
if (treeElement.element === element) {
return { element, children };
}
@@ -80,9 +94,10 @@ export function splice<T>(treeElement: ICompressedTreeElement<T>, element: T, ch
};
}
export interface ICompressedTreeModelOptions<T, TFilterData> extends IObjectTreeModelOptions<ICompressedTreeNode<T>, TFilterData> { }
interface ICompressedObjectTreeModelOptions<T, TFilterData> extends IObjectTreeModelOptions<ICompressedTreeNode<T>, TFilterData> { }
export class CompressedTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> implements ITreeModel<ICompressedTreeNode<T> | null, TFilterData, T | null> {
// 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> {
readonly rootRef = null;
@@ -92,35 +107,77 @@ export class CompressedTreeModel<T extends NonNullable<any>, TFilterData extends
private model: ObjectTreeModel<ICompressedTreeNode<T>, TFilterData>;
private nodes = new Map<T | null, ICompressedTreeNode<T>>();
private enabled: boolean = true;
get size(): number { return this.nodes.size; }
constructor(list: ISpliceable<ITreeNode<ICompressedTreeNode<T>, TFilterData>>, options: ICompressedTreeModelOptions<T, TFilterData> = {}) {
this.model = new ObjectTreeModel(list, options);
constructor(
private user: string,
list: ISpliceable<ITreeNode<ICompressedTreeNode<T>, TFilterData>>,
options: ICompressedObjectTreeModelOptions<T, TFilterData> = {}
) {
this.model = new ObjectTreeModel(user, list, options);
}
setChildren(
element: T | null,
children: ISequence<ICompressedTreeElement<T>> | undefined,
onDidCreateNode?: (node: ITreeNode<ICompressedTreeNode<T>, TFilterData>) => void,
onDidDeleteNode?: (node: ITreeNode<ICompressedTreeNode<T>, TFilterData>) => void
): Iterator<ITreeElement<T | null>> {
children: ISequence<ICompressedTreeElement<T>> | undefined
): void {
if (element === null) {
const compressedChildren = Iterator.map(Iterator.from(children), this.enabled ? compress : noCompress);
this._setChildren(null, compressedChildren);
return;
}
const compressedNode = this.nodes.get(element);
if (!compressedNode) {
throw new Error('Unknown compressed tree node');
}
const node = this.model.getNode(compressedNode) as ITreeNode<ICompressedTreeNode<T>, TFilterData>;
const compressedParentNode = this.model.getParentNodeLocation(compressedNode);
const parent = this.model.getNode(compressedParentNode) as ITreeNode<ICompressedTreeNode<T>, TFilterData>;
const decompressedElement = decompress(node);
const splicedElement = splice(decompressedElement, element, Iterator.from(children));
const recompressedElement = (this.enabled ? compress : noCompress)(splicedElement);
const parentChildren = parent.children
.map(child => child === node ? recompressedElement : child);
this._setChildren(parent.element, parentChildren);
}
isCompressionEnabled(): boolean {
return this.enabled;
}
setCompressionEnabled(enabled: boolean): void {
if (enabled === this.enabled) {
return;
}
this.enabled = enabled;
const root = this.model.getNode();
const rootChildren = Iterator.from((root.children as unknown) as ITreeNode<ICompressedTreeNode<T>>[]); // {{SQL CARBON EDIT}} strict-null-checks
const decompressedRootChildren = Iterator.map(rootChildren, decompress);
const recompressedRootChildren = Iterator.map(decompressedRootChildren, enabled ? compress : noCompress);
this._setChildren(null, recompressedRootChildren);
}
private _setChildren(
node: ICompressedTreeNode<T> | null,
children: ISequence<ITreeElement<ICompressedTreeNode<T>>> | undefined
): void {
const insertedElements = new Set<T | null>();
const _onDidCreateNode = (node: ITreeNode<ICompressedTreeNode<T>, TFilterData>) => {
for (const element of node.element.elements) {
insertedElements.add(element);
this.nodes.set(element, node.element);
}
// if (this.identityProvider) {
// const id = this.identityProvider.getId(node.element).toString();
// insertedElementIds.add(id);
// this.nodesByIdentity.set(id, node);
// }
if (onDidCreateNode) {
onDidCreateNode(node);
}
};
const _onDidDeleteNode = (node: ITreeNode<ICompressedTreeNode<T>, TFilterData>) => {
@@ -129,40 +186,9 @@ export class CompressedTreeModel<T extends NonNullable<any>, TFilterData extends
this.nodes.delete(element);
}
}
// if (this.identityProvider) {
// const id = this.identityProvider.getId(node.element).toString();
// if (!insertedElementIds.has(id)) {
// this.nodesByIdentity.delete(id);
// }
// }
if (onDidDeleteNode) {
onDidDeleteNode(node);
}
};
if (element === null) {
const compressedChildren = Iterator.map(Iterator.from(children), compress);
const result = this.model.setChildren(null, compressedChildren, _onDidCreateNode, _onDidDeleteNode);
return Iterator.map(result, decompress);
}
const compressedNode = this.nodes.get(element);
const node = this.model.getNode(compressedNode) as ITreeNode<ICompressedTreeNode<T>, TFilterData>;
const parent = node.parent!;
const decompressedElement = decompress(node);
const splicedElement = splice(decompressedElement, element, Iterator.from(children));
const recompressedElement = compress(splicedElement);
const parentChildren = parent.children
.map(child => child === node ? recompressedElement : child);
this.model.setChildren(parent.element, parentChildren, _onDidCreateNode, _onDidDeleteNode);
// TODO
return Iterator.empty();
this.model.setChildren(node, children, _onDidCreateNode, _onDidDeleteNode);
}
getListIndex(location: T | null): number {
@@ -207,11 +233,6 @@ export class CompressedTreeModel<T extends NonNullable<any>, TFilterData extends
return parentNode.elements[parentNode.elements.length - 1];
}
getParentElement(location: T | null): ICompressedTreeNode<T> | null {
const compressedNode = this.getCompressedNode(location);
return this.model.getParentElement(compressedNode);
}
getFirstElementChild(location: T | null): ICompressedTreeNode<T> | null | undefined {
const compressedNode = this.getCompressedNode(location);
return this.model.getFirstElementChild(compressedNode);
@@ -227,6 +248,11 @@ export class CompressedTreeModel<T extends NonNullable<any>, TFilterData extends
return this.model.isCollapsible(compressedNode);
}
setCollapsible(location: T | null, collapsible?: boolean): boolean {
const compressedNode = this.getCompressedNode(location);
return this.model.setCollapsible(compressedNode, collapsible);
}
isCollapsed(location: T | null): boolean {
const compressedNode = this.getCompressedNode(location);
return this.model.isCollapsed(compressedNode);
@@ -256,7 +282,7 @@ export class CompressedTreeModel<T extends NonNullable<any>, TFilterData extends
this.model.resort(compressedNode, recursive);
}
private getCompressedNode(element: T | null): ICompressedTreeNode<T> | null {
getCompressedNode(element: T | null): ICompressedTreeNode<T> | null {
if (element === null) {
return null;
}
@@ -264,78 +290,120 @@ export class CompressedTreeModel<T extends NonNullable<any>, TFilterData extends
const node = this.nodes.get(element);
if (!node) {
throw new Error(`Tree element not found: ${element}`);
throw new TreeError(this.user, `Tree element not found: ${element}`);
}
return node;
}
}
// Compressible Object Tree
export type ElementMapper<T> = (elements: T[]) => T;
export const DefaultElementMapper: ElementMapper<any> = elements => elements[elements.length - 1];
export type NodeMapper<T, TFilterData> = (node: ITreeNode<ICompressedTreeNode<T> | null, TFilterData>) => ITreeNode<T | null, TFilterData>;
export type CompressedNodeUnwrapper<T> = (node: ICompressedTreeNode<T>) => T;
type CompressedNodeWeakMapper<T, TFilterData> = WeakMapper<ITreeNode<ICompressedTreeNode<T> | null, TFilterData>, ITreeNode<T | null, TFilterData>>;
function mapNode<T, TFilterData>(elementMapper: ElementMapper<T>, node: ITreeNode<ICompressedTreeNode<T> | null, TFilterData>): ITreeNode<T | null, TFilterData> {
class CompressedTreeNodeWrapper<T, TFilterData> implements ITreeNode<T | null, TFilterData> {
get element(): T | null { return this.node.element === null ? null : this.unwrapper(this.node.element); }
get children(): ITreeNode<T | null, TFilterData>[] { return this.node.children.map(node => new CompressedTreeNodeWrapper(this.unwrapper, node)); }
get depth(): number { return this.node.depth; }
get visibleChildrenCount(): number { return this.node.visibleChildrenCount; }
get visibleChildIndex(): number { return this.node.visibleChildIndex; }
get collapsible(): boolean { return this.node.collapsible; }
get collapsed(): boolean { return this.node.collapsed; }
get visible(): boolean { return this.node.visible; }
get filterData(): TFilterData | undefined { return this.node.filterData; }
constructor(
private unwrapper: CompressedNodeUnwrapper<T>,
private node: ITreeNode<ICompressedTreeNode<T> | null, TFilterData>
) { }
}
function mapList<T, TFilterData>(nodeMapper: CompressedNodeWeakMapper<T, TFilterData>, list: ISpliceable<ITreeNode<T, TFilterData>>): ISpliceable<ITreeNode<ICompressedTreeNode<T>, TFilterData>> {
return {
...node,
element: node.element === null ? null : elementMapper(node.element.elements),
children: node.children.map(child => mapNode(elementMapper, child)),
parent: typeof node.parent === 'undefined' ? node.parent : mapNode(elementMapper, node.parent)
splice(start: number, deleteCount: number, toInsert: ITreeNode<ICompressedTreeNode<T>, TFilterData>[]): void {
list.splice(start, deleteCount, toInsert.map(node => nodeMapper.map(node)) as ITreeNode<T, TFilterData>[]);
}
};
}
function createNodeMapper<T, TFilterData>(elementMapper: ElementMapper<T>): NodeMapper<T, TFilterData> {
return node => mapNode(elementMapper, node);
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(compressedNodeUnwrapper(node), compressedNodeUnwrapper(otherNode));
}
},
identityProvider: options.identityProvider && {
getId(node: ICompressedTreeNode<T>): { toString(): string; } {
return options.identityProvider!.getId(compressedNodeUnwrapper(node));
}
},
filter: options.filter && {
filter(node: ICompressedTreeNode<T>, parentVisibility: TreeVisibility): TreeFilterResult<TFilterData> {
return options.filter!.filter(compressedNodeUnwrapper(node), parentVisibility);
}
}
};
}
export interface ICompressedObjectTreeModelOptions<T, TFilterData> extends ICompressedTreeModelOptions<T, TFilterData> {
export interface ICompressibleObjectTreeModelOptions<T, TFilterData> extends IObjectTreeModelOptions<T, TFilterData> {
readonly elementMapper?: ElementMapper<T>;
}
export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> implements IObjectTreeModel<T, TFilterData> {
export class CompressibleObjectTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> implements IObjectTreeModel<T, TFilterData> {
readonly rootRef = null;
get onDidSplice(): Event<ITreeModelSpliceEvent<T | null, TFilterData>> {
return Event.map(this.model.onDidSplice, ({ insertedNodes, deletedNodes }) => ({
insertedNodes: insertedNodes.map(this.mapNode),
deletedNodes: deletedNodes.map(this.mapNode),
insertedNodes: insertedNodes.map(node => this.nodeMapper.map(node)),
deletedNodes: deletedNodes.map(node => this.nodeMapper.map(node)),
}));
}
get onDidChangeCollapseState(): Event<ICollapseStateChangeEvent<T | null, TFilterData>> {
return Event.map(this.model.onDidChangeCollapseState, ({ node, deep }) => ({
node: this.mapNode(node),
node: this.nodeMapper.map(node),
deep
}));
}
get onDidChangeRenderNodeCount(): Event<ITreeNode<T | null, TFilterData>> {
return Event.map(this.model.onDidChangeRenderNodeCount, this.mapNode);
return Event.map(this.model.onDidChangeRenderNodeCount, node => this.nodeMapper.map(node));
}
private mapElement: ElementMapper<T | null>;
private mapNode: NodeMapper<T | null, TFilterData>;
private model: CompressedTreeModel<T, TFilterData>;
private elementMapper: ElementMapper<T>;
private nodeMapper: CompressedNodeWeakMapper<T, TFilterData>;
private model: CompressedObjectTreeModel<T, TFilterData>;
constructor(
list: ISpliceable<ITreeNode<ICompressedTreeNode<T>, TFilterData>>,
options: ICompressedObjectTreeModelOptions<T, TFilterData> = {}
user: string,
list: ISpliceable<ITreeNode<T, TFilterData>>,
options: ICompressibleObjectTreeModelOptions<T, TFilterData> = {}
) {
this.mapElement = options.elementMapper || DefaultElementMapper;
this.mapNode = createNodeMapper(this.mapElement);
this.model = new CompressedTreeModel(list, options);
this.elementMapper = options.elementMapper || DefaultElementMapper;
const compressedNodeUnwrapper: CompressedNodeUnwrapper<T> = node => this.elementMapper(node.elements);
this.nodeMapper = new WeakMapper(node => new CompressedTreeNodeWrapper(compressedNodeUnwrapper, node));
this.model = new CompressedObjectTreeModel(user, mapList(this.nodeMapper, list), mapOptions(compressedNodeUnwrapper, options));
}
setChildren(
element: T | null,
children: ISequence<ITreeElement<T>> | undefined
): Iterator<ITreeElement<T>> {
setChildren(element: T | null, children?: ISequence<ICompressedTreeElement<T>>): void {
this.model.setChildren(element, children);
}
// TODO
return Iterator.empty();
isCompressionEnabled(): boolean {
return this.model.isCompressionEnabled();
}
setCompressionEnabled(enabled: boolean): void {
this.model.setCompressionEnabled(enabled);
}
getListIndex(location: T | null): number {
@@ -347,7 +415,7 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
}
getNode(location?: T | null | undefined): ITreeNode<T | null, any> {
return this.mapNode(this.model.getNode(location));
return this.nodeMapper.map(this.model.getNode(location));
}
getNodeLocation(node: ITreeNode<T | null, any>): T | null {
@@ -358,16 +426,6 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
return this.model.getParentNodeLocation(location);
}
getParentElement(location: T | null): T | null {
const result = this.model.getParentElement(location);
if (result === null) {
return null; // {{SQL CARBON EDIT}} strict-null-check
}
return this.mapElement(result.elements);
}
getFirstElementChild(location: T | null): T | null | undefined {
const result = this.model.getFirstElementChild(location);
@@ -375,7 +433,7 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
return null; // {{SQL CARBON EDIT}} strict-null-check
}
return this.mapElement(result.elements);
return this.elementMapper(result.elements);
}
getLastElementAncestor(location?: T | null | undefined): T | null | undefined {
@@ -385,13 +443,17 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
return null; // {{SQL CARBON EDIT}} strict-null-check
}
return this.mapElement(result.elements);
return this.elementMapper(result.elements);
}
isCollapsible(location: T | null): boolean {
return this.model.isCollapsible(location);
}
setCollapsible(location: T | null, collapsed?: boolean): boolean {
return this.model.setCollapsible(location, collapsed);
}
isCollapsed(location: T | null): boolean {
return this.model.isCollapsed(location);
}
@@ -415,4 +477,8 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
resort(element: T | null = null, recursive = true): void {
return this.model.resort(element, recursive);
}
}
getCompressedTreeNode(element: T): ITreeNode<ICompressedTreeNode<T>, TFilterData> {
return this.model.getNode(element) as ITreeNode<ICompressedTreeNode<T>, TFilterData>;
}
}

View File

@@ -5,7 +5,7 @@
import { AbstractTree, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abstractTree';
import { ISpliceable } from 'vs/base/common/sequence';
import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, IDataSource } from 'vs/base/browser/ui/tree/tree';
import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, IDataSource, TreeError } from 'vs/base/browser/ui/tree/tree';
import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel';
import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list';
import { Iterator } from 'vs/base/common/iterator';
@@ -30,13 +30,14 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
private nodesByIdentity = new Map<string, ITreeNode<T, TFilterData>>();
constructor(
private user: string,
container: HTMLElement,
delegate: IListVirtualDelegate<T>,
renderers: ITreeRenderer<T, TFilterData, any>[],
private dataSource: IDataSource<TInput, T>,
options: IDataTreeOptions<T, TFilterData> = {}
) {
super(container, delegate, renderers, options);
super(user, container, delegate, renderers, options);
this.identityProvider = options.identityProvider;
}
@@ -48,7 +49,7 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
setInput(input: TInput, viewState?: IDataTreeViewState): void {
if (viewState && !this.identityProvider) {
throw new Error('Can\'t restore tree view state without an identity provider');
throw new TreeError(this.user, 'Can\'t restore tree view state without an identity provider');
}
this.input = input;
@@ -89,7 +90,7 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
updateChildren(element: TInput | T = this.input!): void {
if (typeof this.input === 'undefined') {
throw new Error('Tree input not set');
throw new TreeError(this.user, 'Tree input not set');
}
let isCollapsed: ((el: T) => boolean | undefined) | undefined;
@@ -169,15 +170,15 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
return { elements, size: children.length };
}
protected createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IDataTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
return new ObjectTreeModel(view, options);
protected createModel(user: string, view: ISpliceable<ITreeNode<T, TFilterData>>, options: IDataTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
return new ObjectTreeModel(user, view, options);
}
// view state
getViewState(): IDataTreeViewState {
if (!this.identityProvider) {
throw new Error('Can\'t get tree view state without an identity provider');
throw new TreeError(this.user, 'Can\'t get tree view state without an identity provider');
}
const getId = (element: T) => this.identityProvider!.getId(element).toString();

View File

@@ -18,17 +18,18 @@ export class IndexTree<T, TFilterData = void> extends AbstractTree<T, TFilterDat
protected model!: IndexTreeModel<T, TFilterData>;
constructor(
user: string,
container: HTMLElement,
delegate: IListVirtualDelegate<T>,
renderers: ITreeRenderer<T, TFilterData, any>[],
private rootElement: T,
options: IIndexTreeOptions<T, TFilterData> = {}
) {
super(container, delegate, renderers, options);
super(user, container, delegate, renderers, options);
}
splice(location: number[], deleteCount: number, toInsert: ISequence<ITreeElement<T>> = Iterator.empty()): Iterator<ITreeElement<T>> {
return this.model.splice(location, deleteCount, toInsert);
splice(location: number[], deleteCount: number, toInsert: ISequence<ITreeElement<T>> = Iterator.empty()): void {
this.model.splice(location, deleteCount, toInsert);
}
rerender(location?: number[]): void {
@@ -40,7 +41,7 @@ export class IndexTree<T, TFilterData = void> extends AbstractTree<T, TFilterDat
this.model.rerender(location);
}
protected createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IIndexTreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, number[]> {
return new IndexTreeModel(view, this.rootElement, options);
protected createModel(user: string, view: ISpliceable<ITreeNode<T, TFilterData>>, options: IIndexTreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, number[]> {
return new IndexTreeModel(user, view, this.rootElement, options);
}
}

View File

@@ -3,15 +3,16 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICollapseStateChangeEvent, ITreeElement, ITreeFilter, ITreeFilterDataResult, ITreeModel, ITreeNode, TreeVisibility, ITreeModelSpliceEvent } from 'vs/base/browser/ui/tree/tree';
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 { Emitter, Event, EventBufferer } from 'vs/base/common/event';
import { ISequence, Iterator } from 'vs/base/common/iterator';
import { ISpliceable } from 'vs/base/common/sequence';
interface IMutableTreeNode<T, TFilterData> extends ITreeNode<T, TFilterData> {
readonly parent: IMutableTreeNode<T, TFilterData> | undefined;
readonly children: IMutableTreeNode<T, TFilterData>[];
// Exported for tests
export interface IIndexTreeNode<T, TFilterData = void> extends ITreeNode<T, TFilterData> {
readonly parent: IIndexTreeNode<T, TFilterData> | undefined;
readonly children: IIndexTreeNode<T, TFilterData>[];
visibleChildrenCount: number;
visibleChildIndex: number;
collapsible: boolean;
@@ -33,24 +34,32 @@ export function getVisibleState(visibility: boolean | TreeVisibility): TreeVisib
}
}
function treeNodeToElement<T>(node: IMutableTreeNode<T, any>): ITreeElement<T> {
const { element, collapsed } = node;
const children = Iterator.map(Iterator.fromArray(node.children), treeNodeToElement);
return { element, children, collapsed };
}
export interface IIndexTreeModelOptions<T, TFilterData> {
readonly collapseByDefault?: boolean; // defaults to false
readonly filter?: ITreeFilter<T, TFilterData>;
readonly autoExpandSingleChildren?: boolean;
}
interface CollapsibleStateUpdate {
readonly collapsible: boolean;
}
interface CollapsedStateUpdate {
readonly collapsed: boolean;
readonly recursive: boolean;
}
type CollapseStateUpdate = CollapsibleStateUpdate | CollapsedStateUpdate;
function isCollapsibleStateUpdate(update: CollapseStateUpdate): update is CollapsibleStateUpdate {
return typeof (update as any).collapsible === 'boolean';
}
export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = void> implements ITreeModel<T, TFilterData, number[]> {
readonly rootRef = [];
private root: IMutableTreeNode<T, TFilterData>;
private root: IIndexTreeNode<T, TFilterData>;
private eventBufferer = new EventBufferer();
private _onDidChangeCollapseState = new Emitter<ICollapseStateChangeEvent<T, TFilterData>>();
@@ -66,7 +75,12 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
private _onDidSplice = new Emitter<ITreeModelSpliceEvent<T, TFilterData>>();
readonly onDidSplice = this._onDidSplice.event;
constructor(private list: ISpliceable<ITreeNode<T, TFilterData>>, rootElement: T, options: IIndexTreeModelOptions<T, TFilterData> = {}) {
constructor(
private user: string,
private list: ISpliceable<ITreeNode<T, TFilterData>>,
rootElement: T,
options: IIndexTreeModelOptions<T, TFilterData> = {}
) {
this.collapseByDefault = typeof options.collapseByDefault === 'undefined' ? false : options.collapseByDefault;
this.filter = options.filter;
this.autoExpandSingleChildren = typeof options.autoExpandSingleChildren === 'undefined' ? false : options.autoExpandSingleChildren;
@@ -92,9 +106,9 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
toInsert?: ISequence<ITreeElement<T>>,
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void,
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
): Iterator<ITreeElement<T>> {
): void {
if (location.length === 0) {
throw new Error('Invalid tree location');
throw new TreeError(this.user, 'Invalid tree location');
}
const { parentNode, listIndex, revealed, visible } = this.getParentNodeWithListIndex(location);
@@ -116,7 +130,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
}
}
const nodesToInsert: IMutableTreeNode<T, TFilterData>[] = [];
const nodesToInsert: IIndexTreeNode<T, TFilterData>[] = [];
let insertedVisibleChildrenCount = 0;
let renderNodeCount = 0;
@@ -170,14 +184,12 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
deletedNodes.forEach(visit);
}
const result = Iterator.map(Iterator.fromArray(deletedNodes), treeNodeToElement);
this._onDidSplice.fire({ insertedNodes: nodesToInsert, deletedNodes });
return result;
}
rerender(location: number[]): void {
if (location.length === 0) {
throw new Error('Invalid tree location');
throw new TreeError(this.user, 'Invalid tree location');
}
const { node, listIndex, revealed } = this.getTreeNodeWithListIndex(location);
@@ -200,6 +212,17 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
return this.getTreeNode(location).collapsible;
}
setCollapsible(location: number[], collapsible?: boolean): boolean {
const node = this.getTreeNode(location);
if (typeof collapsible === 'undefined') {
collapsible = !node.collapsible;
}
const update: CollapsibleStateUpdate = { collapsible };
return this.eventBufferer.bufferEvents(() => this._setCollapseState(location, update));
}
isCollapsed(location: number[]): boolean {
return this.getTreeNode(location).collapsed;
}
@@ -211,15 +234,16 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
collapsed = !node.collapsed;
}
return this.eventBufferer.bufferEvents(() => this._setCollapsed(location, collapsed!, recursive));
const update: CollapsedStateUpdate = { collapsed, recursive: recursive || false };
return this.eventBufferer.bufferEvents(() => this._setCollapseState(location, update));
}
private _setCollapsed(location: number[], collapsed: boolean, recursive?: boolean): boolean {
private _setCollapseState(location: number[], update: CollapseStateUpdate): boolean {
const { node, listIndex, revealed } = this.getTreeNodeWithListIndex(location);
const result = this._setListNodeCollapsed(node, listIndex, revealed, collapsed!, recursive || false);
const result = this._setListNodeCollapseState(node, listIndex, revealed, update);
if (node !== this.root && this.autoExpandSingleChildren && !collapsed! && !recursive) {
if (node !== this.root && this.autoExpandSingleChildren && result && !isCollapsibleStateUpdate(update) && node.collapsible && !node.collapsed && !update.recursive) {
let onlyVisibleChildIndex = -1;
for (let i = 0; i < node.children.length; i++) {
@@ -236,17 +260,17 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
}
if (onlyVisibleChildIndex > -1) {
this._setCollapsed([...location, onlyVisibleChildIndex], false, false);
this._setCollapseState([...location, onlyVisibleChildIndex], update);
}
}
return result;
}
private _setListNodeCollapsed(node: IMutableTreeNode<T, TFilterData>, listIndex: number, revealed: boolean, collapsed: boolean, recursive: boolean): boolean {
const result = this._setNodeCollapsed(node, collapsed, recursive, false);
private _setListNodeCollapseState(node: IIndexTreeNode<T, TFilterData>, listIndex: number, revealed: boolean, update: CollapseStateUpdate): boolean {
const result = this._setNodeCollapseState(node, update, false);
if (!revealed || !node.visible) {
if (!revealed || !node.visible || !result) {
return result;
}
@@ -258,20 +282,28 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
return result;
}
private _setNodeCollapsed(node: IMutableTreeNode<T, TFilterData>, collapsed: boolean, recursive: boolean, deep: boolean): boolean {
let result = node.collapsible && node.collapsed !== collapsed;
private _setNodeCollapseState(node: IIndexTreeNode<T, TFilterData>, update: CollapseStateUpdate, deep: boolean): boolean {
let result: boolean;
if (node.collapsible) {
node.collapsed = collapsed;
if (node === this.root) {
result = false;
} else {
if (isCollapsibleStateUpdate(update)) {
result = node.collapsible !== update.collapsible;
node.collapsible = update.collapsible;
} else {
result = node.collapsed !== update.collapsed;
node.collapsed = update.collapsed;
}
if (result) {
this._onDidChangeCollapseState.fire({ node, deep });
}
}
if (recursive) {
if (!isCollapsibleStateUpdate(update) && update.recursive) {
for (const child of node.children) {
result = this._setNodeCollapsed(child, collapsed, true, true) || result;
result = this._setNodeCollapseState(child, update, true) || result;
}
}
@@ -287,7 +319,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
location = location.slice(0, location.length - 1);
if (node.collapsed) {
this._setCollapsed(location, false);
this._setCollapseState(location, { collapsed: false, recursive: false });
}
}
});
@@ -301,13 +333,13 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
private createTreeNode(
treeElement: ITreeElement<T>,
parent: IMutableTreeNode<T, TFilterData>,
parent: IIndexTreeNode<T, TFilterData>,
parentVisibility: TreeVisibility,
revealed: boolean,
treeListElements: ITreeNode<T, TFilterData>[],
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void
): IMutableTreeNode<T, TFilterData> {
const node: IMutableTreeNode<T, TFilterData> = {
): IIndexTreeNode<T, TFilterData> {
const node: IIndexTreeNode<T, TFilterData> = {
parent,
element: treeElement.element,
children: [],
@@ -364,7 +396,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
return node;
}
private updateNodeAfterCollapseChange(node: IMutableTreeNode<T, TFilterData>): ITreeNode<T, TFilterData>[] {
private updateNodeAfterCollapseChange(node: IIndexTreeNode<T, TFilterData>): ITreeNode<T, TFilterData>[] {
const previousRenderNodeCount = node.renderNodeCount;
const result: ITreeNode<T, TFilterData>[] = [];
@@ -374,7 +406,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
return result;
}
private _updateNodeAfterCollapseChange(node: IMutableTreeNode<T, TFilterData>, result: ITreeNode<T, TFilterData>[]): number {
private _updateNodeAfterCollapseChange(node: IIndexTreeNode<T, TFilterData>, result: ITreeNode<T, TFilterData>[]): number {
if (node.visible === false) {
return 0;
}
@@ -392,7 +424,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
return node.renderNodeCount;
}
private updateNodeAfterFilterChange(node: IMutableTreeNode<T, TFilterData>): ITreeNode<T, TFilterData>[] {
private updateNodeAfterFilterChange(node: IIndexTreeNode<T, TFilterData>): ITreeNode<T, TFilterData>[] {
const previousRenderNodeCount = node.renderNodeCount;
const result: ITreeNode<T, TFilterData>[] = [];
@@ -402,7 +434,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
return result;
}
private _updateNodeAfterFilterChange(node: IMutableTreeNode<T, TFilterData>, parentVisibility: TreeVisibility, result: ITreeNode<T, TFilterData>[], revealed = true): boolean {
private _updateNodeAfterFilterChange(node: IIndexTreeNode<T, TFilterData>, parentVisibility: TreeVisibility, result: ITreeNode<T, TFilterData>[], revealed = true): boolean {
let visibility: TreeVisibility;
if (node !== this.root) {
@@ -456,7 +488,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
return node.visible;
}
private _updateAncestorsRenderNodeCount(node: IMutableTreeNode<T, TFilterData> | undefined, diff: number): void {
private _updateAncestorsRenderNodeCount(node: IIndexTreeNode<T, TFilterData> | undefined, diff: number): void {
if (diff === 0) {
return;
}
@@ -468,7 +500,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
}
}
private _filterNode(node: IMutableTreeNode<T, TFilterData>, parentVisibility: TreeVisibility): TreeVisibility {
private _filterNode(node: IIndexTreeNode<T, TFilterData>, parentVisibility: TreeVisibility): TreeVisibility {
const result = this.filter ? this.filter.filter(node.element, parentVisibility) : TreeVisibility.Visible;
if (typeof result === 'boolean') {
@@ -484,7 +516,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
}
// cheap
private getTreeNode(location: number[], node: IMutableTreeNode<T, TFilterData> = this.root): IMutableTreeNode<T, TFilterData> {
private getTreeNode(location: number[], node: IIndexTreeNode<T, TFilterData> = this.root): IIndexTreeNode<T, TFilterData> {
if (!location || location.length === 0) {
return node;
}
@@ -492,14 +524,14 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
const [index, ...rest] = location;
if (index < 0 || index > node.children.length) {
throw new Error('Invalid tree location');
throw new TreeError(this.user, 'Invalid tree location');
}
return this.getTreeNode(rest, node.children[index]);
}
// expensive
private getTreeNodeWithListIndex(location: number[]): { node: IMutableTreeNode<T, TFilterData>, listIndex: number, revealed: boolean, visible: boolean } {
private getTreeNodeWithListIndex(location: number[]): { node: IIndexTreeNode<T, TFilterData>, listIndex: number, revealed: boolean, visible: boolean } {
if (location.length === 0) {
return { node: this.root, listIndex: -1, revealed: true, visible: false };
}
@@ -508,7 +540,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
const index = location[location.length - 1];
if (index < 0 || index > parentNode.children.length) {
throw new Error('Invalid tree location');
throw new TreeError(this.user, 'Invalid tree location');
}
const node = parentNode.children[index];
@@ -516,11 +548,11 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
return { node, listIndex, revealed, visible: visible && node.visible };
}
private getParentNodeWithListIndex(location: number[], node: IMutableTreeNode<T, TFilterData> = this.root, listIndex: number = 0, revealed = true, visible = true): { parentNode: IMutableTreeNode<T, TFilterData>; listIndex: number; revealed: boolean; visible: boolean; } {
private getParentNodeWithListIndex(location: number[], node: IIndexTreeNode<T, TFilterData> = this.root, listIndex: number = 0, revealed = true, visible = true): { parentNode: IIndexTreeNode<T, TFilterData>; listIndex: number; revealed: boolean; visible: boolean; } {
const [index, ...rest] = location;
if (index < 0 || index > node.children.length) {
throw new Error('Invalid tree location');
throw new TreeError(this.user, 'Invalid tree location');
}
// TODO@joao perf!
@@ -545,27 +577,24 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
// TODO@joao perf!
getNodeLocation(node: ITreeNode<T, TFilterData>): number[] {
const location: number[] = [];
let indexTreeNode = node as IIndexTreeNode<T, TFilterData>; // typing woes
while (node.parent) {
location.push(node.parent.children.indexOf(node));
node = node.parent;
while (indexTreeNode.parent) {
location.push(indexTreeNode.parent.children.indexOf(indexTreeNode));
indexTreeNode = indexTreeNode.parent;
}
return location.reverse();
}
getParentNodeLocation(location: number[]): number[] {
if (location.length <= 1) {
getParentNodeLocation(location: number[]): number[] | undefined {
if (location.length === 0) {
return undefined;
} else if (location.length === 1) {
return [];
} else {
return tail2(location)[0];
}
return tail2(location)[0];
}
getParentElement(location: number[]): T {
const parentLocation = this.getParentNodeLocation(location);
const node = this.getTreeNode(parentLocation);
return node.element;
}
getFirstElementChild(location: number[]): T | undefined {

View File

@@ -3,13 +3,14 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Iterator, ISequence } from 'vs/base/common/iterator';
import { ISequence } from 'vs/base/common/iterator';
import { AbstractTree, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abstractTree';
import { ISpliceable } from 'vs/base/common/sequence';
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 } 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';
export interface IObjectTreeOptions<T, TFilterData = void> extends IAbstractTreeOptions<T, TFilterData> {
sorter?: ITreeSorter<T>;
@@ -22,19 +23,17 @@ export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends
get onDidChangeCollapseState(): Event<ICollapseStateChangeEvent<T | null, TFilterData>> { return this.model.onDidChangeCollapseState; }
constructor(
user: string,
container: HTMLElement,
delegate: IListVirtualDelegate<T>,
renderers: ITreeRenderer<T, TFilterData, any>[],
options: IObjectTreeOptions<T, TFilterData> = {}
) {
super(container, delegate, renderers, options);
super(user, container, delegate, renderers, options);
}
setChildren(
element: T | null,
children?: ISequence<ITreeElement<T>>
): Iterator<ITreeElement<T | null>> {
return this.model.setChildren(element, children);
setChildren(element: T | null, children?: ISequence<ITreeElement<T>>): void {
this.model.setChildren(element, children);
}
rerender(element?: T): void {
@@ -50,7 +49,117 @@ export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends
this.model.resort(element, recursive);
}
protected createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IObjectTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
return new ObjectTreeModel(view, options);
protected createModel(user: string, view: ISpliceable<ITreeNode<T, TFilterData>>, options: IObjectTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
return new ObjectTreeModel(user, view, options);
}
}
interface ICompressedTreeNodeProvider<T, TFilterData> {
getCompressedTreeNode(element: T): ITreeNode<ICompressedTreeNode<T>, TFilterData>;
}
export interface ICompressibleObjectTreeOptions<T, TFilterData = void> extends IObjectTreeOptions<T, TFilterData> {
readonly elementMapper?: ElementMapper<T>;
}
export interface ICompressibleTreeRenderer<T, TFilterData = void, TTemplateData = void> extends ITreeRenderer<T, TFilterData, TTemplateData> {
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<T>, TFilterData>, index: number, templateData: TTemplateData, height: number | undefined): void;
disposeCompressedElements?(node: ITreeNode<ICompressedTreeNode<T>, TFilterData>, index: number, templateData: TTemplateData, height: number | undefined): void;
}
interface CompressibleTemplateData<T, TFilterData, TTemplateData> {
compressedTreeNode: ITreeNode<ICompressedTreeNode<T>, TFilterData> | undefined;
readonly data: TTemplateData;
}
class CompressibleRenderer<T, TFilterData, TTemplateData> implements ITreeRenderer<T, TFilterData, CompressibleTemplateData<T, TFilterData, TTemplateData>> {
readonly templateId: string;
readonly onDidChangeTwistieState: Event<T> | undefined;
compressedTreeNodeProvider: ICompressedTreeNodeProvider<T, TFilterData>;
constructor(private renderer: ICompressibleTreeRenderer<T, TFilterData, TTemplateData>) {
this.templateId = renderer.templateId;
if (renderer.onDidChangeTwistieState) {
this.onDidChangeTwistieState = renderer.onDidChangeTwistieState;
}
}
renderTemplate(container: HTMLElement): CompressibleTemplateData<T, TFilterData, TTemplateData> {
const data = this.renderer.renderTemplate(container);
return { compressedTreeNode: undefined, data };
}
renderElement(node: ITreeNode<T, TFilterData>, index: number, templateData: CompressibleTemplateData<T, TFilterData, TTemplateData>, height: number | undefined): void {
const compressedTreeNode = this.compressedTreeNodeProvider.getCompressedTreeNode(node.element);
if (compressedTreeNode.element.elements.length === 1) {
templateData.compressedTreeNode = undefined;
this.renderer.renderElement(node, index, templateData.data, height);
} else {
templateData.compressedTreeNode = compressedTreeNode;
this.renderer.renderCompressedElements(compressedTreeNode, index, templateData.data, height);
}
}
disposeElement(node: ITreeNode<T, TFilterData>, index: number, templateData: CompressibleTemplateData<T, TFilterData, TTemplateData>, height: number | undefined): void {
if (templateData.compressedTreeNode) {
if (this.renderer.disposeCompressedElements) {
this.renderer.disposeCompressedElements(templateData.compressedTreeNode, index, templateData.data, height);
}
} else {
if (this.renderer.disposeElement) {
this.renderer.disposeElement(node, index, templateData.data, height);
}
}
}
disposeTemplate(templateData: CompressibleTemplateData<T, TFilterData, TTemplateData>): void {
this.renderer.disposeTemplate(templateData.data);
}
renderTwistie?(element: T, twistieElement: HTMLElement): void {
if (this.renderer.renderTwistie) {
this.renderer.renderTwistie(element, twistieElement);
}
}
}
export class CompressibleObjectTree<T extends NonNullable<any>, TFilterData = void> extends ObjectTree<T, TFilterData> {
protected model: CompressibleObjectTreeModel<T, TFilterData>;
constructor(
user: string,
container: HTMLElement,
delegate: IListVirtualDelegate<T>,
renderers: ICompressibleTreeRenderer<T, TFilterData, any>[],
options: IObjectTreeOptions<T, TFilterData> = {}
) {
const compressibleRenderers = renderers.map(r => new CompressibleRenderer(r));
super(user, container, delegate, compressibleRenderers, options);
compressibleRenderers.forEach(r => r.compressedTreeNodeProvider = this);
}
setChildren(element: T | null, children?: ISequence<ICompressedTreeElement<T>>): void {
this.model.setChildren(element, children);
}
protected createModel(user: string, view: ISpliceable<ITreeNode<T, TFilterData>>, options: ICompressibleObjectTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
return new CompressibleObjectTreeModel(user, view, options);
}
isCompressionEnabled(): boolean {
return this.model.isCompressionEnabled();
}
setCompressionEnabled(enabled: boolean): void {
this.model.setCompressionEnabled(enabled);
}
getCompressedTreeNode(element: T): ITreeNode<ICompressedTreeNode<T>, TFilterData> {
return this.model.getCompressedTreeNode(element)!;
}
}

View File

@@ -7,13 +7,13 @@ import { ISpliceable } from 'vs/base/common/sequence';
import { Iterator, ISequence, getSequenceIterator } from 'vs/base/common/iterator';
import { IndexTreeModel, IIndexTreeModelOptions } from 'vs/base/browser/ui/tree/indexTreeModel';
import { Event } from 'vs/base/common/event';
import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent, ITreeModelSpliceEvent } from 'vs/base/browser/ui/tree/tree';
import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError } from 'vs/base/browser/ui/tree/tree';
import { IIdentityProvider } from 'vs/base/browser/ui/list/list';
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: ISequence<ITreeElement<T>> | undefined): Iterator<ITreeElement<T>>;
setChildren(element: T | null, children: ISequence<ITreeElement<T>> | undefined): void;
resort(element?: T | null, recursive?: boolean): void;
}
@@ -38,8 +38,12 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
get size(): number { return this.nodes.size; }
constructor(list: ISpliceable<ITreeNode<T, TFilterData>>, options: IObjectTreeModelOptions<T, TFilterData> = {}) {
this.model = new IndexTreeModel(list, null, options);
constructor(
private user: string,
list: ISpliceable<ITreeNode<T, TFilterData>>,
options: IObjectTreeModelOptions<T, TFilterData> = {}
) {
this.model = new IndexTreeModel(user, list, null, options);
this.onDidSplice = this.model.onDidSplice;
this.onDidChangeCollapseState = this.model.onDidChangeCollapseState as Event<ICollapseStateChangeEvent<T, TFilterData>>;
this.onDidChangeRenderNodeCount = this.model.onDidChangeRenderNodeCount as Event<ITreeNode<T, TFilterData>>;
@@ -60,9 +64,9 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
children: ISequence<ITreeElement<T>> | undefined,
onDidCreateNode?: ITreeNodeCallback<T, TFilterData>,
onDidDeleteNode?: ITreeNodeCallback<T, TFilterData>
): Iterator<ITreeElement<T>> {
): void {
const location = this.getElementLocation(element);
return this._setChildren(location, this.preserveCollapseState(children), onDidCreateNode, onDidDeleteNode);
this._setChildren(location, this.preserveCollapseState(children), onDidCreateNode, onDidDeleteNode);
}
private _setChildren(
@@ -70,7 +74,7 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
children: ISequence<ITreeElement<T>> | undefined,
onDidCreateNode?: ITreeNodeCallback<T, TFilterData>,
onDidDeleteNode?: ITreeNodeCallback<T, TFilterData>
): Iterator<ITreeElement<T>> {
): void {
const insertedElements = new Set<T | null>();
const insertedElementIds = new Set<string>();
@@ -106,15 +110,13 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
}
};
const result = this.model.splice(
this.model.splice(
[...location, 0],
Number.MAX_VALUE,
children,
_onDidCreateNode,
_onDidDeleteNode
);
return result as Iterator<ITreeElement<T>>;
}
private preserveCollapseState(elements: ISequence<ITreeElement<T>> | undefined): ISequence<ITreeElement<T>> {
@@ -182,11 +184,6 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
}));
}
getParentElement(ref: T | null = null): T | null {
const location = this.getElementLocation(ref);
return this.model.getParentElement(location);
}
getFirstElementChild(ref: T | null = null): T | null | undefined {
const location = this.getElementLocation(ref);
return this.model.getFirstElementChild(location);
@@ -212,6 +209,11 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
return this.model.isCollapsible(location);
}
setCollapsible(element: T | null, collapsible?: boolean): boolean {
const location = this.getElementLocation(element);
return this.model.setCollapsible(location, collapsible);
}
isCollapsed(element: T | null): boolean {
const location = this.getElementLocation(element);
return this.model.isCollapsed(location);
@@ -239,7 +241,7 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
const node = this.nodes.get(element);
if (!node) {
throw new Error(`Tree element not found: ${element}`);
throw new TreeError(this.user, `Tree element not found: ${element}`);
}
return node;
@@ -251,16 +253,15 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
getParentNodeLocation(element: T | null): T | null {
if (element === null) {
throw new Error(`Invalid getParentNodeLocation call`);
throw new TreeError(this.user, `Invalid getParentNodeLocation call`);
}
const node = this.nodes.get(element);
const node = this.nodes.get(element)!;
const location = this.model.getNodeLocation(node);
const parentLocation = this.model.getParentNodeLocation(location);
const parent = this.model.getNode(parentLocation);
if (!node) {
throw new Error(`Tree element not found: ${element}`);
}
return node.parent!.element;
return parent.element;
}
private getElementLocation(element: T | null): number[] {
@@ -271,7 +272,7 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
const node = this.nodes.get(element);
if (!node) {
throw new Error(`Tree element not found: ${element}`);
throw new TreeError(this.user, `Tree element not found: ${element}`);
}
return this.model.getNodeLocation(node);

View File

@@ -81,7 +81,6 @@ export interface ITreeElement<T> {
export interface ITreeNode<T, TFilterData = void> {
readonly element: T;
readonly parent: ITreeNode<T, TFilterData> | undefined;
readonly children: ITreeNode<T, TFilterData>[];
readonly depth: number;
readonly visibleChildrenCount: number;
@@ -113,13 +112,13 @@ export interface ITreeModel<T, TFilterData, TRef> {
getListRenderCount(location: TRef): number;
getNode(location?: TRef): ITreeNode<T, any>;
getNodeLocation(node: ITreeNode<T, any>): TRef;
getParentNodeLocation(location: TRef): TRef;
getParentNodeLocation(location: TRef): TRef | undefined;
getParentElement(location: TRef): T;
getFirstElementChild(location: TRef): T | undefined;
getLastElementAncestor(location?: TRef): T | undefined;
isCollapsible(location: TRef): boolean;
setCollapsible(location: TRef, collapsible?: boolean): boolean;
isCollapsed(location: TRef): boolean;
setCollapsed(location: TRef, collapsed?: boolean, recursive?: boolean): boolean;
expandTo(location: TRef): void;
@@ -159,7 +158,6 @@ export interface ITreeContextMenuEvent<T> {
export interface ITreeNavigator<T> {
current(): T | null;
previous(): T | null;
parent(): T | null;
first(): T | null;
last(): T | null;
next(): T | null;
@@ -194,3 +192,28 @@ export const TreeDragOverReactions = {
export interface ITreeDragAndDrop<T> extends IListDragAndDrop<T> {
onDragOver(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction;
}
export class TreeError extends Error {
constructor(user: string, message: string) {
super(`TreeError [${user}] ${message}`);
}
}
export class WeakMapper<K extends object, V> {
constructor(private fn: (k: K) => V) { }
private _map = new WeakMap<K, V>();
map(key: K): V {
let result = this._map.get(key);
if (!result) {
result = this.fn(key);
this._map.set(key, result);
}
return result;
}
}