From 2d01fca9b09d5a690bbab4f9e2ceedf99992ef4b Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Sat, 28 Jun 2025 21:26:50 +0200 Subject: [PATCH] prompt file: code complete in headers --- .../promptHeaderAutocompletion.ts | 195 ++++++++++++++++++ .../promptSyntax/promptFileContributions.ts | 3 +- 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts new file mode 100644 index 00000000000..b3c6dc95ada --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -0,0 +1,195 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { IPromptsService } from '../service/promptsService.js'; +import { ITextModel } from '../../../../../../editor/common/model.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { ALL_PROMPTS_LANGUAGE_SELECTOR, getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; +import { Position } from '../../../../../../editor/common/core/position.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; +import { CompletionContext, CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList } from '../../../../../../editor/common/languages.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; + +export class PromptHeaderAutocompletion extends Disposable implements CompletionItemProvider { + /** + * Debug display name for this provider. + */ + public readonly _debugDisplayName: string = 'PromptHeaderAutocompletion'; + + /** + * List of trigger characters handled by this provider. + */ + public readonly triggerCharacters = [':']; + + constructor( + @IPromptsService private readonly promptsService: IPromptsService, + @ILanguageFeaturesService private readonly languageService: ILanguageFeaturesService, + + ) { + super(); + + this._register(this.languageService.completionProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, this)); + } + + /** + * The main function of this provider that calculates + * completion items based on the provided arguments. + */ + public async provideCompletionItems( + model: ITextModel, + position: Position, + context: CompletionContext, + token: CancellationToken, + ): Promise { + + const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); + if (!promptType) { + // if the model is not a prompt, we don't provide any completions + return undefined; + } + + const parser = this.promptsService.getSyntaxParserFor(model); + await parser.start(token).settled(); + + if (token.isCancellationRequested) { + return undefined; + } + + if (!parser.header) { + return undefined; + } + await parser.header.settled; + + const fullHeaderRange = parser.header.range; + const headerRange = new Range(fullHeaderRange.startLineNumber + 1, 0, fullHeaderRange.endLineNumber - 1, model.getLineMaxColumn(fullHeaderRange.endLineNumber - 1),); + + if (!headerRange.containsPosition(position)) { + // if the position is not inside the header, we don't provide any completions + return undefined; + } + + const lineText = model.getLineContent(position.lineNumber); + const colonIndex = lineText.indexOf(':'); + const colonPosition = colonIndex !== -1 ? new Position(position.lineNumber, colonIndex + 1) : undefined; + + if (!colonPosition || position.isBeforeOrEqual(colonPosition)) { + return this.providePropertyCompletions(model, position, headerRange, colonPosition, promptType); + } else if (colonPosition && colonPosition.isBefore(position)) { + return this.provideValueCompletions(model, position, headerRange, colonPosition, promptType); + } + return undefined; + } + private async providePropertyCompletions( + model: ITextModel, + position: Position, + headerRange: Range, + colonPosition: Position | undefined, + promptType: string, + ): Promise { + + const suggestions: CompletionItem[] = []; + const supportedProperties = this.getSupportedProperties(promptType); + this.removeUsedProperties(supportedProperties, model, headerRange, position); + + const getInsertText = (property: string): string => { + if (colonPosition) { + return property; + } + const valueSuggestions = this.getValueSuggestions(promptType, property); + if (valueSuggestions.length > 0) { + return `${property}: \${0:${valueSuggestions[0]}}`; + } else { + return `${property}: \$0`; + } + }; + + + for (const property of supportedProperties) { + const item: CompletionItem = { + label: property, + kind: CompletionItemKind.Property, + insertText: getInsertText(property), + insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, + range: new Range(position.lineNumber, 1, position.lineNumber, !colonPosition ? model.getLineMaxColumn(position.lineNumber) : colonPosition.column), + }; + suggestions.push(item); + } + + return { suggestions }; + } + + private async provideValueCompletions( + model: ITextModel, + position: Position, + headerRange: Range, + colonPosition: Position, + promptType: string, + ): Promise { + + const suggestions: CompletionItem[] = []; + const lineContent = model.getLineContent(position.lineNumber); + const property = lineContent.substring(0, colonPosition.column - 1).trim(); + + if (!this.getSupportedProperties(promptType).has(property)) { + return undefined; + } + const bracketIndex = lineContent.indexOf('['); + if (bracketIndex !== -1 && bracketIndex <= position.column - 1) { + // if the property is already inside a bracket, we don't provide value completions + return undefined; + } + + const values = this.getValueSuggestions(promptType, property); + for (const value of values) { + const item: CompletionItem = { + label: value, + kind: CompletionItemKind.Value, + insertText: value, + range: new Range(position.lineNumber, position.column, position.lineNumber, model.getLineMaxColumn(position.lineNumber)), + }; + suggestions.push(item); + } + return { suggestions }; + } + + private getSupportedProperties(promptType: string): Set { + switch (promptType) { + case PromptsType.instructions: + return new Set(['applyTo', 'description']); + case PromptsType.prompt: + return new Set(['mode', 'tools', 'description']); + default: + return new Set(['tools', 'description']); + } + } + + private removeUsedProperties(properties: Set, model: ITextModel, headerRange: Range, position: Position): void { + for (let i = headerRange.startLineNumber; i <= headerRange.endLineNumber; i++) { + if (i !== position.lineNumber) { + const lineText = model.getLineContent(i); + const colonIndex = lineText.indexOf(':'); + if (colonIndex !== -1) { + const property = lineText.substring(0, colonIndex).trim(); + properties.delete(property); + } + } + } + } + + private getValueSuggestions(promptType: string, property: string): string[] { + if (promptType === PromptsType.instructions && property === 'applyTo') { + return ['**', '**/*.ts, **/*.js', '**/*.php', '**/*.py']; + } + if (promptType === PromptsType.prompt && property === 'mode') { + return ['agent', 'edit', 'ask']; + } + if (property === 'tools' && (promptType === PromptsType.prompt || promptType === PromptsType.mode)) { + return ['[]', `['codebase', 'editFiles', 'fetch']`]; + } + return []; + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileContributions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileContributions.ts index 9e277049e46..e569a0c2942 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileContributions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileContributions.ts @@ -12,6 +12,7 @@ import { PromptLinkDiagnosticsInstanceManager } from './languageProviders/prompt import { PromptHeaderDiagnosticsInstanceManager } from './languageProviders/promptHeaderDiagnosticsProvider.js'; import { isWindows } from '../../../../../base/common/platform.js'; import { PromptPathAutocompletion } from './languageProviders/promptPathAutocompletion.js'; +import { PromptHeaderAutocompletion } from './languageProviders/promptHeaderAutocompletion.js'; /** @@ -43,7 +44,7 @@ export function registerPromptFileContributions(): void { if (!isWindows) { registerContribution(PromptPathAutocompletion); } - + registerContribution(PromptHeaderAutocompletion); registerContribution(ConfigMigration); }