monaco-editor-core
Version:
A browser based code editor
372 lines (371 loc) • 14.8 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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);
}
}