Path: blob/main/src/vs/workbench/contrib/debug/browser/callStackWidget.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as dom from '../../../../base/browser/dom.js';6import { Button } from '../../../../base/browser/ui/button/button.js';7import { IListRenderer, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';8import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';9import { assertNever } from '../../../../base/common/assert.js';10import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';11import { Codicon } from '../../../../base/common/codicons.js';12import { Emitter, Event } from '../../../../base/common/event.js';13import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';14import { autorun, autorunWithStore, derived, IObservable, ISettableObservable, observableValue, transaction } from '../../../../base/common/observable.js';15import { ThemeIcon } from '../../../../base/common/themables.js';16import { Constants } from '../../../../base/common/uint.js';17import { URI } from '../../../../base/common/uri.js';18import { generateUuid } from '../../../../base/common/uuid.js';19import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';20import { EditorContributionCtor, EditorContributionInstantiation, IEditorContributionDescription } from '../../../../editor/browser/editorExtensions.js';21import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';22import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js';23import { IEditorOptions } from '../../../../editor/common/config/editorOptions.js';24import { Position } from '../../../../editor/common/core/position.js';25import { Range } from '../../../../editor/common/core/range.js';26import { IWordAtPosition } from '../../../../editor/common/core/wordHelper.js';27import { IEditorContribution, IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js';28import { Location } from '../../../../editor/common/languages.js';29import { ITextModelService } from '../../../../editor/common/services/resolverService.js';30import { ClickLinkGesture, ClickLinkMouseEvent } from '../../../../editor/contrib/gotoSymbol/browser/link/clickLinkGesture.js';31import { localize, localize2 } from '../../../../nls.js';32import { createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';33import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';34import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';35import { TextEditorSelectionRevealType } from '../../../../platform/editor/common/editor.js';36import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';37import { ILabelService } from '../../../../platform/label/common/label.js';38import { WorkbenchList } from '../../../../platform/list/browser/listService.js';39import { INotificationService } from '../../../../platform/notification/common/notification.js';40import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js';41import { ResourceLabel } from '../../../browser/labels.js';42import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';43import { makeStackFrameColumnDecoration, TOP_STACK_FRAME_DECORATION } from './callStackEditorContribution.js';44import './media/callStackWidget.css';454647export class CallStackFrame {48constructor(49public readonly name: string,50public readonly source?: URI,51public readonly line = 1,52public readonly column = 1,53) { }54}5556export class SkippedCallFrames {57constructor(58public readonly label: string,59public readonly load: (token: CancellationToken) => Promise<AnyStackFrame[]>,60) { }61}6263export abstract class CustomStackFrame {64public readonly showHeader = observableValue('CustomStackFrame.showHeader', true);65public abstract readonly height: IObservable<number>;66public abstract readonly label: string;67public icon?: ThemeIcon;68public abstract render(container: HTMLElement): IDisposable;69public renderActions?(container: HTMLElement): IDisposable;70}7172export type AnyStackFrame = SkippedCallFrames | CallStackFrame | CustomStackFrame;7374interface IFrameLikeItem {75readonly collapsed: ISettableObservable<boolean>;76readonly height: IObservable<number>;77}7879class WrappedCallStackFrame extends CallStackFrame implements IFrameLikeItem {80public readonly editorHeight = observableValue('WrappedCallStackFrame.height', this.source ? 100 : 0);81public readonly collapsed = observableValue('WrappedCallStackFrame.collapsed', false);8283public readonly height = derived(reader => {84return this.collapsed.read(reader) ? CALL_STACK_WIDGET_HEADER_HEIGHT : CALL_STACK_WIDGET_HEADER_HEIGHT + this.editorHeight.read(reader);85});8687constructor(original: CallStackFrame) {88super(original.name, original.source, original.line, original.column);89}90}9192class WrappedCustomStackFrame implements IFrameLikeItem {93public readonly collapsed = observableValue('WrappedCallStackFrame.collapsed', false);9495public readonly height = derived(reader => {96const headerHeight = this.original.showHeader.read(reader) ? CALL_STACK_WIDGET_HEADER_HEIGHT : 0;97return this.collapsed.read(reader) ? headerHeight : headerHeight + this.original.height.read(reader);98});99100constructor(public readonly original: CustomStackFrame) { }101}102103const isFrameLike = (item: unknown): item is IFrameLikeItem =>104item instanceof WrappedCallStackFrame || item instanceof WrappedCustomStackFrame;105106type ListItem = WrappedCallStackFrame | SkippedCallFrames | WrappedCustomStackFrame;107108const WIDGET_CLASS_NAME = 'multiCallStackWidget';109110/**111* A reusable widget that displays a call stack as a series of editors. Note112* that this both used in debug's exception widget as well as in the testing113* call stack view.114*/115export class CallStackWidget extends Disposable {116private readonly list: WorkbenchList<ListItem>;117private readonly layoutEmitter = this._register(new Emitter<void>());118private readonly currentFramesDs = this._register(new DisposableStore());119private cts?: CancellationTokenSource;120121public get onDidChangeContentHeight() {122return this.list.onDidChangeContentHeight;123}124125public get onDidScroll() {126return this.list.onDidScroll;127}128129public get contentHeight() {130return this.list.contentHeight;131}132133constructor(134container: HTMLElement,135containingEditor: ICodeEditor | undefined,136@IInstantiationService instantiationService: IInstantiationService,137) {138super();139140container.classList.add(WIDGET_CLASS_NAME);141this._register(toDisposable(() => container.classList.remove(WIDGET_CLASS_NAME)));142143this.list = this._register(instantiationService.createInstance(144WorkbenchList,145'TestResultStackWidget',146container,147new StackDelegate(),148[149instantiationService.createInstance(FrameCodeRenderer, containingEditor, this.layoutEmitter.event),150instantiationService.createInstance(MissingCodeRenderer),151instantiationService.createInstance(CustomRenderer),152instantiationService.createInstance(SkippedRenderer, (i) => this.loadFrame(i)),153],154{155multipleSelectionSupport: false,156mouseSupport: false,157keyboardSupport: false,158setRowLineHeight: false,159alwaysConsumeMouseWheel: false,160accessibilityProvider: instantiationService.createInstance(StackAccessibilityProvider),161}162) as WorkbenchList<ListItem>);163}164165/** Replaces the call frames display in the view. */166public setFrames(frames: AnyStackFrame[]): void {167// cancel any existing load168this.currentFramesDs.clear();169this.cts = new CancellationTokenSource();170this._register(toDisposable(() => this.cts!.dispose(true)));171172this.list.splice(0, this.list.length, this.mapFrames(frames));173}174175public layout(height?: number, width?: number): void {176this.list.layout(height, width);177this.layoutEmitter.fire();178}179180public collapseAll() {181transaction(tx => {182for (let i = 0; i < this.list.length; i++) {183const frame = this.list.element(i);184if (isFrameLike(frame)) {185frame.collapsed.set(true, tx);186}187}188});189}190191private async loadFrame(replacing: SkippedCallFrames): Promise<void> {192if (!this.cts) {193return;194}195196const frames = await replacing.load(this.cts.token);197if (this.cts.token.isCancellationRequested) {198return;199}200201const index = this.list.indexOf(replacing);202this.list.splice(index, 1, this.mapFrames(frames));203}204205private mapFrames(frames: AnyStackFrame[]): ListItem[] {206const result: ListItem[] = [];207for (const frame of frames) {208if (frame instanceof SkippedCallFrames) {209result.push(frame);210continue;211}212213const wrapped = frame instanceof CustomStackFrame214? new WrappedCustomStackFrame(frame) : new WrappedCallStackFrame(frame);215result.push(wrapped);216217this.currentFramesDs.add(autorun(reader => {218const height = wrapped.height.read(reader);219const idx = this.list.indexOf(wrapped);220if (idx !== -1) {221this.list.updateElementHeight(idx, height);222}223}));224}225226return result;227}228}229230class StackAccessibilityProvider implements IListAccessibilityProvider<ListItem> {231constructor(@ILabelService private readonly labelService: ILabelService) { }232233getAriaLabel(e: ListItem): string | IObservable<string> | null {234if (e instanceof SkippedCallFrames) {235return e.label;236}237238if (e instanceof WrappedCustomStackFrame) {239return e.original.label;240}241242if (e instanceof CallStackFrame) {243if (e.source && e.line) {244return localize({245comment: ['{0} is an extension-defined label, then line number and filename'],246key: 'stackTraceLabel',247}, '{0}, line {1} in {2}', e.name, e.line, this.labelService.getUriLabel(e.source, { relative: true }));248}249250return e.name;251}252253assertNever(e);254}255getWidgetAriaLabel(): string {256return localize('stackTrace', 'Stack Trace');257}258}259260class StackDelegate implements IListVirtualDelegate<ListItem> {261getHeight(element: ListItem): number {262if (element instanceof CallStackFrame || element instanceof WrappedCustomStackFrame) {263return element.height.get();264}265if (element instanceof SkippedCallFrames) {266return CALL_STACK_WIDGET_HEADER_HEIGHT;267}268269assertNever(element);270}271272getTemplateId(element: ListItem): string {273if (element instanceof CallStackFrame) {274return element.source ? FrameCodeRenderer.templateId : MissingCodeRenderer.templateId;275}276if (element instanceof SkippedCallFrames) {277return SkippedRenderer.templateId;278}279if (element instanceof WrappedCustomStackFrame) {280return CustomRenderer.templateId;281}282283assertNever(element);284}285}286287interface IStackTemplateData extends IAbstractFrameRendererTemplateData {288editor: CodeEditorWidget;289toolbar: MenuWorkbenchToolBar;290}291292const editorOptions: IEditorOptions = {293scrollBeyondLastLine: false,294scrollbar: {295vertical: 'hidden',296horizontal: 'hidden',297handleMouseWheel: false,298useShadows: false,299},300overviewRulerLanes: 0,301fixedOverflowWidgets: true,302overviewRulerBorder: false,303stickyScroll: { enabled: false },304minimap: { enabled: false },305readOnly: true,306automaticLayout: false,307};308309const makeFrameElements = () => dom.h('div.multiCallStackFrame', [310dom.h('div.header@header', [311dom.h('div.collapse-button@collapseButton'),312dom.h('div.title.show-file-icons@title'),313dom.h('div.actions@actions'),314]),315316dom.h('div.editorParent', [317dom.h('div.editorContainer@editor'),318])319]);320321export const CALL_STACK_WIDGET_HEADER_HEIGHT = 24;322323interface IAbstractFrameRendererTemplateData {324container: HTMLElement;325label: ResourceLabel;326elements: ReturnType<typeof makeFrameElements>;327decorations: string[];328collapse: Button;329elementStore: DisposableStore;330templateStore: DisposableStore;331}332333abstract class AbstractFrameRenderer<T extends IAbstractFrameRendererTemplateData> implements IListRenderer<ListItem, T> {334public abstract templateId: string;335336constructor(337@IInstantiationService protected readonly instantiationService: IInstantiationService,338) { }339340renderTemplate(container: HTMLElement): T {341const elements = makeFrameElements();342container.appendChild(elements.root);343344345const templateStore = new DisposableStore();346container.classList.add('multiCallStackFrameContainer');347templateStore.add(toDisposable(() => {348container.classList.remove('multiCallStackFrameContainer');349elements.root.remove();350}));351352const label = templateStore.add(this.instantiationService.createInstance(ResourceLabel, elements.title, {}));353354const collapse = templateStore.add(new Button(elements.collapseButton, {}));355356const contentId = generateUuid();357elements.editor.id = contentId;358elements.editor.role = 'region';359elements.collapseButton.setAttribute('aria-controls', contentId);360361return this.finishRenderTemplate({362container,363decorations: [],364elements,365label,366collapse,367elementStore: templateStore.add(new DisposableStore()),368templateStore,369});370}371372protected abstract finishRenderTemplate(data: IAbstractFrameRendererTemplateData): T;373374renderElement(element: ListItem, index: number, template: T): void {375const { elementStore } = template;376elementStore.clear();377const item = element as IFrameLikeItem;378379this.setupCollapseButton(item, template);380}381382private setupCollapseButton(item: IFrameLikeItem, { elementStore, elements, collapse }: T) {383elementStore.add(autorun(reader => {384collapse.element.className = '';385const collapsed = item.collapsed.read(reader);386collapse.icon = collapsed ? Codicon.chevronRight : Codicon.chevronDown;387collapse.element.ariaExpanded = String(!collapsed);388elements.root.classList.toggle('collapsed', collapsed);389}));390const toggleCollapse = () => item.collapsed.set(!item.collapsed.get(), undefined);391elementStore.add(collapse.onDidClick(toggleCollapse));392elementStore.add(dom.addDisposableListener(elements.title, 'click', toggleCollapse));393}394395disposeElement(element: ListItem, index: number, templateData: T): void {396templateData.elementStore.clear();397}398399disposeTemplate(templateData: T): void {400templateData.templateStore.dispose();401}402}403404const CONTEXT_LINES = 2;405406/** Renderer for a normal stack frame where code is available. */407class FrameCodeRenderer extends AbstractFrameRenderer<IStackTemplateData> {408public static readonly templateId = 'f';409410public readonly templateId = FrameCodeRenderer.templateId;411412constructor(413private readonly containingEditor: ICodeEditor | undefined,414private readonly onLayout: Event<void>,415@ITextModelService private readonly modelService: ITextModelService,416@IInstantiationService instantiationService: IInstantiationService,417) {418super(instantiationService);419}420421protected override finishRenderTemplate(data: IAbstractFrameRendererTemplateData): IStackTemplateData {422// override default e.g. language contributions, only allow users to click423// on code in the call stack to go to its source location424const contributions: IEditorContributionDescription[] = [{425id: ClickToLocationContribution.ID,426instantiation: EditorContributionInstantiation.BeforeFirstInteraction,427ctor: ClickToLocationContribution as EditorContributionCtor,428}];429430const editor = this.containingEditor431? this.instantiationService.createInstance(432EmbeddedCodeEditorWidget,433data.elements.editor,434editorOptions,435{ isSimpleWidget: true, contributions },436this.containingEditor,437)438: this.instantiationService.createInstance(439CodeEditorWidget,440data.elements.editor,441editorOptions,442{ isSimpleWidget: true, contributions },443);444445data.templateStore.add(editor);446447const toolbar = data.templateStore.add(this.instantiationService.createInstance(MenuWorkbenchToolBar, data.elements.actions, MenuId.DebugCallStackToolbar, {448menuOptions: { shouldForwardArgs: true },449actionViewItemProvider: (action, options) => createActionViewItem(this.instantiationService, action, options),450}));451452return { ...data, editor, toolbar };453}454455override renderElement(element: ListItem, index: number, template: IStackTemplateData): void {456super.renderElement(element, index, template);457458const { elementStore, editor } = template;459460const item = element as WrappedCallStackFrame;461const uri = item.source!;462463template.label.element.setFile(uri);464const cts = new CancellationTokenSource();465elementStore.add(toDisposable(() => cts.dispose(true)));466this.modelService.createModelReference(uri).then(reference => {467if (cts.token.isCancellationRequested) {468return reference.dispose();469}470471elementStore.add(reference);472editor.setModel(reference.object.textEditorModel);473this.setupEditorAfterModel(item, template);474this.setupEditorLayout(item, template);475});476}477478private setupEditorLayout(item: WrappedCallStackFrame, { elementStore, container, editor }: IStackTemplateData) {479const layout = () => {480const prev = editor.getContentHeight();481editor.layout({ width: container.clientWidth, height: prev });482483const next = editor.getContentHeight();484if (next !== prev) {485editor.layout({ width: container.clientWidth, height: next });486}487488item.editorHeight.set(next, undefined);489};490elementStore.add(editor.onDidChangeModelDecorations(layout));491elementStore.add(editor.onDidChangeModelContent(layout));492elementStore.add(editor.onDidChangeModelOptions(layout));493elementStore.add(this.onLayout(layout));494layout();495}496497private setupEditorAfterModel(item: WrappedCallStackFrame, template: IStackTemplateData): void {498const range = Range.fromPositions({499column: item.column ?? 1,500lineNumber: item.line ?? 1,501});502503template.toolbar.context = { uri: item.source, range };504505template.editor.setHiddenAreas([506Range.fromPositions(507{ column: 1, lineNumber: 1 },508{ column: 1, lineNumber: Math.max(1, item.line - CONTEXT_LINES - 1) },509),510Range.fromPositions(511{ column: 1, lineNumber: item.line + CONTEXT_LINES + 1 },512{ column: 1, lineNumber: Constants.MAX_SAFE_SMALL_INTEGER },513),514]);515516template.editor.changeDecorations(accessor => {517for (const d of template.decorations) {518accessor.removeDecoration(d);519}520template.decorations.length = 0;521522const beforeRange = range.setStartPosition(range.startLineNumber, 1);523const hasCharactersBefore = !!template.editor.getModel()?.getValueInRange(beforeRange).trim();524const decoRange = range.setEndPosition(range.startLineNumber, Constants.MAX_SAFE_SMALL_INTEGER);525526template.decorations.push(accessor.addDecoration(527decoRange,528makeStackFrameColumnDecoration(!hasCharactersBefore),529));530template.decorations.push(accessor.addDecoration(531decoRange,532TOP_STACK_FRAME_DECORATION,533));534});535536item.editorHeight.set(template.editor.getContentHeight(), undefined);537}538}539540interface IMissingTemplateData {541elements: ReturnType<typeof makeFrameElements>;542label: ResourceLabel;543}544545/** Renderer for a call frame that's missing a URI */546class MissingCodeRenderer implements IListRenderer<ListItem, IMissingTemplateData> {547public static readonly templateId = 'm';548public readonly templateId = MissingCodeRenderer.templateId;549550constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { }551552renderTemplate(container: HTMLElement): IMissingTemplateData {553const elements = makeFrameElements();554elements.root.classList.add('missing');555container.appendChild(elements.root);556const label = this.instantiationService.createInstance(ResourceLabel, elements.title, {});557return { elements, label };558}559560renderElement(element: ListItem, _index: number, templateData: IMissingTemplateData): void {561const cast = element as CallStackFrame;562templateData.label.element.setResource({563name: cast.name,564description: localize('stackFrameLocation', 'Line {0} column {1}', cast.line, cast.column),565range: { startLineNumber: cast.line, startColumn: cast.column, endColumn: cast.column, endLineNumber: cast.line },566}, {567icon: Codicon.fileBinary,568});569}570571disposeTemplate(templateData: IMissingTemplateData): void {572templateData.label.dispose();573templateData.elements.root.remove();574}575}576577/** Renderer for a call frame that's missing a URI */578class CustomRenderer extends AbstractFrameRenderer<IAbstractFrameRendererTemplateData> {579public static readonly templateId = 'c';580public readonly templateId = CustomRenderer.templateId;581582protected override finishRenderTemplate(data: IAbstractFrameRendererTemplateData): IAbstractFrameRendererTemplateData {583return data;584}585586override renderElement(element: ListItem, index: number, template: IAbstractFrameRendererTemplateData): void {587super.renderElement(element, index, template);588589const item = element as WrappedCustomStackFrame;590const { elementStore, container, label } = template;591592label.element.setResource({ name: item.original.label }, { icon: item.original.icon });593594elementStore.add(autorun(reader => {595template.elements.header.style.display = item.original.showHeader.read(reader) ? '' : 'none';596}));597598elementStore.add(autorunWithStore((reader, store) => {599if (!item.collapsed.read(reader)) {600store.add(item.original.render(container));601}602}));603604const actions = item.original.renderActions?.(template.elements.actions);605if (actions) {606elementStore.add(actions);607}608}609}610611interface ISkippedTemplateData {612button: Button;613current?: SkippedCallFrames;614store: DisposableStore;615}616617/** Renderer for a button to load more call frames */618class SkippedRenderer implements IListRenderer<ListItem, ISkippedTemplateData> {619public static readonly templateId = 's';620public readonly templateId = SkippedRenderer.templateId;621622constructor(623private readonly loadFrames: (fromItem: SkippedCallFrames) => Promise<void>,624@INotificationService private readonly notificationService: INotificationService,625) { }626627renderTemplate(container: HTMLElement): ISkippedTemplateData {628const store = new DisposableStore();629const button = new Button(container, { title: '', ...defaultButtonStyles });630const data: ISkippedTemplateData = { button, store };631632store.add(button);633store.add(button.onDidClick(() => {634if (!data.current || !button.enabled) {635return;636}637638button.enabled = false;639this.loadFrames(data.current).catch(e => {640this.notificationService.error(localize('failedToLoadFrames', 'Failed to load stack frames: {0}', e.message));641});642}));643644return data;645}646647renderElement(element: ListItem, index: number, templateData: ISkippedTemplateData): void {648const cast = element as SkippedCallFrames;649templateData.button.enabled = true;650templateData.button.label = cast.label;651templateData.current = cast;652}653654disposeTemplate(templateData: ISkippedTemplateData): void {655templateData.store.dispose();656}657}658659/** A simple contribution that makes all data in the editor clickable to go to the location */660class ClickToLocationContribution extends Disposable implements IEditorContribution {661public static readonly ID = 'clickToLocation';662private readonly linkDecorations: IEditorDecorationsCollection;663private current: { line: number; word: IWordAtPosition } | undefined;664665constructor(666private readonly editor: ICodeEditor,667@IEditorService editorService: IEditorService,668) {669super();670this.linkDecorations = editor.createDecorationsCollection();671this._register(toDisposable(() => this.linkDecorations.clear()));672673const clickLinkGesture = this._register(new ClickLinkGesture(editor));674675this._register(clickLinkGesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, keyboardEvent]) => {676this.onMove(mouseEvent);677}));678this._register(clickLinkGesture.onExecute((e) => {679const model = this.editor.getModel();680if (!this.current || !model) {681return;682}683684editorService.openEditor({685resource: model.uri,686options: {687selection: Range.fromPositions(new Position(this.current.line, this.current.word.startColumn)),688selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport,689},690}, e.hasSideBySideModifier ? SIDE_GROUP : undefined);691}));692}693694private onMove(mouseEvent: ClickLinkMouseEvent) {695if (!mouseEvent.hasTriggerModifier) {696return this.clear();697}698699const position = mouseEvent.target.position;700const word = position && this.editor.getModel()?.getWordAtPosition(position);701if (!word) {702return this.clear();703}704705const prev = this.current?.word;706if (prev && prev.startColumn === word.startColumn && prev.endColumn === word.endColumn && prev.word === word.word) {707return;708}709710this.current = { word, line: position.lineNumber };711this.linkDecorations.set([{712range: new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn),713options: {714description: 'call-stack-go-to-file-link',715inlineClassName: 'call-stack-go-to-file-link',716},717}]);718}719720private clear() {721this.linkDecorations.clear();722this.current = undefined;723}724}725726registerAction2(class extends Action2 {727constructor() {728super({729id: 'callStackWidget.goToFile',730title: localize2('goToFile', 'Open File'),731icon: Codicon.goToFile,732menu: {733id: MenuId.DebugCallStackToolbar,734order: 22,735group: 'navigation',736},737});738}739740async run(accessor: ServicesAccessor, { uri, range }: Location): Promise<void> {741const editorService = accessor.get(IEditorService);742await editorService.openEditor({743resource: uri,744options: {745selection: range,746selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport,747},748});749}750});751752753