Path: blob/main/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.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 * as aria from '../../../../base/browser/ui/aria/aria.js';7import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';8import { Codicon } from '../../../../base/common/codicons.js';9import { Event } from '../../../../base/common/event.js';10import { IMarkdownString } from '../../../../base/common/htmlContent.js';11import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';12import { escapeRegExpCharacters } from '../../../../base/common/strings.js';13import { assertReturnsDefined } from '../../../../base/common/types.js';14import './parameterHints.css';15import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../browser/editorBrowser.js';16import { EDITOR_FONT_DEFAULTS, EditorOption } from '../../../common/config/editorOptions.js';17import * as languages from '../../../common/languages.js';18import { ILanguageService } from '../../../common/languages/language.js';19import { IMarkdownRenderResult, MarkdownRenderer } from '../../../browser/widget/markdownRenderer/browser/markdownRenderer.js';20import { ParameterHintsModel } from './parameterHintsModel.js';21import { Context } from './provideSignatureHelp.js';22import * as nls from '../../../../nls.js';23import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';24import { IOpenerService } from '../../../../platform/opener/common/opener.js';25import { listHighlightForeground, registerColor } from '../../../../platform/theme/common/colorRegistry.js';26import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';27import { ThemeIcon } from '../../../../base/common/themables.js';2829const $ = dom.$;3031const parameterHintsNextIcon = registerIcon('parameter-hints-next', Codicon.chevronDown, nls.localize('parameterHintsNextIcon', 'Icon for show next parameter hint.'));32const parameterHintsPreviousIcon = registerIcon('parameter-hints-previous', Codicon.chevronUp, nls.localize('parameterHintsPreviousIcon', 'Icon for show previous parameter hint.'));3334export class ParameterHintsWidget extends Disposable implements IContentWidget {3536private static readonly ID = 'editor.widget.parameterHintsWidget';3738private readonly markdownRenderer: MarkdownRenderer;39private readonly renderDisposeables = this._register(new DisposableStore());40private readonly keyVisible: IContextKey<boolean>;41private readonly keyMultipleSignatures: IContextKey<boolean>;4243private domNodes?: {44readonly element: HTMLElement;45readonly signature: HTMLElement;46readonly docs: HTMLElement;47readonly overloads: HTMLElement;48readonly scrollbar: DomScrollableElement;49};5051private visible: boolean = false;52private announcedLabel: string | null = null;5354// Editor.IContentWidget.allowEditorOverflow55allowEditorOverflow = true;5657constructor(58private readonly editor: ICodeEditor,59private readonly model: ParameterHintsModel,60@IContextKeyService contextKeyService: IContextKeyService,61@IOpenerService openerService: IOpenerService,62@ILanguageService languageService: ILanguageService63) {64super();6566this.markdownRenderer = new MarkdownRenderer({ editor }, languageService, openerService);6768this.keyVisible = Context.Visible.bindTo(contextKeyService);69this.keyMultipleSignatures = Context.MultipleSignatures.bindTo(contextKeyService);70}7172private createParameterHintDOMNodes() {73const element = $('.editor-widget.parameter-hints-widget');74const wrapper = dom.append(element, $('.phwrapper'));75wrapper.tabIndex = -1;7677const controls = dom.append(wrapper, $('.controls'));78const previous = dom.append(controls, $('.button' + ThemeIcon.asCSSSelector(parameterHintsPreviousIcon)));79const overloads = dom.append(controls, $('.overloads'));80const next = dom.append(controls, $('.button' + ThemeIcon.asCSSSelector(parameterHintsNextIcon)));8182this._register(dom.addDisposableListener(previous, 'click', e => {83dom.EventHelper.stop(e);84this.previous();85}));8687this._register(dom.addDisposableListener(next, 'click', e => {88dom.EventHelper.stop(e);89this.next();90}));9192const body = $('.body');93const scrollbar = new DomScrollableElement(body, {94alwaysConsumeMouseWheel: true,95});96this._register(scrollbar);97wrapper.appendChild(scrollbar.getDomNode());9899const signature = dom.append(body, $('.signature'));100const docs = dom.append(body, $('.docs'));101102element.style.userSelect = 'text';103104this.domNodes = {105element,106signature,107overloads,108docs,109scrollbar,110};111112this.editor.addContentWidget(this);113this.hide();114115this._register(this.editor.onDidChangeCursorSelection(e => {116if (this.visible) {117this.editor.layoutContentWidget(this);118}119}));120121const updateFont = () => {122if (!this.domNodes) {123return;124}125126const fontInfo = this.editor.getOption(EditorOption.fontInfo);127const element = this.domNodes.element;128element.style.fontSize = `${fontInfo.fontSize}px`;129element.style.lineHeight = `${fontInfo.lineHeight / fontInfo.fontSize}`;130element.style.setProperty('--vscode-parameterHintsWidget-editorFontFamily', fontInfo.fontFamily);131element.style.setProperty('--vscode-parameterHintsWidget-editorFontFamilyDefault', EDITOR_FONT_DEFAULTS.fontFamily);132};133134updateFont();135136this._register(Event.chain(137this.editor.onDidChangeConfiguration.bind(this.editor),138$ => $.filter(e => e.hasChanged(EditorOption.fontInfo))139)(updateFont));140141this._register(this.editor.onDidLayoutChange(e => this.updateMaxHeight()));142this.updateMaxHeight();143}144145public show(): void {146if (this.visible) {147return;148}149150if (!this.domNodes) {151this.createParameterHintDOMNodes();152}153154this.keyVisible.set(true);155this.visible = true;156setTimeout(() => {157this.domNodes?.element.classList.add('visible');158}, 100);159this.editor.layoutContentWidget(this);160}161162public hide(): void {163this.renderDisposeables.clear();164165if (!this.visible) {166return;167}168169this.keyVisible.reset();170this.visible = false;171this.announcedLabel = null;172this.domNodes?.element.classList.remove('visible');173this.editor.layoutContentWidget(this);174}175176getPosition(): IContentWidgetPosition | null {177if (this.visible) {178return {179position: this.editor.getPosition(),180preference: [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW]181};182}183return null;184}185186public render(hints: languages.SignatureHelp): void {187this.renderDisposeables.clear();188189if (!this.domNodes) {190return;191}192193const multiple = hints.signatures.length > 1;194this.domNodes.element.classList.toggle('multiple', multiple);195this.keyMultipleSignatures.set(multiple);196197this.domNodes.signature.innerText = '';198this.domNodes.docs.innerText = '';199200const signature = hints.signatures[hints.activeSignature];201if (!signature) {202return;203}204205const code = dom.append(this.domNodes.signature, $('.code'));206const hasParameters = signature.parameters.length > 0;207const activeParameterIndex = signature.activeParameter ?? hints.activeParameter;208209if (!hasParameters) {210const label = dom.append(code, $('span'));211label.textContent = signature.label;212} else {213this.renderParameters(code, signature, activeParameterIndex);214}215216const activeParameter: languages.ParameterInformation | undefined = signature.parameters[activeParameterIndex];217if (activeParameter?.documentation) {218const documentation = $('span.documentation');219if (typeof activeParameter.documentation === 'string') {220documentation.textContent = activeParameter.documentation;221} else {222const renderedContents = this.renderMarkdownDocs(activeParameter.documentation);223documentation.appendChild(renderedContents.element);224}225dom.append(this.domNodes.docs, $('p', {}, documentation));226}227228if (signature.documentation === undefined) {229/** no op */230} else if (typeof signature.documentation === 'string') {231dom.append(this.domNodes.docs, $('p', {}, signature.documentation));232} else {233const renderedContents = this.renderMarkdownDocs(signature.documentation);234dom.append(this.domNodes.docs, renderedContents.element);235}236237const hasDocs = this.hasDocs(signature, activeParameter);238239this.domNodes.signature.classList.toggle('has-docs', hasDocs);240this.domNodes.docs.classList.toggle('empty', !hasDocs);241242this.domNodes.overloads.textContent =243String(hints.activeSignature + 1).padStart(hints.signatures.length.toString().length, '0') + '/' + hints.signatures.length;244245if (activeParameter) {246let labelToAnnounce = '';247const param = signature.parameters[activeParameterIndex];248if (Array.isArray(param.label)) {249labelToAnnounce = signature.label.substring(param.label[0], param.label[1]);250} else {251labelToAnnounce = param.label;252}253if (param.documentation) {254labelToAnnounce += typeof param.documentation === 'string' ? `, ${param.documentation}` : `, ${param.documentation.value}`;255}256if (signature.documentation) {257labelToAnnounce += typeof signature.documentation === 'string' ? `, ${signature.documentation}` : `, ${signature.documentation.value}`;258}259260// Select method gets called on every user type while parameter hints are visible.261// We do not want to spam the user with same announcements, so we only announce if the current parameter changed.262263if (this.announcedLabel !== labelToAnnounce) {264aria.alert(nls.localize('hint', "{0}, hint", labelToAnnounce));265this.announcedLabel = labelToAnnounce;266}267}268269this.editor.layoutContentWidget(this);270this.domNodes.scrollbar.scanDomNode();271}272273private renderMarkdownDocs(markdown: IMarkdownString): IMarkdownRenderResult {274const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(markdown, {275asyncRenderCallback: () => {276this.domNodes?.scrollbar.scanDomNode();277}278}));279renderedContents.element.classList.add('markdown-docs');280return renderedContents;281}282283private hasDocs(signature: languages.SignatureInformation, activeParameter: languages.ParameterInformation | undefined): boolean {284if (activeParameter && typeof activeParameter.documentation === 'string' && assertReturnsDefined(activeParameter.documentation).length > 0) {285return true;286}287if (activeParameter && typeof activeParameter.documentation === 'object' && assertReturnsDefined(activeParameter.documentation).value.length > 0) {288return true;289}290if (signature.documentation && typeof signature.documentation === 'string' && assertReturnsDefined(signature.documentation).length > 0) {291return true;292}293if (signature.documentation && typeof signature.documentation === 'object' && assertReturnsDefined(signature.documentation.value).length > 0) {294return true;295}296return false;297}298299private renderParameters(parent: HTMLElement, signature: languages.SignatureInformation, activeParameterIndex: number): void {300const [start, end] = this.getParameterLabelOffsets(signature, activeParameterIndex);301302const beforeSpan = document.createElement('span');303beforeSpan.textContent = signature.label.substring(0, start);304305const paramSpan = document.createElement('span');306paramSpan.textContent = signature.label.substring(start, end);307paramSpan.className = 'parameter active';308309const afterSpan = document.createElement('span');310afterSpan.textContent = signature.label.substring(end);311312dom.append(parent, beforeSpan, paramSpan, afterSpan);313}314315private getParameterLabelOffsets(signature: languages.SignatureInformation, paramIdx: number): [number, number] {316const param = signature.parameters[paramIdx];317if (!param) {318return [0, 0];319} else if (Array.isArray(param.label)) {320return param.label;321} else if (!param.label.length) {322return [0, 0];323} else {324const regex = new RegExp(`(\\W|^)${escapeRegExpCharacters(param.label)}(?=\\W|$)`, 'g');325regex.test(signature.label);326const idx = regex.lastIndex - param.label.length;327return idx >= 0328? [idx, regex.lastIndex]329: [0, 0];330}331}332333next(): void {334this.editor.focus();335this.model.next();336}337338previous(): void {339this.editor.focus();340this.model.previous();341}342343getDomNode(): HTMLElement {344if (!this.domNodes) {345this.createParameterHintDOMNodes();346}347return this.domNodes!.element;348}349350getId(): string {351return ParameterHintsWidget.ID;352}353354private updateMaxHeight(): void {355if (!this.domNodes) {356return;357}358const height = Math.max(this.editor.getLayoutInfo().height / 4, 250);359const maxHeight = `${height}px`;360this.domNodes.element.style.maxHeight = maxHeight;361const wrapper = this.domNodes.element.getElementsByClassName('phwrapper') as HTMLCollectionOf<HTMLElement>;362if (wrapper.length) {363wrapper[0].style.maxHeight = maxHeight;364}365}366}367368registerColor('editorHoverWidget.highlightForeground', listHighlightForeground, nls.localize('editorHoverWidgetHighlightForeground', 'Foreground color of the active item in the parameter hint.'));369370371