UNPKG

sussudio

Version:

An unofficial VS Code Internal API

1,210 lines (1,208 loc) 60.5 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { createStyleSheet, EventHelper } from "../../dom.mjs"; import { DomEmitter } from "../../event.mjs"; import { StandardKeyboardEvent } from "../../keyboardEvent.mjs"; import { Gesture } from "../../touch.mjs"; import { alert } from "../aria/aria.mjs"; import { CombinedSpliceable } from "./splice.mjs"; import { binarySearch, firstOrDefault, range } from "../../../common/arrays.mjs"; import { timeout } from "../../../common/async.mjs"; import { Color } from "../../../common/color.mjs"; import { memoize } from "../../../common/decorators.mjs"; import { Emitter, Event, EventBufferer } from "../../../common/event.mjs"; import { matchesPrefix } from "../../../common/filters.mjs"; import { DisposableStore, dispose } from "../../../common/lifecycle.mjs"; import { clamp } from "../../../common/numbers.mjs"; import { mixin } from "../../../common/objects.mjs"; import * as platform from "../../../common/platform.mjs"; import { isNumber } from "../../../common/types.mjs"; import "../../../../css!./list.mjs"; import { ListError } from './list'; import { ListView } from './listView'; class TraitRenderer { trait; renderedElements = []; constructor(trait) { this.trait = trait; } get templateId() { return `template:${this.trait.name}`; } renderTemplate(container) { return container; } renderElement(element, index, templateData) { const renderedElementIndex = this.renderedElements.findIndex(el => el.templateData === templateData); if (renderedElementIndex >= 0) { const rendered = this.renderedElements[renderedElementIndex]; this.trait.unrender(templateData); rendered.index = index; } else { const rendered = { index, templateData }; this.renderedElements.push(rendered); } this.trait.renderIndex(index, templateData); } splice(start, deleteCount, insertCount) { const rendered = []; for (const renderedElement of this.renderedElements) { if (renderedElement.index < start) { rendered.push(renderedElement); } else if (renderedElement.index >= start + deleteCount) { rendered.push({ index: renderedElement.index + insertCount - deleteCount, templateData: renderedElement.templateData }); } } this.renderedElements = rendered; } renderIndexes(indexes) { for (const { index, templateData } of this.renderedElements) { if (indexes.indexOf(index) > -1) { this.trait.renderIndex(index, templateData); } } } disposeTemplate(templateData) { const index = this.renderedElements.findIndex(el => el.templateData === templateData); if (index < 0) { return; } this.renderedElements.splice(index, 1); } } class Trait { _trait; length = 0; indexes = []; sortedIndexes = []; _onChange = new Emitter(); onChange = this._onChange.event; get name() { return this._trait; } get renderer() { return new TraitRenderer(this); } constructor(_trait) { this._trait = _trait; } splice(start, deleteCount, elements) { deleteCount = Math.max(0, Math.min(deleteCount, this.length - start)); const diff = elements.length - deleteCount; const end = start + deleteCount; const sortedIndexes = [ ...this.sortedIndexes.filter(i => i < start), ...elements.map((hasTrait, i) => hasTrait ? i + start : -1).filter(i => i !== -1), ...this.sortedIndexes.filter(i => i >= end).map(i => i + diff) ]; const length = this.length + diff; if (this.sortedIndexes.length > 0 && sortedIndexes.length === 0 && length > 0) { const first = this.sortedIndexes.find(index => index >= start) ?? length - 1; sortedIndexes.push(Math.min(first, length - 1)); } this.renderer.splice(start, deleteCount, elements.length); this._set(sortedIndexes, sortedIndexes); this.length = length; } renderIndex(index, container) { container.classList.toggle(this._trait, this.contains(index)); } unrender(container) { container.classList.remove(this._trait); } /** * Sets the indexes which should have this trait. * * @param indexes Indexes which should have this trait. * @return The old indexes which had this trait. */ set(indexes, browserEvent) { return this._set(indexes, [...indexes].sort(numericSort), browserEvent); } _set(indexes, sortedIndexes, browserEvent) { const result = this.indexes; const sortedResult = this.sortedIndexes; this.indexes = indexes; this.sortedIndexes = sortedIndexes; const toRender = disjunction(sortedResult, indexes); this.renderer.renderIndexes(toRender); this._onChange.fire({ indexes, browserEvent }); return result; } get() { return this.indexes; } contains(index) { return binarySearch(this.sortedIndexes, index, numericSort) >= 0; } dispose() { dispose(this._onChange); } } __decorate([ memoize ], Trait.prototype, "renderer", null); class SelectionTrait extends Trait { setAriaSelected; constructor(setAriaSelected) { super('selected'); this.setAriaSelected = setAriaSelected; } renderIndex(index, container) { super.renderIndex(index, container); if (this.setAriaSelected) { if (this.contains(index)) { container.setAttribute('aria-selected', 'true'); } else { container.setAttribute('aria-selected', 'false'); } } } } /** * The TraitSpliceable is used as a util class to be able * to preserve traits across splice calls, given an identity * provider. */ class TraitSpliceable { trait; view; identityProvider; constructor(trait, view, identityProvider) { this.trait = trait; this.view = view; this.identityProvider = identityProvider; } splice(start, deleteCount, elements) { if (!this.identityProvider) { return this.trait.splice(start, deleteCount, elements.map(() => false)); } const pastElementsWithTrait = this.trait.get().map(i => this.identityProvider.getId(this.view.element(i)).toString()); const elementsWithTrait = elements.map(e => pastElementsWithTrait.indexOf(this.identityProvider.getId(e).toString()) > -1); this.trait.splice(start, deleteCount, elementsWithTrait); } } export function isInputElement(e) { return e.tagName === 'INPUT' || e.tagName === 'TEXTAREA'; } export function isMonacoEditor(e) { if (e.classList.contains('monaco-editor')) { return true; } if (e.classList.contains('monaco-list')) { return false; } if (!e.parentElement) { return false; } return isMonacoEditor(e.parentElement); } export function isButton(e) { if ((e.tagName === 'A' && e.classList.contains('monaco-button')) || (e.tagName === 'DIV' && e.classList.contains('monaco-button-dropdown'))) { return true; } if (e.classList.contains('monaco-list')) { return false; } if (!e.parentElement) { return false; } return isButton(e.parentElement); } class KeyboardController { list; view; disposables = new DisposableStore(); multipleSelectionDisposables = new DisposableStore(); get onKeyDown() { return this.disposables.add(Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event) .filter(e => !isInputElement(e.target)) .map(e => new StandardKeyboardEvent(e))); } constructor(list, view, options) { this.list = list; this.view = view; this.onKeyDown.filter(e => e.keyCode === 3 /* KeyCode.Enter */).on(this.onEnter, this, this.disposables); this.onKeyDown.filter(e => e.keyCode === 16 /* KeyCode.UpArrow */).on(this.onUpArrow, this, this.disposables); this.onKeyDown.filter(e => e.keyCode === 18 /* KeyCode.DownArrow */).on(this.onDownArrow, this, this.disposables); this.onKeyDown.filter(e => e.keyCode === 11 /* KeyCode.PageUp */).on(this.onPageUpArrow, this, this.disposables); this.onKeyDown.filter(e => e.keyCode === 12 /* KeyCode.PageDown */).on(this.onPageDownArrow, this, this.disposables); this.onKeyDown.filter(e => e.keyCode === 9 /* KeyCode.Escape */).on(this.onEscape, this, this.disposables); if (options.multipleSelectionSupport !== false) { this.onKeyDown.filter(e => (platform.isMacintosh ? e.metaKey : e.ctrlKey) && e.keyCode === 31 /* KeyCode.KeyA */).on(this.onCtrlA, this, this.multipleSelectionDisposables); } } updateOptions(optionsUpdate) { if (optionsUpdate.multipleSelectionSupport !== undefined) { this.multipleSelectionDisposables.clear(); if (optionsUpdate.multipleSelectionSupport) { this.onKeyDown.filter(e => (platform.isMacintosh ? e.metaKey : e.ctrlKey) && e.keyCode === 31 /* KeyCode.KeyA */).on(this.onCtrlA, this, this.multipleSelectionDisposables); } } } onEnter(e) { e.preventDefault(); e.stopPropagation(); this.list.setSelection(this.list.getFocus(), e.browserEvent); } onUpArrow(e) { e.preventDefault(); e.stopPropagation(); this.list.focusPrevious(1, false, e.browserEvent); const el = this.list.getFocus()[0]; this.list.setAnchor(el); this.list.reveal(el); this.view.domNode.focus(); } onDownArrow(e) { e.preventDefault(); e.stopPropagation(); this.list.focusNext(1, false, e.browserEvent); const el = this.list.getFocus()[0]; this.list.setAnchor(el); this.list.reveal(el); this.view.domNode.focus(); } onPageUpArrow(e) { e.preventDefault(); e.stopPropagation(); this.list.focusPreviousPage(e.browserEvent); const el = this.list.getFocus()[0]; this.list.setAnchor(el); this.list.reveal(el); this.view.domNode.focus(); } onPageDownArrow(e) { e.preventDefault(); e.stopPropagation(); this.list.focusNextPage(e.browserEvent); const el = this.list.getFocus()[0]; this.list.setAnchor(el); this.list.reveal(el); this.view.domNode.focus(); } onCtrlA(e) { e.preventDefault(); e.stopPropagation(); this.list.setSelection(range(this.list.length), e.browserEvent); this.list.setAnchor(undefined); this.view.domNode.focus(); } onEscape(e) { if (this.list.getSelection().length) { e.preventDefault(); e.stopPropagation(); this.list.setSelection([], e.browserEvent); this.list.setAnchor(undefined); this.view.domNode.focus(); } } dispose() { this.disposables.dispose(); this.multipleSelectionDisposables.dispose(); } } __decorate([ memoize ], KeyboardController.prototype, "onKeyDown", null); export var TypeNavigationMode; (function (TypeNavigationMode) { TypeNavigationMode[TypeNavigationMode["Automatic"] = 0] = "Automatic"; TypeNavigationMode[TypeNavigationMode["Trigger"] = 1] = "Trigger"; })(TypeNavigationMode || (TypeNavigationMode = {})); var TypeNavigationControllerState; (function (TypeNavigationControllerState) { TypeNavigationControllerState[TypeNavigationControllerState["Idle"] = 0] = "Idle"; TypeNavigationControllerState[TypeNavigationControllerState["Typing"] = 1] = "Typing"; })(TypeNavigationControllerState || (TypeNavigationControllerState = {})); export const DefaultKeyboardNavigationDelegate = new class { mightProducePrintableCharacter(event) { if (event.ctrlKey || event.metaKey || event.altKey) { return false; } return (event.keyCode >= 31 /* KeyCode.KeyA */ && event.keyCode <= 56 /* KeyCode.KeyZ */) || (event.keyCode >= 21 /* KeyCode.Digit0 */ && event.keyCode <= 30 /* KeyCode.Digit9 */) || (event.keyCode >= 93 /* KeyCode.Numpad0 */ && event.keyCode <= 102 /* KeyCode.Numpad9 */) || (event.keyCode >= 80 /* KeyCode.Semicolon */ && event.keyCode <= 90 /* KeyCode.Quote */); } }; class TypeNavigationController { list; view; keyboardNavigationLabelProvider; keyboardNavigationEventFilter; delegate; enabled = false; state = TypeNavigationControllerState.Idle; mode = TypeNavigationMode.Automatic; triggered = false; previouslyFocused = -1; enabledDisposables = new DisposableStore(); disposables = new DisposableStore(); constructor(list, view, keyboardNavigationLabelProvider, keyboardNavigationEventFilter, delegate) { this.list = list; this.view = view; this.keyboardNavigationLabelProvider = keyboardNavigationLabelProvider; this.keyboardNavigationEventFilter = keyboardNavigationEventFilter; this.delegate = delegate; this.updateOptions(list.options); } updateOptions(options) { if (options.typeNavigationEnabled ?? true) { this.enable(); } else { this.disable(); } this.mode = options.typeNavigationMode ?? TypeNavigationMode.Automatic; } trigger() { this.triggered = !this.triggered; } enable() { if (this.enabled) { return; } let typing = false; const onChar = this.enabledDisposables.add(Event.chain(this.enabledDisposables.add(new DomEmitter(this.view.domNode, 'keydown')).event)) .filter(e => !isInputElement(e.target)) .filter(() => this.mode === TypeNavigationMode.Automatic || this.triggered) .map(event => new StandardKeyboardEvent(event)) .filter(e => typing || this.keyboardNavigationEventFilter(e)) .filter(e => this.delegate.mightProducePrintableCharacter(e)) .forEach(e => EventHelper.stop(e, true)) .map(event => event.browserEvent.key) .event; const onClear = Event.debounce(onChar, () => null, 800, undefined, undefined, this.enabledDisposables); const onInput = Event.reduce(Event.any(onChar, onClear), (r, i) => i === null ? null : ((r || '') + i), undefined, this.enabledDisposables); onInput(this.onInput, this, this.enabledDisposables); onClear(this.onClear, this, this.enabledDisposables); onChar(() => typing = true, undefined, this.enabledDisposables); onClear(() => typing = false, undefined, this.enabledDisposables); this.enabled = true; this.triggered = false; } disable() { if (!this.enabled) { return; } this.enabledDisposables.clear(); this.enabled = false; this.triggered = false; } onClear() { const focus = this.list.getFocus(); if (focus.length > 0 && focus[0] === this.previouslyFocused) { // List: re-announce element on typing end since typed keys will interrupt aria label of focused element // Do not announce if there was a focus change at the end to prevent duplication https://github.com/microsoft/vscode/issues/95961 const ariaLabel = this.list.options.accessibilityProvider?.getAriaLabel(this.list.element(focus[0])); if (ariaLabel) { alert(ariaLabel); } } this.previouslyFocused = -1; } onInput(word) { if (!word) { this.state = TypeNavigationControllerState.Idle; this.triggered = false; return; } const focus = this.list.getFocus(); const start = focus.length > 0 ? focus[0] : 0; const delta = this.state === TypeNavigationControllerState.Idle ? 1 : 0; this.state = TypeNavigationControllerState.Typing; for (let i = 0; i < this.list.length; i++) { const index = (start + i + delta) % this.list.length; const label = this.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(this.view.element(index)); const labelStr = label && label.toString(); if (typeof labelStr === 'undefined' || matchesPrefix(word, labelStr)) { this.previouslyFocused = start; this.list.setFocus([index]); this.list.reveal(index); return; } } } dispose() { this.disable(); this.enabledDisposables.dispose(); this.disposables.dispose(); } } class DOMFocusController { list; view; disposables = new DisposableStore(); constructor(list, view) { this.list = list; this.view = view; const onKeyDown = this.disposables.add(Event.chain(this.disposables.add(new DomEmitter(view.domNode, 'keydown')).event)) .filter(e => !isInputElement(e.target)) .map(e => new StandardKeyboardEvent(e)); onKeyDown.filter(e => e.keyCode === 2 /* KeyCode.Tab */ && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey) .on(this.onTab, this, this.disposables); } onTab(e) { if (e.target !== this.view.domNode) { return; } const focus = this.list.getFocus(); if (focus.length === 0) { return; } const focusedDomElement = this.view.domElement(focus[0]); if (!focusedDomElement) { return; } const tabIndexElement = focusedDomElement.querySelector('[tabIndex]'); if (!tabIndexElement || !(tabIndexElement instanceof HTMLElement) || tabIndexElement.tabIndex === -1) { return; } const style = window.getComputedStyle(tabIndexElement); if (style.visibility === 'hidden' || style.display === 'none') { return; } e.preventDefault(); e.stopPropagation(); tabIndexElement.focus(); } dispose() { this.disposables.dispose(); } } export function isSelectionSingleChangeEvent(event) { return platform.isMacintosh ? event.browserEvent.metaKey : event.browserEvent.ctrlKey; } export function isSelectionRangeChangeEvent(event) { return event.browserEvent.shiftKey; } function isMouseRightClick(event) { return event instanceof MouseEvent && event.button === 2; } const DefaultMultipleSelectionController = { isSelectionSingleChangeEvent, isSelectionRangeChangeEvent }; export class MouseController { list; multipleSelectionController; mouseSupport; disposables = new DisposableStore(); _onPointer = new Emitter(); onPointer = this._onPointer.event; constructor(list) { this.list = list; if (list.options.multipleSelectionSupport !== false) { this.multipleSelectionController = this.list.options.multipleSelectionController || DefaultMultipleSelectionController; } this.mouseSupport = typeof list.options.mouseSupport === 'undefined' || !!list.options.mouseSupport; if (this.mouseSupport) { list.onMouseDown(this.onMouseDown, this, this.disposables); list.onContextMenu(this.onContextMenu, this, this.disposables); list.onMouseDblClick(this.onDoubleClick, this, this.disposables); list.onTouchStart(this.onMouseDown, this, this.disposables); this.disposables.add(Gesture.addTarget(list.getHTMLElement())); } Event.any(list.onMouseClick, list.onMouseMiddleClick, list.onTap)(this.onViewPointer, this, this.disposables); } updateOptions(optionsUpdate) { if (optionsUpdate.multipleSelectionSupport !== undefined) { this.multipleSelectionController = undefined; if (optionsUpdate.multipleSelectionSupport) { this.multipleSelectionController = this.list.options.multipleSelectionController || DefaultMultipleSelectionController; } } } isSelectionSingleChangeEvent(event) { if (!this.multipleSelectionController) { return false; } return this.multipleSelectionController.isSelectionSingleChangeEvent(event); } isSelectionRangeChangeEvent(event) { if (!this.multipleSelectionController) { return false; } return this.multipleSelectionController.isSelectionRangeChangeEvent(event); } isSelectionChangeEvent(event) { return this.isSelectionSingleChangeEvent(event) || this.isSelectionRangeChangeEvent(event); } onMouseDown(e) { if (isMonacoEditor(e.browserEvent.target)) { return; } if (document.activeElement !== e.browserEvent.target) { this.list.domFocus(); } } onContextMenu(e) { if (isInputElement(e.browserEvent.target) || isMonacoEditor(e.browserEvent.target)) { return; } const focus = typeof e.index === 'undefined' ? [] : [e.index]; this.list.setFocus(focus, e.browserEvent); } onViewPointer(e) { if (!this.mouseSupport) { return; } if (isInputElement(e.browserEvent.target) || isMonacoEditor(e.browserEvent.target)) { return; } const focus = e.index; if (typeof focus === 'undefined') { this.list.setFocus([], e.browserEvent); this.list.setSelection([], e.browserEvent); this.list.setAnchor(undefined); return; } if (this.isSelectionRangeChangeEvent(e)) { return this.changeSelection(e); } if (this.isSelectionChangeEvent(e)) { return this.changeSelection(e); } this.list.setFocus([focus], e.browserEvent); this.list.setAnchor(focus); if (!isMouseRightClick(e.browserEvent)) { this.list.setSelection([focus], e.browserEvent); } this._onPointer.fire(e); } onDoubleClick(e) { if (isInputElement(e.browserEvent.target) || isMonacoEditor(e.browserEvent.target)) { return; } if (this.isSelectionChangeEvent(e)) { return; } const focus = this.list.getFocus(); this.list.setSelection(focus, e.browserEvent); } changeSelection(e) { const focus = e.index; let anchor = this.list.getAnchor(); if (this.isSelectionRangeChangeEvent(e)) { if (typeof anchor === 'undefined') { const currentFocus = this.list.getFocus()[0]; anchor = currentFocus ?? focus; this.list.setAnchor(anchor); } const min = Math.min(anchor, focus); const max = Math.max(anchor, focus); const rangeSelection = range(min, max + 1); const selection = this.list.getSelection(); const contiguousRange = getContiguousRangeContaining(disjunction(selection, [anchor]), anchor); if (contiguousRange.length === 0) { return; } const newSelection = disjunction(rangeSelection, relativeComplement(selection, contiguousRange)); this.list.setSelection(newSelection, e.browserEvent); this.list.setFocus([focus], e.browserEvent); } else if (this.isSelectionSingleChangeEvent(e)) { const selection = this.list.getSelection(); const newSelection = selection.filter(i => i !== focus); this.list.setFocus([focus]); this.list.setAnchor(focus); if (selection.length === newSelection.length) { this.list.setSelection([...newSelection, focus], e.browserEvent); } else { this.list.setSelection(newSelection, e.browserEvent); } } } dispose() { this.disposables.dispose(); } } export class DefaultStyleController { styleElement; selectorSuffix; constructor(styleElement, selectorSuffix) { this.styleElement = styleElement; this.selectorSuffix = selectorSuffix; } style(styles) { const suffix = this.selectorSuffix && `.${this.selectorSuffix}`; const content = []; if (styles.listBackground) { if (styles.listBackground.isOpaque()) { content.push(`.monaco-list${suffix} .monaco-list-rows { background: ${styles.listBackground}; }`); } else if (!platform.isMacintosh) { // subpixel AA doesn't exist in macOS console.warn(`List with id '${this.selectorSuffix}' was styled with a non-opaque background color. This will break sub-pixel antialiasing.`); } } if (styles.listFocusBackground) { content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused { background-color: ${styles.listFocusBackground}; }`); content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused:hover { background-color: ${styles.listFocusBackground}; }`); // overwrite :hover style in this case! } if (styles.listFocusForeground) { content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused { color: ${styles.listFocusForeground}; }`); } if (styles.listActiveSelectionBackground) { content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected { background-color: ${styles.listActiveSelectionBackground}; }`); content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected:hover { background-color: ${styles.listActiveSelectionBackground}; }`); // overwrite :hover style in this case! } if (styles.listActiveSelectionForeground) { content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected { color: ${styles.listActiveSelectionForeground}; }`); } if (styles.listActiveSelectionIconForeground) { content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected .codicon { color: ${styles.listActiveSelectionIconForeground}; }`); } if (styles.listFocusAndSelectionOutline) { content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected { outline-color: ${styles.listFocusAndSelectionOutline} !important; }`); } if (styles.listFocusAndSelectionBackground) { content.push(` .monaco-drag-image, .monaco-list${suffix}:focus .monaco-list-row.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; } `); } if (styles.listFocusAndSelectionForeground) { content.push(` .monaco-drag-image, .monaco-list${suffix}:focus .monaco-list-row.selected.focused { color: ${styles.listFocusAndSelectionForeground}; } `); } if (styles.listInactiveFocusForeground) { content.push(`.monaco-list${suffix} .monaco-list-row.focused { color: ${styles.listInactiveFocusForeground}; }`); content.push(`.monaco-list${suffix} .monaco-list-row.focused:hover { color: ${styles.listInactiveFocusForeground}; }`); // overwrite :hover style in this case! } if (styles.listInactiveSelectionIconForeground) { content.push(`.monaco-list${suffix} .monaco-list-row.focused .codicon { color: ${styles.listInactiveSelectionIconForeground}; }`); } if (styles.listInactiveFocusBackground) { content.push(`.monaco-list${suffix} .monaco-list-row.focused { background-color: ${styles.listInactiveFocusBackground}; }`); content.push(`.monaco-list${suffix} .monaco-list-row.focused:hover { background-color: ${styles.listInactiveFocusBackground}; }`); // overwrite :hover style in this case! } if (styles.listInactiveSelectionBackground) { content.push(`.monaco-list${suffix} .monaco-list-row.selected { background-color: ${styles.listInactiveSelectionBackground}; }`); content.push(`.monaco-list${suffix} .monaco-list-row.selected:hover { background-color: ${styles.listInactiveSelectionBackground}; }`); // overwrite :hover style in this case! } if (styles.listInactiveSelectionForeground) { content.push(`.monaco-list${suffix} .monaco-list-row.selected { color: ${styles.listInactiveSelectionForeground}; }`); } if (styles.listHoverBackground) { content.push(`.monaco-list${suffix}:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`); } if (styles.listHoverForeground) { content.push(`.monaco-list${suffix}:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`); } if (styles.listSelectionOutline) { content.push(`.monaco-list${suffix} .monaco-list-row.selected { outline: 1px dotted ${styles.listSelectionOutline}; outline-offset: -1px; }`); } if (styles.listFocusOutline) { content.push(` .monaco-drag-image, .monaco-list${suffix}:focus .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; } .monaco-workbench.context-menu-visible .monaco-list${suffix}.last-focused .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; } `); } if (styles.listInactiveFocusOutline) { content.push(`.monaco-list${suffix} .monaco-list-row.focused { outline: 1px dotted ${styles.listInactiveFocusOutline}; outline-offset: -1px; }`); } if (styles.listHoverOutline) { content.push(`.monaco-list${suffix} .monaco-list-row:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`); } if (styles.listDropBackground) { content.push(` .monaco-list${suffix}.drop-target, .monaco-list${suffix} .monaco-list-rows.drop-target, .monaco-list${suffix} .monaco-list-row.drop-target { background-color: ${styles.listDropBackground} !important; color: inherit !important; } `); } if (styles.tableColumnsBorder) { content.push(` .monaco-table > .monaco-split-view2, .monaco-table > .monaco-split-view2 .monaco-sash.vertical::before, .monaco-workbench:not(.reduce-motion) .monaco-table:hover > .monaco-split-view2, .monaco-workbench:not(.reduce-motion) .monaco-table:hover > .monaco-split-view2 .monaco-sash.vertical::before { border-color: ${styles.tableColumnsBorder}; } .monaco-workbench:not(.reduce-motion) .monaco-table > .monaco-split-view2, .monaco-workbench:not(.reduce-motion) .monaco-table > .monaco-split-view2 .monaco-sash.vertical::before { border-color: transparent; } `); } if (styles.tableOddRowsBackgroundColor) { content.push(` .monaco-table .monaco-list-row[data-parity=odd]:not(.focused):not(.selected):not(:hover) .monaco-table-tr, .monaco-table .monaco-list:not(:focus) .monaco-list-row[data-parity=odd].focused:not(.selected):not(:hover) .monaco-table-tr, .monaco-table .monaco-list:not(.focused) .monaco-list-row[data-parity=odd].focused:not(.selected):not(:hover) .monaco-table-tr { background-color: ${styles.tableOddRowsBackgroundColor}; } `); } this.styleElement.textContent = content.join('\n'); } } const defaultStyles = { listFocusBackground: Color.fromHex('#7FB0D0'), listActiveSelectionBackground: Color.fromHex('#0E639C'), listActiveSelectionForeground: Color.fromHex('#FFFFFF'), listActiveSelectionIconForeground: Color.fromHex('#FFFFFF'), listFocusAndSelectionOutline: Color.fromHex('#90C2F9'), listFocusAndSelectionBackground: Color.fromHex('#094771'), listFocusAndSelectionForeground: Color.fromHex('#FFFFFF'), listInactiveSelectionBackground: Color.fromHex('#3F3F46'), listInactiveSelectionIconForeground: Color.fromHex('#FFFFFF'), listHoverBackground: Color.fromHex('#2A2D2E'), listDropBackground: Color.fromHex('#383B3D'), treeIndentGuidesStroke: Color.fromHex('#a9a9a9'), tableColumnsBorder: Color.fromHex('#cccccc').transparent(0.2), tableOddRowsBackgroundColor: Color.fromHex('#cccccc').transparent(0.04) }; const DefaultOptions = { keyboardSupport: true, mouseSupport: true, multipleSelectionSupport: true, dnd: { getDragURI() { return null; }, onDragStart() { }, onDragOver() { return false; }, drop() { } } }; // TODO@Joao: move these utils into a SortedArray class function getContiguousRangeContaining(range, value) { const index = range.indexOf(value); if (index === -1) { return []; } const result = []; let i = index - 1; while (i >= 0 && range[i] === value - (index - i)) { result.push(range[i--]); } result.reverse(); i = index; while (i < range.length && range[i] === value + (i - index)) { result.push(range[i++]); } return result; } /** * Given two sorted collections of numbers, returns the intersection * between them (OR). */ function disjunction(one, other) { const result = []; let i = 0, j = 0; while (i < one.length || j < other.length) { if (i >= one.length) { result.push(other[j++]); } else if (j >= other.length) { result.push(one[i++]); } else if (one[i] === other[j]) { result.push(one[i]); i++; j++; continue; } else if (one[i] < other[j]) { result.push(one[i++]); } else { result.push(other[j++]); } } return result; } /** * Given two sorted collections of numbers, returns the relative * complement between them (XOR). */ function relativeComplement(one, other) { const result = []; let i = 0, j = 0; while (i < one.length || j < other.length) { if (i >= one.length) { result.push(other[j++]); } else if (j >= other.length) { result.push(one[i++]); } else if (one[i] === other[j]) { i++; j++; continue; } else if (one[i] < other[j]) { result.push(one[i++]); } else { j++; } } return result; } const numericSort = (a, b) => a - b; class PipelineRenderer { _templateId; renderers; constructor(_templateId, renderers) { this._templateId = _templateId; this.renderers = renderers; } get templateId() { return this._templateId; } renderTemplate(container) { return this.renderers.map(r => r.renderTemplate(container)); } renderElement(element, index, templateData, height) { let i = 0; for (const renderer of this.renderers) { renderer.renderElement(element, index, templateData[i++], height); } } disposeElement(element, index, templateData, height) { let i = 0; for (const renderer of this.renderers) { renderer.disposeElement?.(element, index, templateData[i], height); i += 1; } } disposeTemplate(templateData) { let i = 0; for (const renderer of this.renderers) { renderer.disposeTemplate(templateData[i++]); } } } class AccessibiltyRenderer { accessibilityProvider; templateId = 'a18n'; constructor(accessibilityProvider) { this.accessibilityProvider = accessibilityProvider; } renderTemplate(container) { return container; } renderElement(element, index, container) { const ariaLabel = this.accessibilityProvider.getAriaLabel(element); if (ariaLabel) { container.setAttribute('aria-label', ariaLabel); } else { container.removeAttribute('aria-label'); } const ariaLevel = this.accessibilityProvider.getAriaLevel && this.accessibilityProvider.getAriaLevel(element); if (typeof ariaLevel === 'number') { container.setAttribute('aria-level', `${ariaLevel}`); } else { container.removeAttribute('aria-level'); } } disposeTemplate(templateData) { // noop } } class ListViewDragAndDrop { list; dnd; constructor(list, dnd) { this.list = list; this.dnd = dnd; } getDragElements(element) { const selection = this.list.getSelectedElements(); const elements = selection.indexOf(element) > -1 ? selection : [element]; return elements; } getDragURI(element) { return this.dnd.getDragURI(element); } getDragLabel(elements, originalEvent) { if (this.dnd.getDragLabel) { return this.dnd.getDragLabel(elements, originalEvent); } return undefined; } onDragStart(data, originalEvent) { this.dnd.onDragStart?.(data, originalEvent); } onDragOver(data, targetElement, targetIndex, originalEvent) { return this.dnd.onDragOver(data, targetElement, targetIndex, originalEvent); } onDragLeave(data, targetElement, targetIndex, originalEvent) { this.dnd.onDragLeave?.(data, targetElement, targetIndex, originalEvent); } onDragEnd(originalEvent) { this.dnd.onDragEnd?.(originalEvent); } drop(data, targetElement, targetIndex, originalEvent) { this.dnd.drop(data, targetElement, targetIndex, originalEvent); } } /** * The {@link List} is a virtual scrolling widget, built on top of the {@link ListView} * widget. * * Features: * - Customizable keyboard and mouse support * - Element traits: focus, selection, achor * - Accessibility support * - Touch support * - Performant template-based rendering * - Horizontal scrolling * - Variable element height support * - Dynamic element height support * - Drag-and-drop support */ export class List { user; _options; focus = new Trait('focused'); selection; anchor = new Trait('anchor'); eventBufferer = new EventBufferer(); view; spliceable; styleController; typeNavigationController; accessibilityProvider; keyboardController; mouseController; _ariaLabel = ''; disposables = new DisposableStore(); get onDidChangeFocus() { return Event.map(this.eventBufferer.wrapEvent(this.focus.onChange), e => this.toListEvent(e), this.disposables); } get onDidChangeSelection() { return Event.map(this.eventBufferer.wrapEvent(this.selection.onChange), e => this.toListEvent(e), this.disposables); } get domId() { return this.view.domId; } get onDidScroll() { return this.view.onDidScroll; } get onMouseClick() { return this.view.onMouseClick; } get onMouseDblClick() { return this.view.onMouseDblClick; } get onMouseMiddleClick() { return this.view.onMouseMiddleClick; } get onPointer() { return this.mouseController.onPointer; } get onMouseUp() { return this.view.onMouseUp; } get onMouseDown() { return this.view.onMouseDown; } get onMouseOver() { return this.view.onMouseOver; } get onMouseMove() { return this.view.onMouseMove; } get onMouseOut() { return this.view.onMouseOut; } get onTouchStart() { return this.view.onTouchStart; } get onTap() { return this.view.onTap; } /** * Possible context menu trigger events: * - ContextMenu key * - Shift F10 * - Ctrl Option Shift M (macOS with VoiceOver) * - Mouse right click */ get onContextMenu() { let didJustPressContextMenuKey = false; const fromKeyDown = this.disposables.add(Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event)) .map(e => new StandardKeyboardEvent(e)) .filter(e => didJustPressContextMenuKey = e.keyCode === 58 /* KeyCode.ContextMenu */ || (e.shiftKey && e.keyCode === 68 /* KeyCode.F10 */)) .map(e => EventHelper.stop(e, true)) .filter(() => false) .event; const fromKeyUp = this.disposables.add(Event.chain(this.disposables.add(new DomEmitter(this.view.domNode, 'keyup')).event)) .forEach(() => didJustPressContextMenuKey = false) .map(e => new StandardKeyboardEvent(e)) .filter(e => e.keyCode === 58 /* KeyCode.ContextMenu */ || (e.shiftKey && e.keyCode === 68 /* KeyCode.F10 */)) .map(e => EventHelper.stop(e, true)) .map(({ browserEvent }) => { const focus = this.getFocus(); const index = focus.length ? focus[0] : undefined; const element = typeof index !== 'undefined' ? this.view.element(index) : undefined; const anchor = typeof index !== 'undefined' ? this.view.domElement(index) : this.view.domNode; return { index, element, anchor, browserEvent }; }) .event; const fromMouse = this.disposables.add(Event.chain(this.view.onContextMenu)) .filter(_ => !didJustPressContextMenuKey) .map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.pageX + 1, y: browserEvent.pageY }, browserEvent })) .event; return Event.any(fromKeyDown, fromKeyUp, fromMouse); } get onKeyDown() { return this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).event; } get onKeyUp() { return this.disposables.add(new DomEmitter(this.view.domNode, 'keyup')).event; } get onKeyPress() { return this.disposables.add(new DomEmitter(this.view.domNode, 'keypress')).event; } get onDidFocus() { return Event.signal(this.disposables.add(new DomEmitter(this.view.domNode, 'focus', true)).event); } get onDidBlur() { return Event.signal(this.disposables.add(new DomEmitter(this.view.domNode, 'blur', true)).event); } _onDidDispose = new Emitter(); onDidDispose = this._onDidDispose.event; constructor(user, container, virtualDelegate, renderers, _options = DefaultOptions) { this.user = user; this._options = _options; const role = this._options.accessibilityProvider && this._options.accessibilityProvider.getWidgetRole ? this._options.accessibilityProvider?.getWidgetRole() : 'list'; this.selection = new SelectionTrait(role !== 'listbox'); mixin(_options, defaultStyles, false); const baseRenderers = [this.focus.renderer, this.selection.renderer]; this.accessibilityProvider = _options.accessibilityProvider; if (this.accessibilityProvider) { baseRenderers.push(new AccessibiltyRenderer(this.accessibilityProvider)); this.accessibilityProvider.onDidChangeActiveDescendant?.(this.onDidChangeActiveDescendant, this, this.disposables); } renderers = renderers.map(r => new PipelineRenderer(r.templateId, [...baseRenderers, r])); const viewOptions = { ..._options, dnd: _options.dnd && new ListViewDragAndDrop(this, _options.dnd) }; this.view = new ListView(container, virtualDelegate, renderers, viewOptions); this.view.domNode.setAttribute('role', role); if (_options.styleController) { this.styleController = _options.styleController(this.view.domId); } else { const styleElement = createStyleSheet(this.view.domNode); this.styleController = new DefaultStyleController(styleElement, this.view.domId); } this.spliceable = new CombinedSpliceable([ new TraitSpliceable(this.focus, this.view, _options.identityProvider), new TraitSpliceable(this.selection, this.view, _options.identityProvider), new TraitSpliceable(this.anchor, this.view, _options.identityProvider), this.view ]); this.disposables.add(this.focus); this.disposables.add(this.selection); this.disposables.add(this.anchor); this.disposables.add(this.view); this.disposables.add(this._onDidDispose); this.disposables.add(new DOMFocusController(this, this.view)); if (typeof _options.keyboardSupport !== 'boolean' || _options.keyboardSupport) { this.keyboardController = new KeyboardController(this, this.view, _options); this.disposables.add(this.keyboardController); } if (_options.keyboardNavigationLabelProvider) { const delegate = _options.keyboardNavigationDelegate || DefaultKeyboardNavigationDelegate; this.typeNavigationController = new TypeNavigationController(this, this.view, _options.keyboardNavigationLabelProvider, _options.keyboardNavigationEventFilter ?? (() => true), delegate); this.disposables.add(this.typeNavigationController); } this.mouseController = this.createMouseController(_options); this.disposables.add(this.mouseController); this.onDidChangeFocus(this._onFocusChange, this, this.disposables); this.onDidChangeSelection(this._onSelectionChange, this, this.disposables); if (this.accessibilityProvider) { this.ariaLabel = this.accessibilityProvider.getWidgetAriaLabel(); } if (this._options.multipleSelectionSupport !== false) { this.view.domNode.setAttribute('aria-multiselectable', 'true'); } } createMouseController(options) { return new MouseController(this); } updateOptions(optionsUpdate = {}) { this._options = { ...this._options, ...optionsUpdate }; this.typeNavigationController?.updateOptions(this._options); if (this._options.multipleSelectionController !== undefined) { if (this._options.multipleSelectionSupport) { this.view.domNode.setAttribute('aria-multiselectable', 'true'); } else { this.view.domNode.removeAttribute('aria-multiselectable'); } } this.mouseController.updateOptions(optionsUpdate); this.keyboardController?.updateOptions(optionsUpdate); this.view.updateOptions(optionsUpdate); } get options() { return this._options; } splice(start, deleteCount, elements = []) { if (start < 0 || start > this.view.length) { throw new ListError(this.user, `Invalid start index: ${start}`); } if (deleteCount < 0) { throw new ListError(this.user, `Invalid delete count: ${deleteCount}`); } if (deleteCount === 0 && elements.length === 0) { return; } this.eventBufferer.bufferEvents(() => this.spliceable.splice(start, deleteCount, elements)); } updateWidth(index) { this.view.updateWidth(index); } updateElementHeight(index, size) { this.view.updateElementHeight(index, size, null); } rerender() { this.view.rerender(); } element(index) { return this.view.element(index); } indexOf(element) { return this.view.indexOf(element); } get length() { return this.view.length; } get contentHeight() { return this.view.contentHeight; } get onDidChangeContentHeight() { return this.view.onDidChangeContentHeight; } get scrollTop() { return this.view.getScrollTop(); } set scrollTop(scrollTop) { this.view.setScrollTop(scrollTop); } get scrollLeft() { return this.view.getScrollLeft(); } set scrollLeft(scrollLeft) { this.view.setScrollLeft(scrollLeft); } get scrollHeight() { return this.view.scrollHeight; } get renderHeight() { return this.view.renderHeight; } get firstVisibleIndex() { return this.view.firstVisibleIndex; } get lastVisibleIndex() { return this.view.lastVisibleIndex; } get ariaLabel() { return this._ariaLabel; } set ariaLabel(value) { this._ariaLabel = value; this.view.domNode.setAttribute('aria-label', value); } domFocus() { this.view.domNode.focus({ preventScroll: true }); } layout(height, width) { this.view.layout(height, width); } triggerTypeNavigation() { this.typeNavigationController?.trigger(); } setSelection(indexes, browserEvent) { for (const index of indexes) { if (index < 0 || index >= this.length) {