|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | + * Licensed under the MIT License. See License.txt in the project root for license information. |
| 4 | + *--------------------------------------------------------------------------------------------*/ |
| 5 | + |
| 6 | +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; |
| 7 | +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; |
| 8 | +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; |
| 9 | +import { IEditorContribution } from 'vs/editor/common/editorCommon'; |
| 10 | +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; |
| 11 | +import { EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; |
| 12 | +import { StickyScrollWidget, StickyScrollWidgetState } from './stickyScrollWidget'; |
| 13 | +import { StickyLineCandidateProvider, StickyRange } from './stickyScrollProvider'; |
| 14 | +import { IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents'; |
| 15 | +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; |
| 16 | +import { localize } from 'vs/nls'; |
| 17 | +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; |
| 18 | +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; |
| 19 | +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; |
| 20 | +import { StickyScrollConfig } from './stickyScrollConfig'; |
| 21 | + |
| 22 | +class StickyScrollController extends Disposable implements IEditorContribution { |
| 23 | + |
| 24 | + static readonly ID = 'store.contrib.stickyScrollController'; |
| 25 | + private readonly editor: ICodeEditor; |
| 26 | + private readonly stickyScrollWidget: StickyScrollWidget; |
| 27 | + private readonly stickyLineCandidateProvider: StickyLineCandidateProvider; |
| 28 | + private readonly sessionStore: DisposableStore = new DisposableStore(); |
| 29 | + |
| 30 | + constructor( |
| 31 | + editor: ICodeEditor, |
| 32 | + @ILanguageFeaturesService _languageFeaturesService: ILanguageFeaturesService, |
| 33 | + ) { |
| 34 | + super(); |
| 35 | + this.editor = editor; |
| 36 | + this.stickyScrollWidget = new StickyScrollWidget(this.editor); |
| 37 | + this.stickyLineCandidateProvider = new StickyLineCandidateProvider(this.editor, _languageFeaturesService); |
| 38 | + |
| 39 | + this._register(this.editor.onDidChangeConfiguration(e => { |
| 40 | + if (e.hasChanged(EditorOption.experimental)) { |
| 41 | + this.readConfiguration(); |
| 42 | + } |
| 43 | + })); |
| 44 | + this.readConfiguration(); |
| 45 | + } |
| 46 | + |
| 47 | + private readConfiguration() { |
| 48 | + const options = this.editor.getOption(EditorOption.experimental); |
| 49 | + if (options.stickyScroll.enabled === false) { |
| 50 | + this.editor.removeOverlayWidget(this.stickyScrollWidget); |
| 51 | + this.sessionStore.clear(); |
| 52 | + return; |
| 53 | + } else { |
| 54 | + this.editor.addOverlayWidget(this.stickyScrollWidget); |
| 55 | + this.sessionStore.add(this.editor.onDidScrollChange(() => this.renderStickyScroll())); |
| 56 | + this.sessionStore.add(this.editor.onDidLayoutChange(() => this.onDidResize())); |
| 57 | + this.sessionStore.add(this.editor.onDidChangeModelTokens((e) => this.onTokensChange(e))); |
| 58 | + this.sessionStore.add(this.stickyLineCandidateProvider.onStickyScrollChange(() => this.renderStickyScroll())); |
| 59 | + const lineNumberOption = this.editor.getOption(EditorOption.lineNumbers); |
| 60 | + if (lineNumberOption.renderType === RenderLineNumbersType.Relative) { |
| 61 | + this.sessionStore.add(this.editor.onDidChangeCursorPosition(() => this.renderStickyScroll())); |
| 62 | + } |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + private needsUpdate(event: IModelTokensChangedEvent) { |
| 67 | + const stickyLineNumbers = this.stickyScrollWidget.getCurrentLines(); |
| 68 | + for (const stickyLineNumber of stickyLineNumbers) { |
| 69 | + for (const range of event.ranges) { |
| 70 | + if (stickyLineNumber >= range.fromLineNumber && stickyLineNumber <= range.toLineNumber) { |
| 71 | + return true; |
| 72 | + } |
| 73 | + } |
| 74 | + } |
| 75 | + return false; |
| 76 | + } |
| 77 | + |
| 78 | + private onTokensChange(event: IModelTokensChangedEvent) { |
| 79 | + if (this.needsUpdate(event)) { |
| 80 | + this.renderStickyScroll(); |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + private onDidResize() { |
| 85 | + const width = this.editor.getLayoutInfo().width - this.editor.getLayoutInfo().minimap.minimapCanvasOuterWidth - this.editor.getLayoutInfo().verticalScrollbarWidth; |
| 86 | + this.stickyScrollWidget.getDomNode().style.width = `${width}px`; |
| 87 | + } |
| 88 | + |
| 89 | + private renderStickyScroll() { |
| 90 | + if (!(this.editor.hasModel())) { |
| 91 | + return; |
| 92 | + } |
| 93 | + const model = this.editor.getModel(); |
| 94 | + if (this.stickyLineCandidateProvider.getVersionId() !== model.getVersionId()) { |
| 95 | + // Old _ranges not updated yet |
| 96 | + return; |
| 97 | + } |
| 98 | + this.stickyScrollWidget.setState(this.getScrollWidgetState()); |
| 99 | + } |
| 100 | + |
| 101 | + private getScrollWidgetState(): StickyScrollWidgetState { |
| 102 | + const lineHeight: number = this.editor.getOption(EditorOption.lineHeight); |
| 103 | + const maxNumberStickyLines = this.editor.getOption(EditorOption.experimental).stickyScroll.maxLineCount; |
| 104 | + const scrollTop: number = this.editor.getScrollTop(); |
| 105 | + let lastLineRelativePosition: number = 0; |
| 106 | + const lineNumbers: number[] = []; |
| 107 | + const arrayVisibleRanges = this.editor.getVisibleRanges(); |
| 108 | + if (arrayVisibleRanges.length !== 0) { |
| 109 | + const fullVisibleRange = new StickyRange(arrayVisibleRanges[0].startLineNumber, arrayVisibleRanges[arrayVisibleRanges.length - 1].endLineNumber); |
| 110 | + const candidateRanges = this.stickyLineCandidateProvider.getCandidateStickyLinesIntersecting(fullVisibleRange); |
| 111 | + for (const range of candidateRanges) { |
| 112 | + const start = range.startLineNumber; |
| 113 | + const end = range.endLineNumber; |
| 114 | + const depth = range.nestingDepth; |
| 115 | + if (end - start > 0) { |
| 116 | + const topOfElementAtDepth = (depth - 1) * lineHeight; |
| 117 | + const bottomOfElementAtDepth = depth * lineHeight; |
| 118 | + |
| 119 | + const bottomOfBeginningLine = this.editor.getBottomForLineNumber(start) - scrollTop; |
| 120 | + const topOfEndLine = this.editor.getTopForLineNumber(end) - scrollTop; |
| 121 | + const bottomOfEndLine = this.editor.getBottomForLineNumber(end) - scrollTop; |
| 122 | + |
| 123 | + if (topOfElementAtDepth > topOfEndLine && topOfElementAtDepth <= bottomOfEndLine) { |
| 124 | + lineNumbers.push(start); |
| 125 | + lastLineRelativePosition = bottomOfEndLine - bottomOfElementAtDepth; |
| 126 | + break; |
| 127 | + } |
| 128 | + else if (bottomOfElementAtDepth > bottomOfBeginningLine && bottomOfElementAtDepth <= bottomOfEndLine) { |
| 129 | + lineNumbers.push(start); |
| 130 | + } |
| 131 | + if (lineNumbers.length === maxNumberStickyLines) { |
| 132 | + break; |
| 133 | + } |
| 134 | + } |
| 135 | + } |
| 136 | + } |
| 137 | + return new StickyScrollWidgetState(lineNumbers, lastLineRelativePosition); |
| 138 | + } |
| 139 | + |
| 140 | + override dispose(): void { |
| 141 | + super.dispose(); |
| 142 | + this.sessionStore.dispose(); |
| 143 | + } |
| 144 | +} |
| 145 | + |
| 146 | +registerEditorContribution(StickyScrollController.ID, StickyScrollController); |
| 147 | + |
| 148 | +registerAction2(class ToggleStickyScroll extends Action2 { |
| 149 | + |
| 150 | + constructor() { |
| 151 | + super({ |
| 152 | + id: 'stickyScroll.toggle', |
| 153 | + title: { |
| 154 | + value: localize('cmd.toggle', "Toggle Sticky Scroll"), |
| 155 | + mnemonicTitle: localize('miStickyScroll', "&&Sticky Scroll"), |
| 156 | + original: 'Toggle Sticky Scroll', |
| 157 | + }, |
| 158 | + // Hardcoding due to import violation |
| 159 | + category: { value: localize('view', "View"), original: 'View' }, |
| 160 | + toggled: ContextKeyExpr.equals('config.stickyScroll.enabled', true), |
| 161 | + menu: [ |
| 162 | + { id: MenuId.CommandPalette }, |
| 163 | + { id: MenuId.MenubarViewMenu, group: '5_editor', order: 6 }, |
| 164 | + ] |
| 165 | + }); |
| 166 | + } |
| 167 | + |
| 168 | + run(accessor: ServicesAccessor): void { |
| 169 | + const config = accessor.get(IConfigurationService); |
| 170 | + const value = StickyScrollConfig.IsEnabled.bindTo(config).getValue(); |
| 171 | + StickyScrollConfig.IsEnabled.bindTo(config).updateValue(!value); |
| 172 | + } |
| 173 | +}); |
| 174 | + |
0 commit comments