Skip to content

Commit 2d01fca

Browse files
committed
prompt file: code complete in headers
1 parent 1d89ed6 commit 2d01fca

File tree

2 files changed

+197
-1
lines changed

2 files changed

+197
-1
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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+
7+
import { IPromptsService } from '../service/promptsService.js';
8+
import { ITextModel } from '../../../../../../editor/common/model.js';
9+
import { Disposable } from '../../../../../../base/common/lifecycle.js';
10+
import { ALL_PROMPTS_LANGUAGE_SELECTOR, getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js';
11+
import { Position } from '../../../../../../editor/common/core/position.js';
12+
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
13+
import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js';
14+
import { CompletionContext, CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList } from '../../../../../../editor/common/languages.js';
15+
import { Range } from '../../../../../../editor/common/core/range.js';
16+
17+
export class PromptHeaderAutocompletion extends Disposable implements CompletionItemProvider {
18+
/**
19+
* Debug display name for this provider.
20+
*/
21+
public readonly _debugDisplayName: string = 'PromptHeaderAutocompletion';
22+
23+
/**
24+
* List of trigger characters handled by this provider.
25+
*/
26+
public readonly triggerCharacters = [':'];
27+
28+
constructor(
29+
@IPromptsService private readonly promptsService: IPromptsService,
30+
@ILanguageFeaturesService private readonly languageService: ILanguageFeaturesService,
31+
32+
) {
33+
super();
34+
35+
this._register(this.languageService.completionProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, this));
36+
}
37+
38+
/**
39+
* The main function of this provider that calculates
40+
* completion items based on the provided arguments.
41+
*/
42+
public async provideCompletionItems(
43+
model: ITextModel,
44+
position: Position,
45+
context: CompletionContext,
46+
token: CancellationToken,
47+
): Promise<CompletionList | undefined> {
48+
49+
const promptType = getPromptsTypeForLanguageId(model.getLanguageId());
50+
if (!promptType) {
51+
// if the model is not a prompt, we don't provide any completions
52+
return undefined;
53+
}
54+
55+
const parser = this.promptsService.getSyntaxParserFor(model);
56+
await parser.start(token).settled();
57+
58+
if (token.isCancellationRequested) {
59+
return undefined;
60+
}
61+
62+
if (!parser.header) {
63+
return undefined;
64+
}
65+
await parser.header.settled;
66+
67+
const fullHeaderRange = parser.header.range;
68+
const headerRange = new Range(fullHeaderRange.startLineNumber + 1, 0, fullHeaderRange.endLineNumber - 1, model.getLineMaxColumn(fullHeaderRange.endLineNumber - 1),);
69+
70+
if (!headerRange.containsPosition(position)) {
71+
// if the position is not inside the header, we don't provide any completions
72+
return undefined;
73+
}
74+
75+
const lineText = model.getLineContent(position.lineNumber);
76+
const colonIndex = lineText.indexOf(':');
77+
const colonPosition = colonIndex !== -1 ? new Position(position.lineNumber, colonIndex + 1) : undefined;
78+
79+
if (!colonPosition || position.isBeforeOrEqual(colonPosition)) {
80+
return this.providePropertyCompletions(model, position, headerRange, colonPosition, promptType);
81+
} else if (colonPosition && colonPosition.isBefore(position)) {
82+
return this.provideValueCompletions(model, position, headerRange, colonPosition, promptType);
83+
}
84+
return undefined;
85+
}
86+
private async providePropertyCompletions(
87+
model: ITextModel,
88+
position: Position,
89+
headerRange: Range,
90+
colonPosition: Position | undefined,
91+
promptType: string,
92+
): Promise<CompletionList | undefined> {
93+
94+
const suggestions: CompletionItem[] = [];
95+
const supportedProperties = this.getSupportedProperties(promptType);
96+
this.removeUsedProperties(supportedProperties, model, headerRange, position);
97+
98+
const getInsertText = (property: string): string => {
99+
if (colonPosition) {
100+
return property;
101+
}
102+
const valueSuggestions = this.getValueSuggestions(promptType, property);
103+
if (valueSuggestions.length > 0) {
104+
return `${property}: \${0:${valueSuggestions[0]}}`;
105+
} else {
106+
return `${property}: \$0`;
107+
}
108+
};
109+
110+
111+
for (const property of supportedProperties) {
112+
const item: CompletionItem = {
113+
label: property,
114+
kind: CompletionItemKind.Property,
115+
insertText: getInsertText(property),
116+
insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,
117+
range: new Range(position.lineNumber, 1, position.lineNumber, !colonPosition ? model.getLineMaxColumn(position.lineNumber) : colonPosition.column),
118+
};
119+
suggestions.push(item);
120+
}
121+
122+
return { suggestions };
123+
}
124+
125+
private async provideValueCompletions(
126+
model: ITextModel,
127+
position: Position,
128+
headerRange: Range,
129+
colonPosition: Position,
130+
promptType: string,
131+
): Promise<CompletionList | undefined> {
132+
133+
const suggestions: CompletionItem[] = [];
134+
const lineContent = model.getLineContent(position.lineNumber);
135+
const property = lineContent.substring(0, colonPosition.column - 1).trim();
136+
137+
if (!this.getSupportedProperties(promptType).has(property)) {
138+
return undefined;
139+
}
140+
const bracketIndex = lineContent.indexOf('[');
141+
if (bracketIndex !== -1 && bracketIndex <= position.column - 1) {
142+
// if the property is already inside a bracket, we don't provide value completions
143+
return undefined;
144+
}
145+
146+
const values = this.getValueSuggestions(promptType, property);
147+
for (const value of values) {
148+
const item: CompletionItem = {
149+
label: value,
150+
kind: CompletionItemKind.Value,
151+
insertText: value,
152+
range: new Range(position.lineNumber, position.column, position.lineNumber, model.getLineMaxColumn(position.lineNumber)),
153+
};
154+
suggestions.push(item);
155+
}
156+
return { suggestions };
157+
}
158+
159+
private getSupportedProperties(promptType: string): Set<string> {
160+
switch (promptType) {
161+
case PromptsType.instructions:
162+
return new Set(['applyTo', 'description']);
163+
case PromptsType.prompt:
164+
return new Set(['mode', 'tools', 'description']);
165+
default:
166+
return new Set(['tools', 'description']);
167+
}
168+
}
169+
170+
private removeUsedProperties(properties: Set<string>, model: ITextModel, headerRange: Range, position: Position): void {
171+
for (let i = headerRange.startLineNumber; i <= headerRange.endLineNumber; i++) {
172+
if (i !== position.lineNumber) {
173+
const lineText = model.getLineContent(i);
174+
const colonIndex = lineText.indexOf(':');
175+
if (colonIndex !== -1) {
176+
const property = lineText.substring(0, colonIndex).trim();
177+
properties.delete(property);
178+
}
179+
}
180+
}
181+
}
182+
183+
private getValueSuggestions(promptType: string, property: string): string[] {
184+
if (promptType === PromptsType.instructions && property === 'applyTo') {
185+
return ['**', '**/*.ts, **/*.js', '**/*.php', '**/*.py'];
186+
}
187+
if (promptType === PromptsType.prompt && property === 'mode') {
188+
return ['agent', 'edit', 'ask'];
189+
}
190+
if (property === 'tools' && (promptType === PromptsType.prompt || promptType === PromptsType.mode)) {
191+
return ['[]', `['codebase', 'editFiles', 'fetch']`];
192+
}
193+
return [];
194+
}
195+
}

src/vs/workbench/contrib/chat/common/promptSyntax/promptFileContributions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { PromptLinkDiagnosticsInstanceManager } from './languageProviders/prompt
1212
import { PromptHeaderDiagnosticsInstanceManager } from './languageProviders/promptHeaderDiagnosticsProvider.js';
1313
import { isWindows } from '../../../../../base/common/platform.js';
1414
import { PromptPathAutocompletion } from './languageProviders/promptPathAutocompletion.js';
15+
import { PromptHeaderAutocompletion } from './languageProviders/promptHeaderAutocompletion.js';
1516

1617

1718
/**
@@ -43,7 +44,7 @@ export function registerPromptFileContributions(): void {
4344
if (!isWindows) {
4445
registerContribution(PromptPathAutocompletion);
4546
}
46-
47+
registerContribution(PromptHeaderAutocompletion);
4748
registerContribution(ConfigMigration);
4849
}
4950

0 commit comments

Comments
 (0)