UNPKG

monaco-editor-core

Version:

A browser based code editor

372 lines (371 loc) • 14.8 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ObjectTreeModel } from './objectTreeModel.js'; import { TreeError, WeakMapper } from './tree.js'; import { equals } from '../../../common/arrays.js'; import { Event } from '../../../common/event.js'; import { Iterable } from '../../../common/iterator.js'; function noCompress(element) { const elements = [element.element]; const incompressible = element.incompressible || false; return { element: { elements, incompressible }, children: Iterable.map(Iterable.from(element.children), noCompress), collapsible: element.collapsible, collapsed: element.collapsed }; } // Exported only for test reasons, do not use directly export function compress(element) { const elements = [element.element]; const incompressible = element.incompressible || false; let childrenIterator; let children; while (true) { [children, childrenIterator] = Iterable.consume(Iterable.from(element.children), 2); if (children.length !== 1) { break; } if (children[0].incompressible) { break; } element = children[0]; elements.push(element.element); } return { element: { elements, incompressible }, children: Iterable.map(Iterable.concat(children, childrenIterator), compress), collapsible: element.collapsible, collapsed: element.collapsed }; } function _decompress(element, index = 0) { let children; if (index < element.element.elements.length - 1) { children = [_decompress(element, index + 1)]; } else { children = Iterable.map(Iterable.from(element.children), el => _decompress(el, 0)); } if (index === 0 && element.element.incompressible) { return { element: element.element.elements[index], children, incompressible: true, collapsible: element.collapsible, collapsed: element.collapsed }; } return { element: element.element.elements[index], children, collapsible: element.collapsible, collapsed: element.collapsed }; } // Exported only for test reasons, do not use directly export function decompress(element) { return _decompress(element, 0); } function splice(treeElement, element, children) { if (treeElement.element === element) { return { ...treeElement, children }; } return { ...treeElement, children: Iterable.map(Iterable.from(treeElement.children), e => splice(e, element, children)) }; } const wrapIdentityProvider = (base) => ({ getId(node) { return node.elements.map(e => base.getId(e).toString()).join('\0'); } }); // Exported only for test reasons, do not use directly export class CompressedObjectTreeModel { get onDidSplice() { return this.model.onDidSplice; } get onDidChangeCollapseState() { return this.model.onDidChangeCollapseState; } get onDidChangeRenderNodeCount() { return this.model.onDidChangeRenderNodeCount; } constructor(user, list, options = {}) { this.user = user; this.rootRef = null; this.nodes = new Map(); this.model = new ObjectTreeModel(user, list, options); this.enabled = typeof options.compressionEnabled === 'undefined' ? true : options.compressionEnabled; this.identityProvider = options.identityProvider; } setChildren(element, children = Iterable.empty(), options) { // Diffs must be deep, since the compression can affect nested elements. // @see https://github.com/microsoft/vscode/pull/114237#issuecomment-759425034 const diffIdentityProvider = options.diffIdentityProvider && wrapIdentityProvider(options.diffIdentityProvider); if (element === null) { const compressedChildren = Iterable.map(children, this.enabled ? compress : noCompress); this._setChildren(null, compressedChildren, { diffIdentityProvider, diffDepth: Infinity }); return; } const compressedNode = this.nodes.get(element); if (!compressedNode) { throw new TreeError(this.user, 'Unknown compressed tree node'); } const node = this.model.getNode(compressedNode); const compressedParentNode = this.model.getParentNodeLocation(compressedNode); const parent = this.model.getNode(compressedParentNode); const decompressedElement = decompress(node); const splicedElement = splice(decompressedElement, element, children); const recompressedElement = (this.enabled ? compress : noCompress)(splicedElement); // If the recompressed node is identical to the original, just set its children. // Saves work and churn diffing the parent element. const elementComparator = options.diffIdentityProvider ? ((a, b) => options.diffIdentityProvider.getId(a) === options.diffIdentityProvider.getId(b)) : undefined; if (equals(recompressedElement.element.elements, node.element.elements, elementComparator)) { this._setChildren(compressedNode, recompressedElement.children || Iterable.empty(), { diffIdentityProvider, diffDepth: 1 }); return; } const parentChildren = parent.children .map(child => child === node ? recompressedElement : child); this._setChildren(parent.element, parentChildren, { diffIdentityProvider, diffDepth: node.depth - parent.depth, }); } isCompressionEnabled() { return this.enabled; } setCompressionEnabled(enabled) { if (enabled === this.enabled) { return; } this.enabled = enabled; const root = this.model.getNode(); const rootChildren = root.children; const decompressedRootChildren = Iterable.map(rootChildren, decompress); const recompressedRootChildren = Iterable.map(decompressedRootChildren, enabled ? compress : noCompress); // it should be safe to always use deep diff mode here if an identity // provider is available, since we know the raw nodes are unchanged. this._setChildren(null, recompressedRootChildren, { diffIdentityProvider: this.identityProvider, diffDepth: Infinity, }); } _setChildren(node, children, options) { const insertedElements = new Set(); const onDidCreateNode = (node) => { for (const element of node.element.elements) { insertedElements.add(element); this.nodes.set(element, node.element); } }; const onDidDeleteNode = (node) => { for (const element of node.element.elements) { if (!insertedElements.has(element)) { this.nodes.delete(element); } } }; this.model.setChildren(node, children, { ...options, onDidCreateNode, onDidDeleteNode }); } has(element) { return this.nodes.has(element); } getListIndex(location) { const node = this.getCompressedNode(location); return this.model.getListIndex(node); } getListRenderCount(location) { const node = this.getCompressedNode(location); return this.model.getListRenderCount(node); } getNode(location) { if (typeof location === 'undefined') { return this.model.getNode(); } const node = this.getCompressedNode(location); return this.model.getNode(node); } // TODO: review this getNodeLocation(node) { const compressedNode = this.model.getNodeLocation(node); if (compressedNode === null) { return null; } return compressedNode.elements[compressedNode.elements.length - 1]; } // TODO: review this getParentNodeLocation(location) { const compressedNode = this.getCompressedNode(location); const parentNode = this.model.getParentNodeLocation(compressedNode); if (parentNode === null) { return null; } return parentNode.elements[parentNode.elements.length - 1]; } getFirstElementChild(location) { const compressedNode = this.getCompressedNode(location); return this.model.getFirstElementChild(compressedNode); } isCollapsible(location) { const compressedNode = this.getCompressedNode(location); return this.model.isCollapsible(compressedNode); } setCollapsible(location, collapsible) { const compressedNode = this.getCompressedNode(location); return this.model.setCollapsible(compressedNode, collapsible); } isCollapsed(location) { const compressedNode = this.getCompressedNode(location); return this.model.isCollapsed(compressedNode); } setCollapsed(location, collapsed, recursive) { const compressedNode = this.getCompressedNode(location); return this.model.setCollapsed(compressedNode, collapsed, recursive); } expandTo(location) { const compressedNode = this.getCompressedNode(location); this.model.expandTo(compressedNode); } rerender(location) { const compressedNode = this.getCompressedNode(location); this.model.rerender(compressedNode); } refilter() { this.model.refilter(); } getCompressedNode(element) { if (element === null) { return null; } const node = this.nodes.get(element); if (!node) { throw new TreeError(this.user, `Tree element not found: ${element}`); } return node; } } export const DefaultElementMapper = elements => elements[elements.length - 1]; class CompressedTreeNodeWrapper { get element() { return this.node.element === null ? null : this.unwrapper(this.node.element); } get children() { return this.node.children.map(node => new CompressedTreeNodeWrapper(this.unwrapper, node)); } get depth() { return this.node.depth; } get visibleChildrenCount() { return this.node.visibleChildrenCount; } get visibleChildIndex() { return this.node.visibleChildIndex; } get collapsible() { return this.node.collapsible; } get collapsed() { return this.node.collapsed; } get visible() { return this.node.visible; } get filterData() { return this.node.filterData; } constructor(unwrapper, node) { this.unwrapper = unwrapper; this.node = node; } } function mapList(nodeMapper, list) { return { splice(start, deleteCount, toInsert) { list.splice(start, deleteCount, toInsert.map(node => nodeMapper.map(node))); }, updateElementHeight(index, height) { list.updateElementHeight(index, height); } }; } function mapOptions(compressedNodeUnwrapper, options) { return { ...options, identityProvider: options.identityProvider && { getId(node) { return options.identityProvider.getId(compressedNodeUnwrapper(node)); } }, sorter: options.sorter && { compare(node, otherNode) { return options.sorter.compare(node.elements[0], otherNode.elements[0]); } }, filter: options.filter && { filter(node, parentVisibility) { return options.filter.filter(compressedNodeUnwrapper(node), parentVisibility); } } }; } export class CompressibleObjectTreeModel { get onDidSplice() { return Event.map(this.model.onDidSplice, ({ insertedNodes, deletedNodes }) => ({ insertedNodes: insertedNodes.map(node => this.nodeMapper.map(node)), deletedNodes: deletedNodes.map(node => this.nodeMapper.map(node)), })); } get onDidChangeCollapseState() { return Event.map(this.model.onDidChangeCollapseState, ({ node, deep }) => ({ node: this.nodeMapper.map(node), deep })); } get onDidChangeRenderNodeCount() { return Event.map(this.model.onDidChangeRenderNodeCount, node => this.nodeMapper.map(node)); } constructor(user, list, options = {}) { this.rootRef = null; this.elementMapper = options.elementMapper || DefaultElementMapper; const compressedNodeUnwrapper = 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, children = Iterable.empty(), options = {}) { this.model.setChildren(element, children, options); } isCompressionEnabled() { return this.model.isCompressionEnabled(); } setCompressionEnabled(enabled) { this.model.setCompressionEnabled(enabled); } has(location) { return this.model.has(location); } getListIndex(location) { return this.model.getListIndex(location); } getListRenderCount(location) { return this.model.getListRenderCount(location); } getNode(location) { return this.nodeMapper.map(this.model.getNode(location)); } getNodeLocation(node) { return node.element; } getParentNodeLocation(location) { return this.model.getParentNodeLocation(location); } getFirstElementChild(location) { const result = this.model.getFirstElementChild(location); if (result === null || typeof result === 'undefined') { return result; } return this.elementMapper(result.elements); } isCollapsible(location) { return this.model.isCollapsible(location); } setCollapsible(location, collapsed) { return this.model.setCollapsible(location, collapsed); } isCollapsed(location) { return this.model.isCollapsed(location); } setCollapsed(location, collapsed, recursive) { return this.model.setCollapsed(location, collapsed, recursive); } expandTo(location) { return this.model.expandTo(location); } rerender(location) { return this.model.rerender(location); } refilter() { return this.model.refilter(); } getCompressedTreeNode(location = null) { return this.model.getNode(location); } }