UNPKG

monaco-editor-core

Version:

A browser based code editor

1,113 lines • 95 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { $, append, clearNode, createStyleSheet, getWindow, h, hasParentWithClass, asCssValueWithDefault, isKeyboardEvent, addDisposableListener } from '../../dom.js'; import { DomEmitter } from '../../event.js'; import { StandardKeyboardEvent } from '../../keyboardEvent.js'; import { ActionBar } from '../actionbar/actionbar.js'; import { FindInput } from '../findinput/findInput.js'; import { unthemedInboxStyles } from '../inputbox/inputBox.js'; import { ElementsDragAndDropData } from '../list/listView.js'; import { isActionItem, isButton, isInputElement, isMonacoCustomToggle, isMonacoEditor, isStickyScrollContainer, isStickyScrollElement, List, MouseController } from '../list/listWidget.js'; import { Toggle, unthemedToggleStyles } from '../toggle/toggle.js'; import { getVisibleState, isFilterResult } from './indexTreeModel.js'; import { TreeMouseEventTarget } from './tree.js'; import { Action } from '../../../common/actions.js'; import { distinct, equals, range } from '../../../common/arrays.js'; import { Delayer, disposableTimeout, timeout } from '../../../common/async.js'; import { Codicon } from '../../../common/codicons.js'; import { ThemeIcon } from '../../../common/themables.js'; import { SetMap } from '../../../common/map.js'; import { Emitter, Event, EventBufferer, Relay } from '../../../common/event.js'; import { fuzzyScore, FuzzyScore } from '../../../common/filters.js'; import { Disposable, DisposableStore, dispose, toDisposable } from '../../../common/lifecycle.js'; import { clamp } from '../../../common/numbers.js'; import { isNumber } from '../../../common/types.js'; import './media/tree.css'; import { localize } from '../../../../nls.js'; import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js'; import { autorun, constObservable } from '../../../common/observable.js'; import { alert } from '../aria/aria.js'; class TreeElementsDragAndDropData extends ElementsDragAndDropData { constructor(data) { super(data.elements.map(node => node.element)); this.data = data; } } function asTreeDragAndDropData(data) { if (data instanceof ElementsDragAndDropData) { return new TreeElementsDragAndDropData(data); } return data; } class TreeNodeListDragAndDrop { constructor(modelProvider, dnd) { this.modelProvider = modelProvider; this.dnd = dnd; this.autoExpandDisposable = Disposable.None; this.disposables = new DisposableStore(); } getDragURI(node) { return this.dnd.getDragURI(node.element); } getDragLabel(nodes, originalEvent) { if (this.dnd.getDragLabel) { return this.dnd.getDragLabel(nodes.map(node => node.element), originalEvent); } return undefined; } onDragStart(data, originalEvent) { this.dnd.onDragStart?.(asTreeDragAndDropData(data), originalEvent); } onDragOver(data, targetNode, targetIndex, targetSector, originalEvent, raw = true) { const result = this.dnd.onDragOver(asTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, targetSector, originalEvent); const didChangeAutoExpandNode = this.autoExpandNode !== targetNode; if (didChangeAutoExpandNode) { this.autoExpandDisposable.dispose(); this.autoExpandNode = targetNode; } if (typeof targetNode === 'undefined') { return result; } if (didChangeAutoExpandNode && typeof result !== 'boolean' && result.autoExpand) { this.autoExpandDisposable = disposableTimeout(() => { const model = this.modelProvider(); const ref = model.getNodeLocation(targetNode); if (model.isCollapsed(ref)) { model.setCollapsed(ref, false); } this.autoExpandNode = undefined; }, 500, this.disposables); } if (typeof result === 'boolean' || !result.accept || typeof result.bubble === 'undefined' || result.feedback) { if (!raw) { const accept = typeof result === 'boolean' ? result : result.accept; const effect = typeof result === 'boolean' ? undefined : result.effect; return { accept, effect, feedback: [targetIndex] }; } return result; } if (result.bubble === 1 /* TreeDragOverBubble.Up */) { const model = this.modelProvider(); 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, targetSector, originalEvent, false); } const model = this.modelProvider(); const ref = model.getNodeLocation(targetNode); const start = model.getListIndex(ref); const length = model.getListRenderCount(ref); return { ...result, feedback: range(start, start + length) }; } drop(data, targetNode, targetIndex, targetSector, originalEvent) { this.autoExpandDisposable.dispose(); this.autoExpandNode = undefined; this.dnd.drop(asTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, targetSector, originalEvent); } onDragEnd(originalEvent) { this.dnd.onDragEnd?.(originalEvent); } dispose() { this.disposables.dispose(); this.dnd.dispose(); } } function asListOptions(modelProvider, options) { return options && { ...options, identityProvider: options.identityProvider && { getId(el) { return options.identityProvider.getId(el.element); } }, dnd: options.dnd && new TreeNodeListDragAndDrop(modelProvider, options.dnd), multipleSelectionController: options.multipleSelectionController && { isSelectionSingleChangeEvent(e) { return options.multipleSelectionController.isSelectionSingleChangeEvent({ ...e, element: e.element }); }, isSelectionRangeChangeEvent(e) { return options.multipleSelectionController.isSelectionRangeChangeEvent({ ...e, element: e.element }); } }, accessibilityProvider: options.accessibilityProvider && { ...options.accessibilityProvider, getSetSize(node) { 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; }, isChecked: options.accessibilityProvider && options.accessibilityProvider.isChecked ? (node) => { return options.accessibilityProvider.isChecked(node.element); } : undefined, getRole: options.accessibilityProvider && options.accessibilityProvider.getRole ? (node) => { return options.accessibilityProvider.getRole(node.element); } : () => 'treeitem', getAriaLabel(e) { return options.accessibilityProvider.getAriaLabel(e.element); }, getWidgetAriaLabel() { return options.accessibilityProvider.getWidgetAriaLabel(); }, getWidgetRole: options.accessibilityProvider && options.accessibilityProvider.getWidgetRole ? () => options.accessibilityProvider.getWidgetRole() : () => 'tree', getAriaLevel: options.accessibilityProvider && options.accessibilityProvider.getAriaLevel ? (node) => options.accessibilityProvider.getAriaLevel(node.element) : (node) => { return node.depth; }, getActiveDescendantId: options.accessibilityProvider.getActiveDescendantId && (node => { return options.accessibilityProvider.getActiveDescendantId(node.element); }) }, keyboardNavigationLabelProvider: options.keyboardNavigationLabelProvider && { ...options.keyboardNavigationLabelProvider, getKeyboardNavigationLabel(node) { return options.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(node.element); } } }; } export class ComposedTreeDelegate { constructor(delegate) { this.delegate = delegate; } getHeight(element) { return this.delegate.getHeight(element.element); } getTemplateId(element) { return this.delegate.getTemplateId(element.element); } hasDynamicHeight(element) { return !!this.delegate.hasDynamicHeight && this.delegate.hasDynamicHeight(element.element); } setDynamicHeight(element, height) { this.delegate.setDynamicHeight?.(element.element, height); } } export var RenderIndentGuides; (function (RenderIndentGuides) { RenderIndentGuides["None"] = "none"; RenderIndentGuides["OnHover"] = "onHover"; RenderIndentGuides["Always"] = "always"; })(RenderIndentGuides || (RenderIndentGuides = {})); class EventCollection { get elements() { return this._elements; } constructor(onDidChange, _elements = []) { this._elements = _elements; this.disposables = new DisposableStore(); this.onDidChange = Event.forEach(onDidChange, elements => this._elements = elements, this.disposables); } dispose() { this.disposables.dispose(); } } export class TreeRenderer { static { this.DefaultIndent = 8; } constructor(renderer, modelProvider, onDidChangeCollapseState, activeNodes, renderedIndentGuides, options = {}) { this.renderer = renderer; this.modelProvider = modelProvider; this.activeNodes = activeNodes; this.renderedIndentGuides = renderedIndentGuides; this.renderedElements = new Map(); this.renderedNodes = new Map(); this.indent = TreeRenderer.DefaultIndent; this.hideTwistiesOfChildlessElements = false; this.shouldRenderIndentGuides = false; this.activeIndentNodes = new Set(); this.indentGuidesDisposable = Disposable.None; this.disposables = new DisposableStore(); this.templateId = renderer.templateId; this.updateOptions(options); Event.map(onDidChangeCollapseState, e => e.node)(this.onDidChangeNodeTwistieState, this, this.disposables); renderer.onDidChangeTwistieState?.(this.onDidChangeTwistieState, this, this.disposables); } updateOptions(options = {}) { if (typeof options.indent !== 'undefined') { const indent = clamp(options.indent, 0, 40); if (indent !== this.indent) { this.indent = indent; for (const [node, templateData] of this.renderedNodes) { this.renderTreeElement(node, templateData); } } } if (typeof options.renderIndentGuides !== 'undefined') { const shouldRenderIndentGuides = options.renderIndentGuides !== RenderIndentGuides.None; if (shouldRenderIndentGuides !== this.shouldRenderIndentGuides) { this.shouldRenderIndentGuides = shouldRenderIndentGuides; for (const [node, templateData] of this.renderedNodes) { this._renderIndentGuides(node, templateData); } this.indentGuidesDisposable.dispose(); if (shouldRenderIndentGuides) { const disposables = new DisposableStore(); this.activeNodes.onDidChange(this._onDidChangeActiveNodes, this, disposables); this.indentGuidesDisposable = disposables; this._onDidChangeActiveNodes(this.activeNodes.elements); } } } if (typeof options.hideTwistiesOfChildlessElements !== 'undefined') { this.hideTwistiesOfChildlessElements = options.hideTwistiesOfChildlessElements; } } renderTemplate(container) { const el = append(container, $('.monaco-tl-row')); const indent = append(el, $('.monaco-tl-indent')); const twistie = append(el, $('.monaco-tl-twistie')); const contents = append(el, $('.monaco-tl-contents')); const templateData = this.renderer.renderTemplate(contents); return { container, indent, twistie, indentGuidesDisposable: Disposable.None, templateData }; } renderElement(node, index, templateData, height) { this.renderedNodes.set(node, templateData); this.renderedElements.set(node.element, node); this.renderTreeElement(node, templateData); this.renderer.renderElement(node, index, templateData.templateData, height); } disposeElement(node, index, templateData, height) { templateData.indentGuidesDisposable.dispose(); this.renderer.disposeElement?.(node, index, templateData.templateData, height); if (typeof height === 'number') { this.renderedNodes.delete(node); this.renderedElements.delete(node.element); } } disposeTemplate(templateData) { this.renderer.disposeTemplate(templateData.templateData); } onDidChangeTwistieState(element) { const node = this.renderedElements.get(element); if (!node) { return; } this.onDidChangeNodeTwistieState(node); } onDidChangeNodeTwistieState(node) { const templateData = this.renderedNodes.get(node); if (!templateData) { return; } this._onDidChangeActiveNodes(this.activeNodes.elements); this.renderTreeElement(node, templateData); } renderTreeElement(node, templateData) { const indent = TreeRenderer.DefaultIndent + (node.depth - 1) * this.indent; templateData.twistie.style.paddingLeft = `${indent}px`; templateData.indent.style.width = `${indent + this.indent - 16}px`; if (node.collapsible) { templateData.container.setAttribute('aria-expanded', String(!node.collapsed)); } else { templateData.container.removeAttribute('aria-expanded'); } templateData.twistie.classList.remove(...ThemeIcon.asClassNameArray(Codicon.treeItemExpanded)); let twistieRendered = false; if (this.renderer.renderTwistie) { twistieRendered = this.renderer.renderTwistie(node.element, templateData.twistie); } if (node.collapsible && (!this.hideTwistiesOfChildlessElements || node.visibleChildrenCount > 0)) { if (!twistieRendered) { templateData.twistie.classList.add(...ThemeIcon.asClassNameArray(Codicon.treeItemExpanded)); } templateData.twistie.classList.add('collapsible'); templateData.twistie.classList.toggle('collapsed', node.collapsed); } else { templateData.twistie.classList.remove('collapsible', 'collapsed'); } this._renderIndentGuides(node, templateData); } _renderIndentGuides(node, templateData) { clearNode(templateData.indent); templateData.indentGuidesDisposable.dispose(); if (!this.shouldRenderIndentGuides) { return; } const disposableStore = new DisposableStore(); const model = this.modelProvider(); while (true) { const ref = model.getNodeLocation(node); const parentRef = model.getParentNodeLocation(ref); if (!parentRef) { break; } const parent = model.getNode(parentRef); const guide = $('.indent-guide', { style: `width: ${this.indent}px` }); if (this.activeIndentNodes.has(parent)) { guide.classList.add('active'); } if (templateData.indent.childElementCount === 0) { templateData.indent.appendChild(guide); } else { templateData.indent.insertBefore(guide, templateData.indent.firstElementChild); } this.renderedIndentGuides.add(parent, guide); disposableStore.add(toDisposable(() => this.renderedIndentGuides.delete(parent, guide))); node = parent; } templateData.indentGuidesDisposable = disposableStore; } _onDidChangeActiveNodes(nodes) { if (!this.shouldRenderIndentGuides) { return; } const set = new Set(); const model = this.modelProvider(); nodes.forEach(node => { const ref = model.getNodeLocation(node); try { const parentRef = model.getParentNodeLocation(ref); if (node.collapsible && node.children.length > 0 && !node.collapsed) { set.add(node); } else if (parentRef) { set.add(model.getNode(parentRef)); } } catch { // noop } }); this.activeIndentNodes.forEach(node => { if (!set.has(node)) { this.renderedIndentGuides.forEach(node, line => line.classList.remove('active')); } }); set.forEach(node => { if (!this.activeIndentNodes.has(node)) { this.renderedIndentGuides.forEach(node, line => line.classList.add('active')); } }); this.activeIndentNodes = set; } dispose() { this.renderedNodes.clear(); this.renderedElements.clear(); this.indentGuidesDisposable.dispose(); dispose(this.disposables); } } class FindFilter { get totalCount() { return this._totalCount; } get matchCount() { return this._matchCount; } constructor(tree, keyboardNavigationLabelProvider, _filter) { this.tree = tree; this.keyboardNavigationLabelProvider = keyboardNavigationLabelProvider; this._filter = _filter; this._totalCount = 0; this._matchCount = 0; this._pattern = ''; this._lowercasePattern = ''; this.disposables = new DisposableStore(); tree.onWillRefilter(this.reset, this, this.disposables); } filter(element, parentVisibility) { let visibility = 1 /* TreeVisibility.Visible */; if (this._filter) { const result = this._filter.filter(element, parentVisibility); if (typeof result === 'boolean') { visibility = result ? 1 /* TreeVisibility.Visible */ : 0 /* TreeVisibility.Hidden */; } else if (isFilterResult(result)) { visibility = getVisibleState(result.visibility); } else { visibility = result; } if (visibility === 0 /* TreeVisibility.Hidden */) { return false; } } this._totalCount++; if (!this._pattern) { this._matchCount++; return { data: FuzzyScore.Default, visibility }; } const label = this.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(element); const labels = Array.isArray(label) ? label : [label]; for (const l of labels) { const labelStr = l && l.toString(); if (typeof labelStr === 'undefined') { return { data: FuzzyScore.Default, visibility }; } let score; if (this.tree.findMatchType === TreeFindMatchType.Contiguous) { const index = labelStr.toLowerCase().indexOf(this._lowercasePattern); if (index > -1) { score = [Number.MAX_SAFE_INTEGER, 0]; for (let i = this._lowercasePattern.length; i > 0; i--) { score.push(index + i - 1); } } } else { score = fuzzyScore(this._pattern, this._lowercasePattern, 0, labelStr, labelStr.toLowerCase(), 0, { firstMatchCanBeWeak: true, boostFullMatch: true }); } if (score) { this._matchCount++; return labels.length === 1 ? { data: score, visibility } : { data: { label: labelStr, score: score }, visibility }; } } if (this.tree.findMode === TreeFindMode.Filter) { if (typeof this.tree.options.defaultFindVisibility === 'number') { return this.tree.options.defaultFindVisibility; } else if (this.tree.options.defaultFindVisibility) { return this.tree.options.defaultFindVisibility(element); } else { return 2 /* TreeVisibility.Recurse */; } } else { return { data: FuzzyScore.Default, visibility }; } } reset() { this._totalCount = 0; this._matchCount = 0; } dispose() { dispose(this.disposables); } } export class ModeToggle extends Toggle { constructor(opts) { super({ icon: Codicon.listFilter, title: localize('filter', "Filter"), isChecked: opts.isChecked ?? false, hoverDelegate: opts.hoverDelegate ?? getDefaultHoverDelegate('element'), inputActiveOptionBorder: opts.inputActiveOptionBorder, inputActiveOptionForeground: opts.inputActiveOptionForeground, inputActiveOptionBackground: opts.inputActiveOptionBackground }); } } export class FuzzyToggle extends Toggle { constructor(opts) { super({ icon: Codicon.searchFuzzy, title: localize('fuzzySearch', "Fuzzy Match"), isChecked: opts.isChecked ?? false, hoverDelegate: opts.hoverDelegate ?? getDefaultHoverDelegate('element'), inputActiveOptionBorder: opts.inputActiveOptionBorder, inputActiveOptionForeground: opts.inputActiveOptionForeground, inputActiveOptionBackground: opts.inputActiveOptionBackground }); } } const unthemedFindWidgetStyles = { inputBoxStyles: unthemedInboxStyles, toggleStyles: unthemedToggleStyles, listFilterWidgetBackground: undefined, listFilterWidgetNoMatchesOutline: undefined, listFilterWidgetOutline: undefined, listFilterWidgetShadow: undefined }; export var TreeFindMode; (function (TreeFindMode) { TreeFindMode[TreeFindMode["Highlight"] = 0] = "Highlight"; TreeFindMode[TreeFindMode["Filter"] = 1] = "Filter"; })(TreeFindMode || (TreeFindMode = {})); export var TreeFindMatchType; (function (TreeFindMatchType) { TreeFindMatchType[TreeFindMatchType["Fuzzy"] = 0] = "Fuzzy"; TreeFindMatchType[TreeFindMatchType["Contiguous"] = 1] = "Contiguous"; })(TreeFindMatchType || (TreeFindMatchType = {})); class FindWidget extends Disposable { set mode(mode) { this.modeToggle.checked = mode === TreeFindMode.Filter; this.findInput.inputBox.setPlaceHolder(mode === TreeFindMode.Filter ? localize('type to filter', "Type to filter") : localize('type to search', "Type to search")); } set matchType(matchType) { this.matchTypeToggle.checked = matchType === TreeFindMatchType.Fuzzy; } constructor(container, tree, contextViewProvider, mode, matchType, options) { super(); this.tree = tree; this.elements = h('.monaco-tree-type-filter', [ h('.monaco-tree-type-filter-grab.codicon.codicon-debug-gripper@grab', { tabIndex: 0 }), h('.monaco-tree-type-filter-input@findInput'), h('.monaco-tree-type-filter-actionbar@actionbar'), ]); this.width = 0; this.right = 0; this.top = 0; this._onDidDisable = new Emitter(); container.appendChild(this.elements.root); this._register(toDisposable(() => this.elements.root.remove())); const styles = options?.styles ?? unthemedFindWidgetStyles; if (styles.listFilterWidgetBackground) { this.elements.root.style.backgroundColor = styles.listFilterWidgetBackground; } if (styles.listFilterWidgetShadow) { this.elements.root.style.boxShadow = `0 0 8px 2px ${styles.listFilterWidgetShadow}`; } const toggleHoverDelegate = this._register(createInstantHoverDelegate()); this.modeToggle = this._register(new ModeToggle({ ...styles.toggleStyles, isChecked: mode === TreeFindMode.Filter, hoverDelegate: toggleHoverDelegate })); this.matchTypeToggle = this._register(new FuzzyToggle({ ...styles.toggleStyles, isChecked: matchType === TreeFindMatchType.Fuzzy, hoverDelegate: toggleHoverDelegate })); this.onDidChangeMode = Event.map(this.modeToggle.onChange, () => this.modeToggle.checked ? TreeFindMode.Filter : TreeFindMode.Highlight, this._store); this.onDidChangeMatchType = Event.map(this.matchTypeToggle.onChange, () => this.matchTypeToggle.checked ? TreeFindMatchType.Fuzzy : TreeFindMatchType.Contiguous, this._store); this.findInput = this._register(new FindInput(this.elements.findInput, contextViewProvider, { label: localize('type to search', "Type to search"), additionalToggles: [this.modeToggle, this.matchTypeToggle], showCommonFindToggles: false, inputBoxStyles: styles.inputBoxStyles, toggleStyles: styles.toggleStyles, history: options?.history })); this.actionbar = this._register(new ActionBar(this.elements.actionbar)); this.mode = mode; const emitter = this._register(new DomEmitter(this.findInput.inputBox.inputElement, 'keydown')); const onKeyDown = Event.chain(emitter.event, $ => $.map(e => new StandardKeyboardEvent(e))); this._register(onKeyDown((e) => { // Using equals() so we reserve modified keys for future use if (e.equals(3 /* KeyCode.Enter */)) { // This is the only keyboard way to return to the tree from a history item that isn't the last one e.preventDefault(); e.stopPropagation(); this.findInput.inputBox.addToHistory(); this.tree.domFocus(); return; } if (e.equals(18 /* KeyCode.DownArrow */)) { e.preventDefault(); e.stopPropagation(); if (this.findInput.inputBox.isAtLastInHistory() || this.findInput.inputBox.isNowhereInHistory()) { // Retain original pre-history DownArrow behavior this.findInput.inputBox.addToHistory(); this.tree.domFocus(); } else { // Downward through history this.findInput.inputBox.showNextValue(); } return; } if (e.equals(16 /* KeyCode.UpArrow */)) { e.preventDefault(); e.stopPropagation(); // Upward through history this.findInput.inputBox.showPreviousValue(); return; } })); const closeAction = this._register(new Action('close', localize('close', "Close"), 'codicon codicon-close', true, () => this.dispose())); this.actionbar.push(closeAction, { icon: true, label: false }); const onGrabMouseDown = this._register(new DomEmitter(this.elements.grab, 'mousedown')); this._register(onGrabMouseDown.event(e => { const disposables = new DisposableStore(); const onWindowMouseMove = disposables.add(new DomEmitter(getWindow(e), 'mousemove')); const onWindowMouseUp = disposables.add(new DomEmitter(getWindow(e), 'mouseup')); const startRight = this.right; const startX = e.pageX; const startTop = this.top; const startY = e.pageY; this.elements.grab.classList.add('grabbing'); const transition = this.elements.root.style.transition; this.elements.root.style.transition = 'unset'; const update = (e) => { const deltaX = e.pageX - startX; this.right = startRight - deltaX; const deltaY = e.pageY - startY; this.top = startTop + deltaY; this.layout(); }; disposables.add(onWindowMouseMove.event(update)); disposables.add(onWindowMouseUp.event(e => { update(e); this.elements.grab.classList.remove('grabbing'); this.elements.root.style.transition = transition; disposables.dispose(); })); })); const onGrabKeyDown = Event.chain(this._register(new DomEmitter(this.elements.grab, 'keydown')).event, $ => $.map(e => new StandardKeyboardEvent(e))); this._register(onGrabKeyDown((e) => { let right; let top; if (e.keyCode === 15 /* KeyCode.LeftArrow */) { right = Number.POSITIVE_INFINITY; } else if (e.keyCode === 17 /* KeyCode.RightArrow */) { right = 0; } else if (e.keyCode === 10 /* KeyCode.Space */) { right = this.right === 0 ? Number.POSITIVE_INFINITY : 0; } if (e.keyCode === 16 /* KeyCode.UpArrow */) { top = 0; } else if (e.keyCode === 18 /* KeyCode.DownArrow */) { top = Number.POSITIVE_INFINITY; } if (right !== undefined) { e.preventDefault(); e.stopPropagation(); this.right = right; this.layout(); } if (top !== undefined) { e.preventDefault(); e.stopPropagation(); this.top = top; const transition = this.elements.root.style.transition; this.elements.root.style.transition = 'unset'; this.layout(); setTimeout(() => { this.elements.root.style.transition = transition; }, 0); } })); this.onDidChangeValue = this.findInput.onDidChange; } layout(width = this.width) { this.width = width; this.right = clamp(this.right, 0, Math.max(0, width - 212)); this.elements.root.style.right = `${this.right}px`; this.top = clamp(this.top, 0, 24); this.elements.root.style.top = `${this.top}px`; } showMessage(message) { this.findInput.showMessage(message); } clearMessage() { this.findInput.clearMessage(); } async dispose() { this._onDidDisable.fire(); this.elements.root.classList.add('disabled'); await timeout(300); super.dispose(); } } class FindController { get pattern() { return this._pattern; } get mode() { return this._mode; } set mode(mode) { if (mode === this._mode) { return; } this._mode = mode; if (this.widget) { this.widget.mode = this._mode; } this.tree.refilter(); this.render(); this._onDidChangeMode.fire(mode); } get matchType() { return this._matchType; } set matchType(matchType) { if (matchType === this._matchType) { return; } this._matchType = matchType; if (this.widget) { this.widget.matchType = this._matchType; } this.tree.refilter(); this.render(); this._onDidChangeMatchType.fire(matchType); } constructor(tree, model, view, filter, contextViewProvider, options = {}) { this.tree = tree; this.view = view; this.filter = filter; this.contextViewProvider = contextViewProvider; this.options = options; this._pattern = ''; this.width = 0; this._onDidChangeMode = new Emitter(); this.onDidChangeMode = this._onDidChangeMode.event; this._onDidChangeMatchType = new Emitter(); this.onDidChangeMatchType = this._onDidChangeMatchType.event; this._onDidChangePattern = new Emitter(); this._onDidChangeOpenState = new Emitter(); this.onDidChangeOpenState = this._onDidChangeOpenState.event; this.enabledDisposables = new DisposableStore(); this.disposables = new DisposableStore(); this._mode = tree.options.defaultFindMode ?? TreeFindMode.Highlight; this._matchType = tree.options.defaultFindMatchType ?? TreeFindMatchType.Fuzzy; model.onDidSplice(this.onDidSpliceModel, this, this.disposables); } updateOptions(optionsUpdate = {}) { if (optionsUpdate.defaultFindMode !== undefined) { this.mode = optionsUpdate.defaultFindMode; } if (optionsUpdate.defaultFindMatchType !== undefined) { this.matchType = optionsUpdate.defaultFindMatchType; } } onDidSpliceModel() { if (!this.widget || this.pattern.length === 0) { return; } this.tree.refilter(); this.render(); } render() { const noMatches = this.filter.totalCount > 0 && this.filter.matchCount === 0; if (this.pattern && noMatches) { alert(localize('replFindNoResults', "No results")); if (this.tree.options.showNotFoundMessage ?? true) { this.widget?.showMessage({ type: 2 /* MessageType.WARNING */, content: localize('not found', "No elements found.") }); } else { this.widget?.showMessage({ type: 2 /* MessageType.WARNING */ }); } } else { this.widget?.clearMessage(); if (this.pattern) { alert(localize('replFindResults', "{0} results", this.filter.matchCount)); } } } shouldAllowFocus(node) { if (!this.widget || !this.pattern) { return true; } if (this.filter.totalCount > 0 && this.filter.matchCount <= 1) { return true; } return !FuzzyScore.isDefault(node.filterData); } layout(width) { this.width = width; this.widget?.layout(width); } dispose() { this._history = undefined; this._onDidChangePattern.dispose(); this.enabledDisposables.dispose(); this.disposables.dispose(); } } function stickyScrollNodeStateEquals(node1, node2) { return node1.position === node2.position && stickyScrollNodeEquals(node1, node2); } function stickyScrollNodeEquals(node1, node2) { return node1.node.element === node2.node.element && node1.startIndex === node2.startIndex && node1.height === node2.height && node1.endIndex === node2.endIndex; } class StickyScrollState { constructor(stickyNodes = []) { this.stickyNodes = stickyNodes; } get count() { return this.stickyNodes.length; } equal(state) { return equals(this.stickyNodes, state.stickyNodes, stickyScrollNodeStateEquals); } lastNodePartiallyVisible() { if (this.count === 0) { return false; } const lastStickyNode = this.stickyNodes[this.count - 1]; if (this.count === 1) { return lastStickyNode.position !== 0; } const secondLastStickyNode = this.stickyNodes[this.count - 2]; return secondLastStickyNode.position + secondLastStickyNode.height !== lastStickyNode.position; } animationStateChanged(previousState) { if (!equals(this.stickyNodes, previousState.stickyNodes, stickyScrollNodeEquals)) { return false; } if (this.count === 0) { return false; } const lastStickyNode = this.stickyNodes[this.count - 1]; const previousLastStickyNode = previousState.stickyNodes[previousState.count - 1]; return lastStickyNode.position !== previousLastStickyNode.position; } } class DefaultStickyScrollDelegate { constrainStickyScrollNodes(stickyNodes, stickyScrollMaxItemCount, maxWidgetHeight) { for (let i = 0; i < stickyNodes.length; i++) { const stickyNode = stickyNodes[i]; const stickyNodeBottom = stickyNode.position + stickyNode.height; if (stickyNodeBottom > maxWidgetHeight || i >= stickyScrollMaxItemCount) { return stickyNodes.slice(0, i); } } return stickyNodes; } } class StickyScrollController extends Disposable { constructor(tree, model, view, renderers, treeDelegate, options = {}) { super(); this.tree = tree; this.model = model; this.view = view; this.treeDelegate = treeDelegate; this.maxWidgetViewRatio = 0.4; const stickyScrollOptions = this.validateStickySettings(options); this.stickyScrollMaxItemCount = stickyScrollOptions.stickyScrollMaxItemCount; this.stickyScrollDelegate = options.stickyScrollDelegate ?? new DefaultStickyScrollDelegate(); this._widget = this._register(new StickyScrollWidget(view.getScrollableElement(), view, tree, renderers, treeDelegate, options.accessibilityProvider)); this.onDidChangeHasFocus = this._widget.onDidChangeHasFocus; this.onContextMenu = this._widget.onContextMenu; this._register(view.onDidScroll(() => this.update())); this._register(view.onDidChangeContentHeight(() => this.update())); this._register(tree.onDidChangeCollapseState(() => this.update())); this.update(); } get height() { return this._widget.height; } getNodeAtHeight(height) { let index; if (height === 0) { index = this.view.firstVisibleIndex; } else { index = this.view.indexAt(height + this.view.scrollTop); } if (index < 0 || index >= this.view.length) { return undefined; } return this.view.element(index); } update() { const firstVisibleNode = this.getNodeAtHeight(0); // Don't render anything if there are no elements if (!firstVisibleNode || this.tree.scrollTop === 0) { this._widget.setState(undefined); return; } const stickyState = this.findStickyState(firstVisibleNode); this._widget.setState(stickyState); } findStickyState(firstVisibleNode) { const stickyNodes = []; let firstVisibleNodeUnderWidget = firstVisibleNode; let stickyNodesHeight = 0; let nextStickyNode = this.getNextStickyNode(firstVisibleNodeUnderWidget, undefined, stickyNodesHeight); while (nextStickyNode) { stickyNodes.push(nextStickyNode); stickyNodesHeight += nextStickyNode.height; if (stickyNodes.length <= this.stickyScrollMaxItemCount) { firstVisibleNodeUnderWidget = this.getNextVisibleNode(nextStickyNode); if (!firstVisibleNodeUnderWidget) { break; } } nextStickyNode = this.getNextStickyNode(firstVisibleNodeUnderWidget, nextStickyNode.node, stickyNodesHeight); } const contrainedStickyNodes = this.constrainStickyNodes(stickyNodes); return contrainedStickyNodes.length ? new StickyScrollState(contrainedStickyNodes) : undefined; } getNextVisibleNode(previousStickyNode) { return this.getNodeAtHeight(previousStickyNode.position + previousStickyNode.height); } getNextStickyNode(firstVisibleNodeUnderWidget, previousStickyNode, stickyNodesHeight) { const nextStickyNode = this.getAncestorUnderPrevious(firstVisibleNodeUnderWidget, previousStickyNode); if (!nextStickyNode) { return undefined; } if (nextStickyNode === firstVisibleNodeUnderWidget) { if (!this.nodeIsUncollapsedParent(firstVisibleNodeUnderWidget)) { return undefined; } if (this.nodeTopAlignsWithStickyNodesBottom(firstVisibleNodeUnderWidget, stickyNodesHeight)) { return undefined; } } return this.createStickyScrollNode(nextStickyNode, stickyNodesHeight); } nodeTopAlignsWithStickyNodesBottom(node, stickyNodesHeight) { const nodeIndex = this.getNodeIndex(node); const elementTop = this.view.getElementTop(nodeIndex); const stickyPosition = stickyNodesHeight; return this.view.scrollTop === elementTop - stickyPosition; } createStickyScrollNode(node, currentStickyNodesHeight) { const height = this.treeDelegate.getHeight(node); const { startIndex, endIndex } = this.getNodeRange(node); const position = this.calculateStickyNodePosition(endIndex, currentStickyNodesHeight, height); return { node, position, height, startIndex, endIndex }; } getAncestorUnderPrevious(node, previousAncestor = undefined) { let currentAncestor = node; let parentOfcurrentAncestor = this.getParentNode(currentAncestor); while (parentOfcurrentAncestor) { if (parentOfcurrentAncestor === previousAncestor) { return currentAncestor; } currentAncestor = parentOfcurrentAncestor; parentOfcurrentAncestor = this.getParentNode(currentAncestor); } if (previousAncestor === undefined) { return currentAncestor; } return undefined; } calculateStickyNodePosition(lastDescendantIndex, stickyRowPositionTop, stickyNodeHeight) { let lastChildRelativeTop = this.view.getRelativeTop(lastDescendantIndex); // If the last descendant is only partially visible at the top of the view, getRelativeTop() returns null // In that case, utilize the next node's relative top to calculate the sticky node's position if (lastChildRelativeTop === null && this.view.firstVisibleIndex === lastDescendantIndex && lastDescendantIndex + 1 < this.view.length) { const nodeHeight = this.treeDelegate.getHeight(this.view.element(lastDescendantIndex)); const nextNodeRelativeTop = this.view.getRelativeTop(lastDescendantIndex + 1); lastChildRelativeTop = nextNodeRelativeTop ? nextNodeRelativeTop - nodeHeight / this.view.renderHeight : null; } if (lastChildRelativeTop === null) { return stickyRowPositionTop; } const lastChildNode = this.view.element(lastDescendantIndex); const lastChildHeight = this.treeDelegate.getHeight(lastChildNode); const topOfLastChild = lastChildRelativeTop * this.view.renderHeight; const bottomOfLastChild = topOfLastChild + lastChildHeight; if (stickyRowPositionTop + stickyNodeHeight > bottomOfLastChild && stickyRowPositionTop <= bottomOfLastChild) { return bottomOfLastChild - stickyNodeHeight; } return stickyRowPositionTop; } constrainStickyNodes(stickyNodes) { if (stickyNodes.length === 0) { return []; } // Check if sticky nodes need to be constrained const maximumStickyWidgetHeight = this.view.renderHeight * this.maxWidgetViewRatio; const lastStickyNode = stickyNodes[stickyNodes.length - 1]; if (stickyNodes.length <= this.stickyScrollMaxItemCount && lastStickyNode.position + lastStickyNode.height <= maximumStickyWidgetHeight) { return stickyNodes; } // constrain sticky nodes const constrainedStickyNodes = this.stickyScrollDelegate.constrainStickyScrollNodes(stickyNodes, this.stickyScrollMaxItemCount, maximumStickyWidgetHeight); if (!constrainedStickyNodes.length) { return []; } // Validate constraints const lastConstrainedStickyNode = constrainedStickyNodes[constrainedStickyNodes.length - 1]; if (constrainedStickyNodes.length > this.stickyScrollMaxItemCount || lastConstrainedStickyNode.position + lastConstrainedStickyNode.height > maximumStickyWidgetHeight) { throw new Error('stickyScrollDelegate violates constraints'); } return constrainedStickyNodes; } getParentNode(node) { const nodeLocation = this.model.getNodeLocation(node); const parentLocation = this.model.getParentNodeLocation(nodeLocation); return parentLocation ? this.model.getNode(parentLocation) : undefined; } nodeIsUncollapsedParent(node) { const nodeLocation = this.model.getNodeLocation(node); return this.model.getListRenderCount(nodeLocation) > 1; } getNodeIndex(node) { const nodeLocation = this.model.getNodeLocation(node); const nodeIndex = this.model.getListIndex(nodeLocation); return nodeIndex; } getNodeRange(node) { const nodeLocation = this.model.getNodeLocation(node); const startIndex = this.model.getListIndex(nodeLocation); if (startIndex < 0) { throw new Error('Node not found in tree'); } const renderCount = this.model.getListRenderCount(nodeLocation); const endIndex = startIndex + renderCount - 1; return { startIndex, endIndex }; } nodePositionTopBelowWidget(node) { const ancestors = []; let currentAncestor = this.getParentNode(node); while (currentAncestor) { ancestors.push(currentAncestor); currentAncestor = this.getParentNode(currentAncestor); } let widgetHeight = 0; for (let i = 0; i < ancestors.length && i < this.stickyScrollMaxItemCount; i++) { widgetHeight += this.treeDelegate.getHeight(ancestors[i]); } return widgetHeight; } domFocus() { this._widget.domFocus(); } // Whether sticky scroll was the last focused part in the tree or not focusedLast() { return this._widget.focusedLast(); } updateOptions(optionsUpdate = {}) { if (!optionsUpdate.stickyScrollMaxItemCount) { return; } const validatedOptions = this.validateStickySettings(optionsUpdate); if (this.stickyScrollMaxItemCount !== validatedOptions.stickyScrollMaxItemCount) { this.stickyScrollMaxItemCount = validatedOptions.stickyScrollMaxItemCount; this.update(); } } validateStickySettings(options) { let stickyScrollMaxItemCount = 7; if (typeof options.stickyScrollMaxItemCount === 'number') { stickyScrollMaxItemCount = Math.max(options.stickyScrollMaxItemCount, 1); } return { stickyScrollMaxItemCount }; } } class StickyScrollWidget { constructor(container, view, tree, treeRenderers, treeDelegate, accessibilityProvider) { this.view = view; this.tree = tree; this.treeRenderers = treeRenderers; this.treeDelegate = treeDelegate; this.accessibilityProvider = accessibilityProvider; this._previousElements = []; this._previousStateDisposables = new DisposableStore(); this._rootDomNode = $('.monaco-tree-sticky-container.empty'); container.appendChild(this._rootDomNode); const shadow = $('.monaco-tree-sticky-container-shadow'); this._rootDomNode.appendChild(shadow); this.stickyScrollFocus = new StickyScrollFocus(this._rootDomNode, view); this.onDidChangeHasFocus = this.stickyScrollFocus.onDidChangeHasFocus; this.onContextMenu = this.stickyScrollFocus.onContextMenu; } get height() { if (!this._previousState) { return 0; } const lastElement = this._previousState.stickyNodes[this._previousState.count - 1]; return lastElement.position + lastElement.height; } setState(state) { const wasVisible = !!this._previousState && this._previousState.count > 0; const isVisible = !!state && state.count > 0; // If state has not changed, do nothing if ((!wasVisible && !isVisible) || (wasVisible && isVisible && this._previousState.equal(state))) { return; } // Update visibility of the widget if changed if (wasVisible !== isVisible) { this.setVisible(isVisible); } if (!isVisible) { this._previousState = undefined; this._previousElements = []; this._previousStateDisposables.clear(); return; } const lastStickyNode = state.stickyNodes[state.count - 1]; // If the new state is only a change in the last node's position, update the position of the last element if (this._previousState && state.animationStateChanged(this._previousState)) { this._previousElements[this._previousState.count - 1].style.top = `${lastStickyNode.position}px`; } // create new dom elements else { this._previousStateDisposables.clear(); const elements = Array(state.count); for (let stickyInd