UNPKG

monaco-editor

Version:
535 lines (534 loc) • 27.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; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var StickyScrollController_1; import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; import { StickyScrollWidget, StickyScrollWidgetState } from './stickyScrollWidget.js'; import { StickyLineCandidateProvider } from './stickyScrollProvider.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; import { ClickLinkGesture } from '../../gotoSymbol/browser/link/clickLinkGesture.js'; import { Range } from '../../../common/core/range.js'; import { getDefinitionsAtPosition } from '../../gotoSymbol/browser/goToSymbol.js'; import { goToDefinitionWithLocation } from '../../inlayHints/browser/inlayHintsLocations.js'; import { Position } from '../../../common/core/position.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js'; import { ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js'; import * as dom from '../../../../base/browser/dom.js'; import { StickyRange } from './stickyScrollElement.js'; import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; import { FoldingController } from '../../folding/browser/folding.js'; import { toggleCollapseState } from '../../folding/browser/foldingModel.js'; let StickyScrollController = class StickyScrollController extends Disposable { static { StickyScrollController_1 = this; } static { this.ID = 'store.contrib.stickyScrollController'; } constructor(_editor, _contextMenuService, _languageFeaturesService, _instaService, _languageConfigurationService, _languageFeatureDebounceService, _contextKeyService) { super(); this._editor = _editor; this._contextMenuService = _contextMenuService; this._languageFeaturesService = _languageFeaturesService; this._instaService = _instaService; this._contextKeyService = _contextKeyService; this._sessionStore = new DisposableStore(); this._maxStickyLines = Number.MAX_SAFE_INTEGER; this._candidateDefinitionsLength = -1; this._focusedStickyElementIndex = -1; this._enabled = false; this._focused = false; this._positionRevealed = false; this._onMouseDown = false; this._endLineNumbers = []; this._stickyScrollWidget = new StickyScrollWidget(this._editor); this._stickyLineCandidateProvider = new StickyLineCandidateProvider(this._editor, _languageFeaturesService, _languageConfigurationService); this._register(this._stickyScrollWidget); this._register(this._stickyLineCandidateProvider); this._widgetState = StickyScrollWidgetState.Empty; this._onDidResize(); this._readConfiguration(); const stickyScrollDomNode = this._stickyScrollWidget.getDomNode(); this._register(this._editor.onDidChangeConfiguration(e => { this._readConfigurationChange(e); })); this._register(dom.addDisposableListener(stickyScrollDomNode, dom.EventType.CONTEXT_MENU, async (event) => { this._onContextMenu(dom.getWindow(stickyScrollDomNode), event); })); this._stickyScrollFocusedContextKey = EditorContextKeys.stickyScrollFocused.bindTo(this._contextKeyService); this._stickyScrollVisibleContextKey = EditorContextKeys.stickyScrollVisible.bindTo(this._contextKeyService); const focusTracker = this._register(dom.trackFocus(stickyScrollDomNode)); this._register(focusTracker.onDidBlur(_ => { // Suppose that the blurring is caused by scrolling, then keep the focus on the sticky scroll // This is determined by the fact that the height of the widget has become zero and there has been no position revealing if (this._positionRevealed === false && stickyScrollDomNode.clientHeight === 0) { this._focusedStickyElementIndex = -1; this.focus(); } // In all other casees, dispose the focus on the sticky scroll else { this._disposeFocusStickyScrollStore(); } })); this._register(focusTracker.onDidFocus(_ => { this.focus(); })); this._registerMouseListeners(); // Suppose that mouse down on the sticky scroll, then do not focus on the sticky scroll because this will be followed by the revealing of a position this._register(dom.addDisposableListener(stickyScrollDomNode, dom.EventType.MOUSE_DOWN, (e) => { this._onMouseDown = true; })); } static get(editor) { return editor.getContribution(StickyScrollController_1.ID); } _disposeFocusStickyScrollStore() { this._stickyScrollFocusedContextKey.set(false); this._focusDisposableStore?.dispose(); this._focused = false; this._positionRevealed = false; this._onMouseDown = false; } focus() { // If the mouse is down, do not focus on the sticky scroll if (this._onMouseDown) { this._onMouseDown = false; this._editor.focus(); return; } const focusState = this._stickyScrollFocusedContextKey.get(); if (focusState === true) { return; } this._focused = true; this._focusDisposableStore = new DisposableStore(); this._stickyScrollFocusedContextKey.set(true); this._focusedStickyElementIndex = this._stickyScrollWidget.lineNumbers.length - 1; this._stickyScrollWidget.focusLineWithIndex(this._focusedStickyElementIndex); } focusNext() { if (this._focusedStickyElementIndex < this._stickyScrollWidget.lineNumberCount - 1) { this._focusNav(true); } } focusPrevious() { if (this._focusedStickyElementIndex > 0) { this._focusNav(false); } } selectEditor() { this._editor.focus(); } // True is next, false is previous _focusNav(direction) { this._focusedStickyElementIndex = direction ? this._focusedStickyElementIndex + 1 : this._focusedStickyElementIndex - 1; this._stickyScrollWidget.focusLineWithIndex(this._focusedStickyElementIndex); } goToFocused() { const lineNumbers = this._stickyScrollWidget.lineNumbers; this._disposeFocusStickyScrollStore(); this._revealPosition({ lineNumber: lineNumbers[this._focusedStickyElementIndex], column: 1 }); } _revealPosition(position) { this._reveaInEditor(position, () => this._editor.revealPosition(position)); } _revealLineInCenterIfOutsideViewport(position) { this._reveaInEditor(position, () => this._editor.revealLineInCenterIfOutsideViewport(position.lineNumber, 0 /* ScrollType.Smooth */)); } _reveaInEditor(position, revealFunction) { if (this._focused) { this._disposeFocusStickyScrollStore(); } this._positionRevealed = true; revealFunction(); this._editor.setSelection(Range.fromPositions(position)); this._editor.focus(); } _registerMouseListeners() { const sessionStore = this._register(new DisposableStore()); const gesture = this._register(new ClickLinkGesture(this._editor, { extractLineNumberFromMouseEvent: (e) => { const position = this._stickyScrollWidget.getEditorPositionFromNode(e.target.element); return position ? position.lineNumber : 0; } })); const getMouseEventTarget = (mouseEvent) => { if (!this._editor.hasModel()) { return null; } if (mouseEvent.target.type !== 12 /* MouseTargetType.OVERLAY_WIDGET */ || mouseEvent.target.detail !== this._stickyScrollWidget.getId()) { // not hovering over our widget return null; } const mouseTargetElement = mouseEvent.target.element; if (!mouseTargetElement || mouseTargetElement.innerText !== mouseTargetElement.innerHTML) { // not on a span element rendering text return null; } const position = this._stickyScrollWidget.getEditorPositionFromNode(mouseTargetElement); if (!position) { // not hovering a sticky scroll line return null; } return { range: new Range(position.lineNumber, position.column, position.lineNumber, position.column + mouseTargetElement.innerText.length), textElement: mouseTargetElement }; }; const stickyScrollWidgetDomNode = this._stickyScrollWidget.getDomNode(); this._register(dom.addStandardDisposableListener(stickyScrollWidgetDomNode, dom.EventType.CLICK, (mouseEvent) => { if (mouseEvent.ctrlKey || mouseEvent.altKey || mouseEvent.metaKey) { // modifier pressed return; } if (!mouseEvent.leftButton) { // not left click return; } if (mouseEvent.shiftKey) { // shift click const lineIndex = this._stickyScrollWidget.getLineIndexFromChildDomNode(mouseEvent.target); if (lineIndex === null) { return; } const position = new Position(this._endLineNumbers[lineIndex], 1); this._revealLineInCenterIfOutsideViewport(position); return; } const isInFoldingIconDomNode = this._stickyScrollWidget.isInFoldingIconDomNode(mouseEvent.target); if (isInFoldingIconDomNode) { // clicked on folding icon const lineNumber = this._stickyScrollWidget.getLineNumberFromChildDomNode(mouseEvent.target); this._toggleFoldingRegionForLine(lineNumber); return; } const isInStickyLine = this._stickyScrollWidget.isInStickyLine(mouseEvent.target); if (!isInStickyLine) { return; } // normal click let position = this._stickyScrollWidget.getEditorPositionFromNode(mouseEvent.target); if (!position) { const lineNumber = this._stickyScrollWidget.getLineNumberFromChildDomNode(mouseEvent.target); if (lineNumber === null) { // not hovering a sticky scroll line return; } position = new Position(lineNumber, 1); } this._revealPosition(position); })); this._register(dom.addStandardDisposableListener(stickyScrollWidgetDomNode, dom.EventType.MOUSE_MOVE, (mouseEvent) => { if (mouseEvent.shiftKey) { const currentEndForLineIndex = this._stickyScrollWidget.getLineIndexFromChildDomNode(mouseEvent.target); if (currentEndForLineIndex === null || this._showEndForLine !== null && this._showEndForLine === currentEndForLineIndex) { return; } this._showEndForLine = currentEndForLineIndex; this._renderStickyScroll(); return; } if (this._showEndForLine !== undefined) { this._showEndForLine = undefined; this._renderStickyScroll(); } })); this._register(dom.addDisposableListener(stickyScrollWidgetDomNode, dom.EventType.MOUSE_LEAVE, (e) => { if (this._showEndForLine !== undefined) { this._showEndForLine = undefined; this._renderStickyScroll(); } })); this._register(gesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, _keyboardEvent]) => { const mouseTarget = getMouseEventTarget(mouseEvent); if (!mouseTarget || !mouseEvent.hasTriggerModifier || !this._editor.hasModel()) { sessionStore.clear(); return; } const { range, textElement } = mouseTarget; if (!range.equalsRange(this._stickyRangeProjectedOnEditor)) { this._stickyRangeProjectedOnEditor = range; sessionStore.clear(); } else if (textElement.style.textDecoration === 'underline') { return; } const cancellationToken = new CancellationTokenSource(); sessionStore.add(toDisposable(() => cancellationToken.dispose(true))); let currentHTMLChild; getDefinitionsAtPosition(this._languageFeaturesService.definitionProvider, this._editor.getModel(), new Position(range.startLineNumber, range.startColumn + 1), false, cancellationToken.token).then((candidateDefinitions => { if (cancellationToken.token.isCancellationRequested) { return; } if (candidateDefinitions.length !== 0) { this._candidateDefinitionsLength = candidateDefinitions.length; const childHTML = textElement; if (currentHTMLChild !== childHTML) { sessionStore.clear(); currentHTMLChild = childHTML; currentHTMLChild.style.textDecoration = 'underline'; sessionStore.add(toDisposable(() => { currentHTMLChild.style.textDecoration = 'none'; })); } else if (!currentHTMLChild) { currentHTMLChild = childHTML; currentHTMLChild.style.textDecoration = 'underline'; sessionStore.add(toDisposable(() => { currentHTMLChild.style.textDecoration = 'none'; })); } } else { sessionStore.clear(); } })); })); this._register(gesture.onCancel(() => { sessionStore.clear(); })); this._register(gesture.onExecute(async (e) => { if (e.target.type !== 12 /* MouseTargetType.OVERLAY_WIDGET */ || e.target.detail !== this._stickyScrollWidget.getId()) { // not hovering over our widget return; } const position = this._stickyScrollWidget.getEditorPositionFromNode(e.target.element); if (!position) { // not hovering a sticky scroll line return; } if (!this._editor.hasModel() || !this._stickyRangeProjectedOnEditor) { return; } if (this._candidateDefinitionsLength > 1) { if (this._focused) { this._disposeFocusStickyScrollStore(); } this._revealPosition({ lineNumber: position.lineNumber, column: 1 }); } this._instaService.invokeFunction(goToDefinitionWithLocation, e, this._editor, { uri: this._editor.getModel().uri, range: this._stickyRangeProjectedOnEditor }); })); } _onContextMenu(targetWindow, e) { const event = new StandardMouseEvent(targetWindow, e); this._contextMenuService.showContextMenu({ menuId: MenuId.StickyScrollContext, getAnchor: () => event, }); } _toggleFoldingRegionForLine(line) { if (!this._foldingModel || line === null) { return; } const stickyLine = this._stickyScrollWidget.getRenderedStickyLine(line); const foldingIcon = stickyLine?.foldingIcon; if (!foldingIcon) { return; } toggleCollapseState(this._foldingModel, Number.MAX_VALUE, [line]); foldingIcon.isCollapsed = !foldingIcon.isCollapsed; const scrollTop = (foldingIcon.isCollapsed ? this._editor.getTopForLineNumber(foldingIcon.foldingEndLine) : this._editor.getTopForLineNumber(foldingIcon.foldingStartLine)) - this._editor.getOption(67 /* EditorOption.lineHeight */) * stickyLine.index + 1; this._editor.setScrollTop(scrollTop); this._renderStickyScroll(line); } _readConfiguration() { const options = this._editor.getOption(116 /* EditorOption.stickyScroll */); if (options.enabled === false) { this._editor.removeOverlayWidget(this._stickyScrollWidget); this._sessionStore.clear(); this._enabled = false; return; } else if (options.enabled && !this._enabled) { // When sticky scroll was just enabled, add the listeners on the sticky scroll this._editor.addOverlayWidget(this._stickyScrollWidget); this._sessionStore.add(this._editor.onDidScrollChange((e) => { if (e.scrollTopChanged) { this._showEndForLine = undefined; this._renderStickyScroll(); } })); this._sessionStore.add(this._editor.onDidLayoutChange(() => this._onDidResize())); this._sessionStore.add(this._editor.onDidChangeModelTokens((e) => this._onTokensChange(e))); this._sessionStore.add(this._stickyLineCandidateProvider.onDidChangeStickyScroll(() => { this._showEndForLine = undefined; this._renderStickyScroll(); })); this._enabled = true; } const lineNumberOption = this._editor.getOption(68 /* EditorOption.lineNumbers */); if (lineNumberOption.renderType === 2 /* RenderLineNumbersType.Relative */) { this._sessionStore.add(this._editor.onDidChangeCursorPosition(() => { this._showEndForLine = undefined; this._renderStickyScroll(0); })); } } _readConfigurationChange(event) { if (event.hasChanged(116 /* EditorOption.stickyScroll */) || event.hasChanged(73 /* EditorOption.minimap */) || event.hasChanged(67 /* EditorOption.lineHeight */) || event.hasChanged(111 /* EditorOption.showFoldingControls */) || event.hasChanged(68 /* EditorOption.lineNumbers */)) { this._readConfiguration(); } if (event.hasChanged(68 /* EditorOption.lineNumbers */)) { this._renderStickyScroll(0); } } _needsUpdate(event) { const stickyLineNumbers = this._stickyScrollWidget.getCurrentLines(); for (const stickyLineNumber of stickyLineNumbers) { for (const range of event.ranges) { if (stickyLineNumber >= range.fromLineNumber && stickyLineNumber <= range.toLineNumber) { return true; } } } return false; } _onTokensChange(event) { if (this._needsUpdate(event)) { // Rebuilding the whole widget from line 0 this._renderStickyScroll(0); } } _onDidResize() { const layoutInfo = this._editor.getLayoutInfo(); // Make sure sticky scroll doesn't take up more than 25% of the editor const theoreticalLines = layoutInfo.height / this._editor.getOption(67 /* EditorOption.lineHeight */); this._maxStickyLines = Math.round(theoreticalLines * .25); } async _renderStickyScroll(rebuildFromLine) { const model = this._editor.getModel(); if (!model || model.isTooLargeForTokenization()) { this._resetState(); return; } const nextRebuildFromLine = this._updateAndGetMinRebuildFromLine(rebuildFromLine); const stickyWidgetVersion = this._stickyLineCandidateProvider.getVersionId(); const shouldUpdateState = stickyWidgetVersion === undefined || stickyWidgetVersion === model.getVersionId(); if (shouldUpdateState) { if (!this._focused) { await this._updateState(nextRebuildFromLine); } else { // Suppose that previously the sticky scroll widget had height 0, then if there are visible lines, set the last line as focused if (this._focusedStickyElementIndex === -1) { await this._updateState(nextRebuildFromLine); this._focusedStickyElementIndex = this._stickyScrollWidget.lineNumberCount - 1; if (this._focusedStickyElementIndex !== -1) { this._stickyScrollWidget.focusLineWithIndex(this._focusedStickyElementIndex); } } else { const focusedStickyElementLineNumber = this._stickyScrollWidget.lineNumbers[this._focusedStickyElementIndex]; await this._updateState(nextRebuildFromLine); // Suppose that after setting the state, there are no sticky lines, set the focused index to -1 if (this._stickyScrollWidget.lineNumberCount === 0) { this._focusedStickyElementIndex = -1; } else { const previousFocusedLineNumberExists = this._stickyScrollWidget.lineNumbers.includes(focusedStickyElementLineNumber); // If the line number is still there, do not change anything // If the line number is not there, set the new focused line to be the last line if (!previousFocusedLineNumberExists) { this._focusedStickyElementIndex = this._stickyScrollWidget.lineNumberCount - 1; } this._stickyScrollWidget.focusLineWithIndex(this._focusedStickyElementIndex); } } } } } _updateAndGetMinRebuildFromLine(rebuildFromLine) { if (rebuildFromLine !== undefined) { const minRebuildFromLineOrInfinity = this._minRebuildFromLine !== undefined ? this._minRebuildFromLine : Infinity; this._minRebuildFromLine = Math.min(rebuildFromLine, minRebuildFromLineOrInfinity); } return this._minRebuildFromLine; } async _updateState(rebuildFromLine) { this._minRebuildFromLine = undefined; this._foldingModel = await FoldingController.get(this._editor)?.getFoldingModel() ?? undefined; this._widgetState = this.findScrollWidgetState(); const stickyWidgetHasLines = this._widgetState.startLineNumbers.length > 0; this._stickyScrollVisibleContextKey.set(stickyWidgetHasLines); this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine); } async _resetState() { this._minRebuildFromLine = undefined; this._foldingModel = undefined; this._widgetState = StickyScrollWidgetState.Empty; this._stickyScrollVisibleContextKey.set(false); this._stickyScrollWidget.setState(undefined, undefined); } findScrollWidgetState() { const lineHeight = this._editor.getOption(67 /* EditorOption.lineHeight */); const maxNumberStickyLines = Math.min(this._maxStickyLines, this._editor.getOption(116 /* EditorOption.stickyScroll */).maxLineCount); const scrollTop = this._editor.getScrollTop(); let lastLineRelativePosition = 0; const startLineNumbers = []; const endLineNumbers = []; const arrayVisibleRanges = this._editor.getVisibleRanges(); if (arrayVisibleRanges.length !== 0) { const fullVisibleRange = new StickyRange(arrayVisibleRanges[0].startLineNumber, arrayVisibleRanges[arrayVisibleRanges.length - 1].endLineNumber); const candidateRanges = this._stickyLineCandidateProvider.getCandidateStickyLinesIntersecting(fullVisibleRange); for (const range of candidateRanges) { const start = range.startLineNumber; const end = range.endLineNumber; const depth = range.nestingDepth; if (end - start > 0) { const topOfElementAtDepth = (depth - 1) * lineHeight; const bottomOfElementAtDepth = depth * lineHeight; const bottomOfBeginningLine = this._editor.getBottomForLineNumber(start) - scrollTop; const topOfEndLine = this._editor.getTopForLineNumber(end) - scrollTop; const bottomOfEndLine = this._editor.getBottomForLineNumber(end) - scrollTop; if (topOfElementAtDepth > topOfEndLine && topOfElementAtDepth <= bottomOfEndLine) { startLineNumbers.push(start); endLineNumbers.push(end + 1); lastLineRelativePosition = bottomOfEndLine - bottomOfElementAtDepth; break; } else if (bottomOfElementAtDepth > bottomOfBeginningLine && bottomOfElementAtDepth <= bottomOfEndLine) { startLineNumbers.push(start); endLineNumbers.push(end + 1); } if (startLineNumbers.length === maxNumberStickyLines) { break; } } } } this._endLineNumbers = endLineNumbers; return new StickyScrollWidgetState(startLineNumbers, endLineNumbers, lastLineRelativePosition, this._showEndForLine); } dispose() { super.dispose(); this._sessionStore.dispose(); } }; StickyScrollController = StickyScrollController_1 = __decorate([ __param(1, IContextMenuService), __param(2, ILanguageFeaturesService), __param(3, IInstantiationService), __param(4, ILanguageConfigurationService), __param(5, ILanguageFeatureDebounceService), __param(6, IContextKeyService) ], StickyScrollController); export { StickyScrollController };