Path: blob/main/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts
5237 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 { EditorOption } from '../../../common/config/editorOptions.js';17import { EDITOR_FONT_DEFAULTS } from '../../../common/config/fontInfo.js';18import * as languages from '../../../common/languages.js';19import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';20import { IRenderedMarkdown } from '../../../../base/browser/markdownRenderer.js';21import { ParameterHintsModel } from './parameterHintsModel.js';22import { Context } from './provideSignatureHelp.js';23import * as nls from '../../../../nls.js';24import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.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 renderDisposeables = this._register(new DisposableStore());39private readonly keyVisible: IContextKey<boolean>;40private readonly keyMultipleSignatures: IContextKey<boolean>;4142private domNodes?: {43readonly element: HTMLElement;44readonly signature: HTMLElement;45readonly docs: HTMLElement;46readonly overloads: HTMLElement;47readonly scrollbar: DomScrollableElement;48};4950private visible: boolean = false;51private announcedLabel: string | null = null;5253// Editor.IContentWidget.allowEditorOverflow54allowEditorOverflow = true;5556constructor(57private readonly editor: ICodeEditor,58private readonly model: ParameterHintsModel,59@IContextKeyService contextKeyService: IContextKeyService,60@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,61) {62super();6364this.keyVisible = Context.Visible.bindTo(contextKeyService);65this.keyMultipleSignatures = Context.MultipleSignatures.bindTo(contextKeyService);66}6768private createParameterHintDOMNodes() {69const element = $('.editor-widget.parameter-hints-widget');70const wrapper = dom.append(element, $('.phwrapper'));71wrapper.tabIndex = -1;7273const controls = dom.append(wrapper, $('.controls'));74const previous = dom.append(controls, $('.button' + ThemeIcon.asCSSSelector(parameterHintsPreviousIcon)));75const overloads = dom.append(controls, $('.overloads'));76const next = dom.append(controls, $('.button' + ThemeIcon.asCSSSelector(parameterHintsNextIcon)));7778this._register(dom.addDisposableListener(previous, 'click', e => {79dom.EventHelper.stop(e);80this.previous();81}));8283this._register(dom.addDisposableListener(next, 'click', e => {84dom.EventHelper.stop(e);85this.next();86}));8788const body = $('.body');89const scrollbar = new DomScrollableElement(body, {90alwaysConsumeMouseWheel: true,91});92this._register(scrollbar);93wrapper.appendChild(scrollbar.getDomNode());9495const signature = dom.append(body, $('.signature'));96const docs = dom.append(body, $('.docs'));9798element.style.userSelect = 'text';99100this.domNodes = {101element,102signature,103overloads,104docs,105scrollbar,106};107108this.editor.addContentWidget(this);109this.hide();110111this._register(this.editor.onDidChangeCursorSelection(e => {112if (this.visible) {113this.editor.layoutContentWidget(this);114}115}));116117const updateFont = () => {118if (!this.domNodes) {119return;120}121122const fontInfo = this.editor.getOption(EditorOption.fontInfo);123const element = this.domNodes.element;124element.style.fontSize = `${fontInfo.fontSize}px`;125element.style.lineHeight = `${fontInfo.lineHeight / fontInfo.fontSize}`;126element.style.setProperty('--vscode-parameterHintsWidget-editorFontFamily', fontInfo.fontFamily);127element.style.setProperty('--vscode-parameterHintsWidget-editorFontFamilyDefault', EDITOR_FONT_DEFAULTS.fontFamily);128};129130updateFont();131132this._register(Event.chain(133this.editor.onDidChangeConfiguration.bind(this.editor),134$ => $.filter(e => e.hasChanged(EditorOption.fontInfo))135)(updateFont));136137this._register(this.editor.onDidLayoutChange(e => this.updateMaxHeight()));138this.updateMaxHeight();139}140141public show(): void {142if (this.visible) {143return;144}145146if (!this.domNodes) {147this.createParameterHintDOMNodes();148}149150this.keyVisible.set(true);151this.visible = true;152setTimeout(() => {153this.domNodes?.element.classList.add('visible');154}, 100);155this.editor.layoutContentWidget(this);156}157158public hide(): void {159this.renderDisposeables.clear();160161if (!this.visible) {162return;163}164165this.keyVisible.reset();166this.visible = false;167this.announcedLabel = null;168this.domNodes?.element.classList.remove('visible');169this.editor.layoutContentWidget(this);170}171172getPosition(): IContentWidgetPosition | null {173if (this.visible) {174return {175position: this.editor.getPosition(),176preference: [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW]177};178}179return null;180}181182public render(hints: languages.SignatureHelp): void {183this.renderDisposeables.clear();184185if (!this.domNodes) {186return;187}188189const multiple = hints.signatures.length > 1;190this.domNodes.element.classList.toggle('multiple', multiple);191this.keyMultipleSignatures.set(multiple);192193this.domNodes.signature.innerText = '';194this.domNodes.docs.innerText = '';195196const signature = hints.signatures[hints.activeSignature];197if (!signature) {198return;199}200201const code = dom.append(this.domNodes.signature, $('.code'));202const hasParameters = signature.parameters.length > 0;203const activeParameterIndex = signature.activeParameter ?? hints.activeParameter;204205if (!hasParameters) {206const label = dom.append(code, $('span'));207label.textContent = signature.label;208} else {209this.renderParameters(code, signature, activeParameterIndex);210}211212const activeParameter: languages.ParameterInformation | undefined = signature.parameters[activeParameterIndex];213if (activeParameter?.documentation) {214const documentation = $('span.documentation');215if (typeof activeParameter.documentation === 'string') {216documentation.textContent = activeParameter.documentation;217} else {218const renderedContents = this.renderMarkdownDocs(activeParameter.documentation);219documentation.appendChild(renderedContents.element);220}221dom.append(this.domNodes.docs, $('p', {}, documentation));222}223224if (signature.documentation === undefined) {225/** no op */226} else if (typeof signature.documentation === 'string') {227dom.append(this.domNodes.docs, $('p', {}, signature.documentation));228} else {229const renderedContents = this.renderMarkdownDocs(signature.documentation);230dom.append(this.domNodes.docs, renderedContents.element);231}232233const hasDocs = this.hasDocs(signature, activeParameter);234235this.domNodes.signature.classList.toggle('has-docs', hasDocs);236this.domNodes.docs.classList.toggle('empty', !hasDocs);237238this.domNodes.overloads.textContent =239String(hints.activeSignature + 1).padStart(hints.signatures.length.toString().length, '0') + '/' + hints.signatures.length;240241if (activeParameter) {242let labelToAnnounce = '';243const param = signature.parameters[activeParameterIndex];244if (Array.isArray(param.label)) {245labelToAnnounce = signature.label.substring(param.label[0], param.label[1]);246} else {247labelToAnnounce = param.label;248}249if (param.documentation) {250labelToAnnounce += typeof param.documentation === 'string' ? `, ${param.documentation}` : `, ${param.documentation.value}`;251}252if (signature.documentation) {253labelToAnnounce += typeof signature.documentation === 'string' ? `, ${signature.documentation}` : `, ${signature.documentation.value}`;254}255256// Select method gets called on every user type while parameter hints are visible.257// We do not want to spam the user with same announcements, so we only announce if the current parameter changed.258259if (this.announcedLabel !== labelToAnnounce) {260aria.alert(nls.localize('hint', "{0}, hint", labelToAnnounce));261this.announcedLabel = labelToAnnounce;262}263}264265this.editor.layoutContentWidget(this);266this.domNodes.scrollbar.scanDomNode();267}268269private renderMarkdownDocs(markdown: IMarkdownString): IRenderedMarkdown {270const renderedContents = this.renderDisposeables.add(this.markdownRendererService.render(markdown, {271context: this.editor,272asyncRenderCallback: () => {273this.domNodes?.scrollbar.scanDomNode();274}275}));276renderedContents.element.classList.add('markdown-docs');277return renderedContents;278}279280private hasDocs(signature: languages.SignatureInformation, activeParameter: languages.ParameterInformation | undefined): boolean {281if (activeParameter && typeof activeParameter.documentation === 'string' && assertReturnsDefined(activeParameter.documentation).length > 0) {282return true;283}284if (activeParameter && typeof activeParameter.documentation === 'object' && assertReturnsDefined(activeParameter.documentation).value.length > 0) {285return true;286}287if (signature.documentation && typeof signature.documentation === 'string' && assertReturnsDefined(signature.documentation).length > 0) {288return true;289}290if (signature.documentation && typeof signature.documentation === 'object' && assertReturnsDefined(signature.documentation.value).length > 0) {291return true;292}293return false;294}295296private renderParameters(parent: HTMLElement, signature: languages.SignatureInformation, activeParameterIndex: number): void {297const [start, end] = this.getParameterLabelOffsets(signature, activeParameterIndex);298299const beforeSpan = document.createElement('span');300beforeSpan.textContent = signature.label.substring(0, start);301302const paramSpan = document.createElement('span');303paramSpan.textContent = signature.label.substring(start, end);304paramSpan.className = 'parameter active';305306const afterSpan = document.createElement('span');307afterSpan.textContent = signature.label.substring(end);308309dom.append(parent, beforeSpan, paramSpan, afterSpan);310}311312private getParameterLabelOffsets(signature: languages.SignatureInformation, paramIdx: number): [number, number] {313const param = signature.parameters[paramIdx];314if (!param) {315return [0, 0];316} else if (Array.isArray(param.label)) {317return param.label;318} else if (!param.label.length) {319return [0, 0];320} else {321const regex = new RegExp(`(\\W|^)${escapeRegExpCharacters(param.label)}(?=\\W|$)`, 'g');322regex.test(signature.label);323const idx = regex.lastIndex - param.label.length;324return idx >= 0325? [idx, regex.lastIndex]326: [0, 0];327}328}329330next(): void {331this.editor.focus();332this.model.next();333}334335previous(): void {336this.editor.focus();337this.model.previous();338}339340getDomNode(): HTMLElement {341if (!this.domNodes) {342this.createParameterHintDOMNodes();343}344return this.domNodes!.element;345}346347getId(): string {348return ParameterHintsWidget.ID;349}350351private updateMaxHeight(): void {352if (!this.domNodes) {353return;354}355const height = Math.max(this.editor.getLayoutInfo().height / 4, 250);356const maxHeight = `${height}px`;357this.domNodes.element.style.maxHeight = maxHeight;358// eslint-disable-next-line no-restricted-syntax359const wrapper = this.domNodes.element.getElementsByClassName('phwrapper') as HTMLCollectionOf<HTMLElement>;360if (wrapper.length) {361wrapper[0].style.maxHeight = maxHeight;362}363}364}365366registerColor('editorHoverWidget.highlightForeground', listHighlightForeground, nls.localize('editorHoverWidgetHighlightForeground', 'Foreground color of the active item in the parameter hint.'));367368369