UNPKG

monaco-editor

Version:
1,156 lines (1,155 loc) • 63.7 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 { asCssValueWithDefault, createStyleSheet, EventHelper, getActiveElement, getWindow, isHTMLElement, isMouseEvent } from '../../dom.js'; import { DomEmitter } from '../../event.js'; import { StandardKeyboardEvent } from '../../keyboardEvent.js'; import { Gesture } from '../../touch.js'; import { alert } from '../aria/aria.js'; import { CombinedSpliceable } from './splice.js'; import { binarySearch, firstOrDefault, range } from '../../../common/arrays.js'; import { timeout } from '../../../common/async.js'; import { Color } from '../../../common/color.js'; import { memoize } from '../../../common/decorators.js'; import { Emitter, Event, EventBufferer } from '../../../common/event.js'; import { matchesFuzzy2, matchesPrefix } from '../../../common/filters.js'; import { DisposableStore, dispose } from '../../../common/lifecycle.js'; import { clamp } from '../../../common/numbers.js'; import * as platform from '../../../common/platform.js'; import { isNumber } from '../../../common/types.js'; import './list.css'; import { ListError } from './list.js'; import { ListView } from './listView.js'; import { StandardMouseEvent } from '../../mouseEvent.js'; import { autorun, constObservable } from '../../../common/observable.js'; class TraitRenderer { constructor(trait) { this.trait = trait; this.renderedElements = []; } 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 { get name() { return this._trait; } get renderer() { return new TraitRenderer(this); } constructor(_trait) { this._trait = _trait; this.indexes = []; this.sortedIndexes = []; this._onChange = new Emitter(); this.onChange = this._onChange.event; } splice(start, deleteCount, elements) { const diff = elements.length - deleteCount; const end = start + deleteCount; const sortedIndexes = []; let i = 0; while (i < this.sortedIndexes.length && this.sortedIndexes[i] < start) { sortedIndexes.push(this.sortedIndexes[i++]); } for (let j = 0; j < elements.length; j++) { if (elements[j]) { sortedIndexes.push(j + start); } } while (i < this.sortedIndexes.length && this.sortedIndexes[i] >= end) { sortedIndexes.push(this.sortedIndexes[i++] + diff); } this.renderer.splice(start, deleteCount, elements.length); this._set(sortedIndexes, sortedIndexes); } 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 { 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 { 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, new Array(elements.length).fill(false)); } const pastElementsWithTrait = this.trait.get().map(i => this.identityProvider.getId(this.view.element(i)).toString()); if (pastElementsWithTrait.length === 0) { return this.trait.splice(start, deleteCount, new Array(elements.length).fill(false)); } const pastElementsWithTraitSet = new Set(pastElementsWithTrait); const elementsWithTrait = elements.map(e => pastElementsWithTraitSet.has(this.identityProvider.getId(e).toString())); this.trait.splice(start, deleteCount, elementsWithTrait); } } export function isInputElement(e) { return e.tagName === 'INPUT' || e.tagName === 'TEXTAREA'; } function isListElementDescendantOfClass(e, className) { if (e.classList.contains(className)) { return true; } if (e.classList.contains('monaco-list')) { return false; } if (!e.parentElement) { return false; } return isListElementDescendantOfClass(e.parentElement, className); } export function isMonacoEditor(e) { return isListElementDescendantOfClass(e, 'monaco-editor'); } export function isMonacoCustomToggle(e) { return isListElementDescendantOfClass(e, 'monaco-custom-toggle'); } export function isActionItem(e) { return isListElementDescendantOfClass(e, 'action-item'); } export function isStickyScrollElement(e) { return isListElementDescendantOfClass(e, 'monaco-tree-sticky-row'); } export function isStickyScrollContainer(e) { return e.classList.contains('monaco-tree-sticky-container'); } 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 { get onKeyDown() { return 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.disposables = new DisposableStore(); this.multipleSelectionDisposables = new DisposableStore(); this.multipleSelectionSupport = options.multipleSelectionSupport; this.disposables.add(this.onKeyDown(e => { switch (e.keyCode) { case 3 /* KeyCode.Enter */: return this.onEnter(e); case 16 /* KeyCode.UpArrow */: return this.onUpArrow(e); case 18 /* KeyCode.DownArrow */: return this.onDownArrow(e); case 11 /* KeyCode.PageUp */: return this.onPageUpArrow(e); case 12 /* KeyCode.PageDown */: return this.onPageDownArrow(e); case 9 /* KeyCode.Escape */: return this.onEscape(e); case 31 /* KeyCode.KeyA */: if (this.multipleSelectionSupport && (platform.isMacintosh ? e.metaKey : e.ctrlKey)) { this.onCtrlA(e); } } })); } updateOptions(optionsUpdate) { if (optionsUpdate.multipleSelectionSupport !== undefined) { this.multipleSelectionSupport = optionsUpdate.multipleSelectionSupport; } } 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 >= 98 /* KeyCode.Numpad0 */ && event.keyCode <= 107 /* KeyCode.Numpad9 */) || (event.keyCode >= 85 /* KeyCode.Semicolon */ && event.keyCode <= 95 /* KeyCode.Quote */); } }; class TypeNavigationController { constructor(list, view, keyboardNavigationLabelProvider, keyboardNavigationEventFilter, delegate) { this.list = list; this.view = view; this.keyboardNavigationLabelProvider = keyboardNavigationLabelProvider; this.keyboardNavigationEventFilter = keyboardNavigationEventFilter; this.delegate = delegate; this.enabled = false; this.state = TypeNavigationControllerState.Idle; this.mode = TypeNavigationMode.Automatic; this.triggered = false; this.previouslyFocused = -1; this.enabledDisposables = new DisposableStore(); this.disposables = new DisposableStore(); this.updateOptions(list.options); } updateOptions(options) { if (options.typeNavigationEnabled ?? true) { this.enable(); } else { this.disable(); } this.mode = options.typeNavigationMode ?? TypeNavigationMode.Automatic; } enable() { if (this.enabled) { return; } let typing = false; const onChar = 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)); const onClear = Event.debounce(onChar, () => null, 800, undefined, 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 (typeof ariaLabel === 'string') { alert(ariaLabel); } else if (ariaLabel) { alert(ariaLabel.get()); } } 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 (this.list.options.typeNavigationEnabled) { if (typeof labelStr !== 'undefined') { // If prefix is found, focus and return early if (matchesPrefix(word, labelStr)) { this.previouslyFocused = start; this.list.setFocus([index]); this.list.reveal(index); return; } const fuzzy = matchesFuzzy2(word, labelStr); if (fuzzy) { const fuzzyScore = fuzzy[0].end - fuzzy[0].start; // ensures that when fuzzy matching, doesn't clash with prefix matching (1 input vs 1+ should be prefix and fuzzy respecitvely). Also makes sure that exact matches are prioritized. if (fuzzyScore > 1 && fuzzy.length === 1) { this.previouslyFocused = start; this.list.setFocus([index]); this.list.reveal(index); return; } } } } else 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 { constructor(list, view) { this.list = list; this.view = view; this.disposables = new DisposableStore(); const onKeyDown = Event.chain(this.disposables.add(new DomEmitter(view.domNode, 'keydown')).event, $ => $ .filter(e => !isInputElement(e.target)) .map(e => new StandardKeyboardEvent(e))); const onTab = Event.chain(onKeyDown, $ => $.filter(e => e.keyCode === 2 /* KeyCode.Tab */ && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey)); onTab(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 || !(isHTMLElement(tabIndexElement)) || tabIndexElement.tabIndex === -1) { return; } const style = getWindow(tabIndexElement).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 isMouseEvent(event) && event.button === 2; } const DefaultMultipleSelectionController = { isSelectionSingleChangeEvent, isSelectionRangeChangeEvent }; export class MouseController { constructor(list) { this.list = list; this.disposables = new DisposableStore(); this._onPointer = new Emitter(); this.onPointer = this._onPointer.event; 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 (getActiveElement() !== 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; } if (e.browserEvent.isHandledByList) { return; } e.browserEvent.isHandledByList = true; 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.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; } if (e.browserEvent.isHandledByList) { return; } e.browserEvent.isHandledByList = true; 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 { constructor(styleElement, selectorSuffix) { this.styleElement = styleElement; this.selectorSuffix = selectorSuffix; } style(styles) { const suffix = this.selectorSuffix && `.${this.selectorSuffix}`; const content = []; if (styles.listBackground) { content.push(`.monaco-list${suffix} .monaco-list-rows { background: ${styles.listBackground}; }`); } 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.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}; }`); } /** * Outlines */ const focusAndSelectionOutline = asCssValueWithDefault(styles.listFocusAndSelectionOutline, asCssValueWithDefault(styles.listSelectionOutline, styles.listFocusOutline ?? '')); if (focusAndSelectionOutline) { // default: listFocusOutline content.push(`.monaco-list${suffix}:focus .monaco-list-row.focused.selected { outline: 1px solid ${focusAndSelectionOutline}; outline-offset: -1px;}`); } if (styles.listFocusOutline) { // default: set 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; } `); } const inactiveFocusAndSelectionOutline = asCssValueWithDefault(styles.listSelectionOutline, styles.listInactiveFocusOutline ?? ''); if (inactiveFocusAndSelectionOutline) { content.push(`.monaco-list${suffix} .monaco-list-row.focused.selected { outline: 1px dotted ${inactiveFocusAndSelectionOutline}; outline-offset: -1px; }`); } if (styles.listSelectionOutline) { // default: activeContrastBorder content.push(`.monaco-list${suffix} .monaco-list-row.selected { outline: 1px dotted ${styles.listSelectionOutline}; outline-offset: -1px; }`); } if (styles.listInactiveFocusOutline) { // default: null content.push(`.monaco-list${suffix} .monaco-list-row.focused { outline: 1px dotted ${styles.listInactiveFocusOutline}; outline-offset: -1px; }`); } if (styles.listHoverOutline) { // default: activeContrastBorder content.push(`.monaco-list${suffix} .monaco-list-row:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`); } if (styles.listDropOverBackground) { 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.listDropOverBackground} !important; color: inherit !important; } `); } if (styles.listDropBetweenBackground) { content.push(` .monaco-list${suffix} .monaco-list-rows.drop-target-before .monaco-list-row:first-child::before, .monaco-list${suffix} .monaco-list-row.drop-target-before::before { content: ""; position: absolute; top: 0px; left: 0px; width: 100%; height: 1px; background-color: ${styles.listDropBetweenBackground}; }`); content.push(` .monaco-list${suffix} .monaco-list-rows.drop-target-after .monaco-list-row:last-child::after, .monaco-list${suffix} .monaco-list-row.drop-target-after::after { content: ""; position: absolute; bottom: 0px; left: 0px; width: 100%; height: 1px; background-color: ${styles.listDropBetweenBackground}; }`); } 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'); } } export const unthemedListStyles = { listFocusBackground: '#7FB0D0', listActiveSelectionBackground: '#0E639C', listActiveSelectionForeground: '#FFFFFF', listActiveSelectionIconForeground: '#FFFFFF', listFocusAndSelectionOutline: '#90C2F9', listFocusAndSelectionBackground: '#094771', listFocusAndSelectionForeground: '#FFFFFF', listInactiveSelectionBackground: '#3F3F46', listInactiveSelectionIconForeground: '#FFFFFF', listHoverBackground: '#2A2D2E', listDropOverBackground: '#383B3D', listDropBetweenBackground: '#EEEEEE', treeIndentGuidesStroke: '#a9a9a9', treeInactiveIndentGuidesStroke: Color.fromHex('#a9a9a9').transparent(0.4).toString(), tableColumnsBorder: Color.fromHex('#cccccc').transparent(0.2).toString(), tableOddRowsBackgroundColor: Color.fromHex('#cccccc').transparent(0.04).toString(), listBackground: undefined, listFocusForeground: undefined, listInactiveSelectionForeground: undefined, listInactiveFocusForeground: undefined, listInactiveFocusBackground: undefined, listHoverForeground: undefined, listFocusOutline: undefined, listInactiveFocusOutline: undefined, listSelectionOutline: undefined, listHoverOutline: undefined, treeStickyScrollBackground: undefined, treeStickyScrollBorder: undefined, treeStickyScrollShadow: undefined }; const DefaultOptions = { keyboardSupport: true, mouseSupport: true, multipleSelectionSupport: true, dnd: { getDragURI() { return null; }, onDragStart() { }, onDragOver() { return false; }, drop() { }, dispose() { } } }; // 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 { 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 { constructor(accessibilityProvider) { this.accessibilityProvider = accessibilityProvider; this.templateId = 'a18n'; } renderTemplate(container) { return { container, disposables: new DisposableStore() }; } renderElement(element, index, data) { const ariaLabel = this.accessibilityProvider.getAriaLabel(element); const observable = (ariaLabel && typeof ariaLabel !== 'string') ? ariaLabel : constObservable(ariaLabel); data.disposables.add(autorun(reader => { this.setAriaLabel(reader.readObservable(observable), data.container); })); const ariaLevel = this.accessibilityProvider.getAriaLevel && this.accessibilityProvider.getAriaLevel(element); if (typeof ariaLevel === 'number') { data.container.setAttribute('aria-level', `${ariaLevel}`); } else { data.container.removeAttribute('aria-level'); } } setAriaLabel(ariaLabel, element) { if (ariaLabel) { element.setAttribute('aria-label', ariaLabel); } else { element.removeAttribute('aria-label'); } } disposeElement(element, index, templateData, height) { templateData.disposables.clear(); } disposeTemplate(templateData) { templateData.disposables.dispose(); } } class ListViewDragAndDrop { 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, targetSector, originalEvent) { return this.dnd.onDragOver(data, targetElement, targetIndex, targetSector, originalEvent); } onDragLeave(data, targetElement, targetIndex, originalEvent) { this.dnd.onDragLeave?.(data, targetElement, targetIndex, originalEvent); } onDragEnd(originalEvent) { this.dnd.onDragEnd?.(originalEvent); } drop(data, targetElement, targetIndex, targetSector, originalEvent) { this.dnd.drop(data, targetElement, targetIndex, targetSector, originalEvent); } dispose() { this.dnd.dispose(); } } /** * 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 { 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 onMouseDown() { return this.view.onMouseDown; } get onMouseOver() { return this.view.onMouseOver; } 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 = 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)); const fromKeyUp = 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 }; })); const fromMouse = Event.chain(this.view.onContextMenu, $ => $.filter(_ => !didJustPressContextMenuKey) .map(({ element, index, browserEvent }) => ({ element, index, anchor: new StandardMouseEvent(getWindow(this.view.domNode), browserEvent), browserEvent }))); return Event.any(fromKeyDown, fromKeyUp, fromMouse); } get onKeyDown() { return this.disposables.add(new DomEmitter(this.view.domNode, 'keydown')).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); } constructor(user, container, virtualDelegate, renderers, _options = DefaultOptions) { this.user = user; this._options = _options; this.focus = new Trait('focused'); this.anchor = new Trait('anchor'); this.eventBufferer = new EventBufferer(); this._ariaLabel = ''; this.disposables = new DisposableStore(); this._onDidDispose = new Emitter(); this.onDidDispose = this._onDidDispose.event; const role = this._options.accessibilityProvider && this._options.accessibilityProvider.getWidgetRole ? this._options.accessibilityProvider?.getWidgetRole() : 'list'; this.selection = new SelectionTrait(role !== 'listbox'); 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 = this.createListView(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.getWidgetAria