|
| 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 { CancellationToken } from '../../../../../../base/common/cancellation.js'; |
| 7 | +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; |
| 8 | +import { Disposable } from '../../../../../../base/common/lifecycle.js'; |
| 9 | +import { Position } from '../../../../../../editor/common/core/position.js'; |
| 10 | +import { Range } from '../../../../../../editor/common/core/range.js'; |
| 11 | +import { Hover, HoverContext, HoverProvider } from '../../../../../../editor/common/languages.js'; |
| 12 | +import { ITextModel } from '../../../../../../editor/common/model.js'; |
| 13 | +import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; |
| 14 | +import { localize } from '../../../../../../nls.js'; |
| 15 | +import { ILanguageModelsService } from '../../languageModels.js'; |
| 16 | +import { ILanguageModelToolsService, ToolSet } from '../../languageModelToolsService.js'; |
| 17 | +import { InstructionsHeader } from '../parsers/promptHeader/instructionsHeader.js'; |
| 18 | +import { PromptModelMetadata } from '../parsers/promptHeader/metadata/model.js'; |
| 19 | +import { PromptToolsMetadata } from '../parsers/promptHeader/metadata/tools.js'; |
| 20 | +import { ModeHeader } from '../parsers/promptHeader/modeHeader.js'; |
| 21 | +import { ALL_PROMPTS_LANGUAGE_SELECTOR, getPromptsTypeForLanguageId } from '../promptTypes.js'; |
| 22 | +import { IPromptsService } from '../service/promptsService.js'; |
| 23 | + |
| 24 | +export class PromptHeaderHoverProvider extends Disposable implements HoverProvider { |
| 25 | + /** |
| 26 | + * Debug display name for this provider. |
| 27 | + */ |
| 28 | + public readonly _debugDisplayName: string = 'PromptHeaderHoverProvider'; |
| 29 | + |
| 30 | + constructor( |
| 31 | + @IPromptsService private readonly promptsService: IPromptsService, |
| 32 | + @ILanguageFeaturesService private readonly languageService: ILanguageFeaturesService, |
| 33 | + @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, |
| 34 | + @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, |
| 35 | + ) { |
| 36 | + super(); |
| 37 | + |
| 38 | + this._register(this.languageService.hoverProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, this)); |
| 39 | + } |
| 40 | + |
| 41 | + private createHover(contents: string, range: Range): Hover { |
| 42 | + return { |
| 43 | + contents: [new MarkdownString(contents)], |
| 44 | + range |
| 45 | + }; |
| 46 | + } |
| 47 | + |
| 48 | + public async provideHover( |
| 49 | + model: ITextModel, |
| 50 | + position: Position, |
| 51 | + token: CancellationToken, |
| 52 | + _context?: HoverContext |
| 53 | + ): Promise<Hover | undefined> { |
| 54 | + |
| 55 | + const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); |
| 56 | + if (!promptType) { |
| 57 | + // if the model is not a prompt, we don't provide any completions |
| 58 | + return undefined; |
| 59 | + } |
| 60 | + |
| 61 | + const parser = this.promptsService.getSyntaxParserFor(model); |
| 62 | + await parser.start(token).settled(); |
| 63 | + |
| 64 | + if (token.isCancellationRequested) { |
| 65 | + return undefined; |
| 66 | + } |
| 67 | + |
| 68 | + const header = parser.header; |
| 69 | + if (!header) { |
| 70 | + return undefined; |
| 71 | + } |
| 72 | + |
| 73 | + await header.settled; |
| 74 | + |
| 75 | + if (header instanceof InstructionsHeader) { |
| 76 | + const descriptionRange = header.metadataUtility.description?.range; |
| 77 | + if (descriptionRange?.containsPosition(position)) { |
| 78 | + return this.createHover(localize('promptHeader.instructions.description', 'The description of the instruction file. It can be used to provide additional context or information about the instructions and is passed to the language model as part of the prompt.'), descriptionRange); |
| 79 | + } |
| 80 | + const applyToRange = header.metadataUtility.applyTo?.range; |
| 81 | + if (applyToRange?.containsPosition(position)) { |
| 82 | + return this.createHover(localize('promptHeader.instructions.applyToRange', 'One or more glob pattern (separated by comma) that describe for which files the instructions apply to. Based on these patterns, the file is automatically included in the prompt, when the context contains a file that matches one or more of these patterns. Use `**` when you want this file to always be added.\nExample: **/*.ts, **/*.js, client/**'), applyToRange); |
| 83 | + } |
| 84 | + |
| 85 | + } else if (header instanceof ModeHeader) { |
| 86 | + const descriptionRange = header.metadataUtility.description?.range; |
| 87 | + if (descriptionRange?.containsPosition(position)) { |
| 88 | + return this.createHover(localize('promptHeader.mode.description', 'The description of the mode file. It can be used to provide additional context or information about the mode to the mode author.'), descriptionRange); |
| 89 | + } |
| 90 | + const model = header.metadataUtility.model; |
| 91 | + if (model?.range.containsPosition(position)) { |
| 92 | + return this.getModelHover(model, model.range, localize('promptHeader.mode.model', 'The model to use in this mode.')); |
| 93 | + } |
| 94 | + const tools = header.metadataUtility.tools; |
| 95 | + if (tools?.range?.containsPosition(position)) { |
| 96 | + return this.getToolHover(tools, position, localize('promptHeader.mode.tools', 'The tools to use in this mode.')); |
| 97 | + } |
| 98 | + } else { |
| 99 | + const descriptionRange = header.metadataUtility.description?.range; |
| 100 | + if (descriptionRange?.containsPosition(position)) { |
| 101 | + return this.createHover(localize('promptHeader.prompt.description', 'The description of the prompt file. It can be used to provide additional context or information about the prompt to the prompt author.'), descriptionRange); |
| 102 | + } |
| 103 | + const model = header.metadataUtility.model; |
| 104 | + if (model?.range.containsPosition(position)) { |
| 105 | + return this.getModelHover(model, model.range, localize('promptHeader.prompt.model', 'The model to use in this prompt.')); |
| 106 | + } |
| 107 | + const tools = header.metadataUtility.tools; |
| 108 | + if (tools?.range?.containsPosition(position)) { |
| 109 | + return this.getToolHover(tools, position, localize('promptHeader.prompt.tools', 'The tools to use in this prompt.')); |
| 110 | + } |
| 111 | + const modeRange = header.metadataUtility.mode?.range; |
| 112 | + if (modeRange?.containsPosition(position)) { |
| 113 | + return this.createHover(localize('promptHeader.prompt.mode', 'The mode (ask, edit or agent) to use when running this prompt.'), modeRange); |
| 114 | + } |
| 115 | + } |
| 116 | + return undefined; |
| 117 | + } |
| 118 | + |
| 119 | + private getToolHover(node: PromptToolsMetadata, position: Position, baseMessage: string): Hover | undefined { |
| 120 | + if (node.value) { |
| 121 | + |
| 122 | + for (const toolName of node.value) { |
| 123 | + const toolRange = node.getToolRange(toolName); |
| 124 | + if (toolRange?.containsPosition(position)) { |
| 125 | + const tool = this.languageModelToolsService.getToolByName(toolName); |
| 126 | + if (tool) { |
| 127 | + return this.createHover(tool.displayName, toolRange); |
| 128 | + } |
| 129 | + const toolSet = this.languageModelToolsService.getToolSetByName(toolName); |
| 130 | + if (toolSet) { |
| 131 | + return this.getToolsetHover(toolSet, toolRange); |
| 132 | + } |
| 133 | + } |
| 134 | + } |
| 135 | + } |
| 136 | + return this.createHover(baseMessage, node.range); |
| 137 | + } |
| 138 | + |
| 139 | + private getToolsetHover(toolSet: ToolSet, range: Range): Hover | undefined { |
| 140 | + const lines: string[] = []; |
| 141 | + lines.push(localize('toolSetName', 'ToolSet: {0}\n\n', toolSet.referenceName)); |
| 142 | + if (toolSet.description) { |
| 143 | + lines.push(toolSet.description); |
| 144 | + } |
| 145 | + for (const tool of toolSet.getTools()) { |
| 146 | + lines.push(`- ${tool.toolReferenceName ?? tool.displayName} (${tool.displayName})`); |
| 147 | + } |
| 148 | + return this.createHover(lines.join('\n'), range); |
| 149 | + } |
| 150 | + |
| 151 | + private getModelHover(node: PromptModelMetadata, range: Range, baseMessage: string): Hover | undefined { |
| 152 | + if (node.value) { |
| 153 | + |
| 154 | + for (const id of this.languageModelsService.getLanguageModelIds()) { |
| 155 | + const meta = this.languageModelsService.lookupLanguageModel(id); |
| 156 | + if (meta) { |
| 157 | + const lines: string[] = []; |
| 158 | + lines.push(baseMessage + '\n'); |
| 159 | + lines.push(localize('modelName', '{0}', meta.description ?? meta.name)); |
| 160 | + lines.push(localize('modelFamily', '- Family: {0}', meta.family)); |
| 161 | + lines.push(localize('modelVendor', '- Vendor: {0}', meta.vendor)); |
| 162 | + return this.createHover(lines.join('\n'), range); |
| 163 | + } |
| 164 | + } |
| 165 | + } |
| 166 | + return this.createHover(baseMessage, range); |
| 167 | + } |
| 168 | + |
| 169 | +} |
0 commit comments