Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompts/node/panel/symbolAtCursor.tsx
13405 views
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 * as l10n from '@vscode/l10n';
7
import { BasePromptElementProps, PrioritizedList, PromptElement, PromptPiece, PromptSizing, UserMessage } from '@vscode/prompt-tsx';
8
import type { CancellationToken, ChatResponseReferencePart, Progress, Uri } from 'vscode';
9
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
10
import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';
11
import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';
12
import { ILanguageFeaturesService, isLocationLink } from '../../../../platform/languages/common/languageFeaturesService';
13
import { TreeSitterExpressionLocationInfo } from '../../../../platform/parser/node/nodes';
14
import { IParserService, treeSitterOffsetRangeToVSCodeRange, vscodeToTreeSitterOffsetRange } from '../../../../platform/parser/node/parserService';
15
import { IScopeSelector } from '../../../../platform/scopeSelection/common/scopeSelection';
16
import { ITabsAndEditorsService } from '../../../../platform/tabs/common/tabsAndEditorsService';
17
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
18
import { basename } from '../../../../util/vs/base/common/path';
19
import { ChatResponseProgressPart, Range, Selection } from '../../../../vscodeTypes';
20
import { CodeBlock } from './safeElements';
21
import { treeSitterInfoToContext } from './symbolDefinitions';
22
23
export interface SymbolAtCursorProps extends BasePromptElementProps {
24
document?: TextDocumentSnapshot;
25
selection?: Selection;
26
priority: number;
27
}
28
29
export type SelectedScope = {
30
symbolAtCursorState: SymbolAtCursorState;
31
definition?: {
32
identifier: string | undefined;
33
text: string;
34
range: Range;
35
uri: Uri;
36
startIndex: number;
37
endIndex: number;
38
};
39
symbolAtCursor?: {
40
selectedText: string;
41
document: TextDocumentSnapshot;
42
range: Range;
43
};
44
} | undefined;
45
46
type SymbolAtCursorState = {
47
document: TextDocumentSnapshot;
48
range: Range;
49
codeAtCursor: string;
50
definitions: TreeSitterExpressionLocationInfo[];
51
references: TreeSitterExpressionLocationInfo[];
52
} | undefined;
53
54
export class SymbolAtCursor extends PromptElement<SymbolAtCursorProps, SymbolAtCursorState> {
55
56
constructor(
57
props: SymbolAtCursorProps,
58
@IIgnoreService private readonly _ignoreService: IIgnoreService,
59
@IConfigurationService private readonly _configurationService: IConfigurationService,
60
@ITabsAndEditorsService private readonly _tabsAndEditorsService: ITabsAndEditorsService,
61
@IScopeSelector private readonly _scopeSelector: IScopeSelector,
62
@IParserService private readonly _parserService: IParserService,
63
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
64
@IWorkspaceService private readonly _workspaceService: IWorkspaceService,
65
) {
66
super(props);
67
}
68
69
private static getSymbolAtCursor(tabsAndEditorsService: ITabsAndEditorsService, props: Omit<SymbolAtCursorProps, 'priority'>): {
70
selectedText: string;
71
document: TextDocumentSnapshot;
72
range: Range;
73
} | undefined {
74
let { selection, document } = props;
75
76
// If not provided, fall back to the active editor
77
if (!selection || !document) {
78
const editor = tabsAndEditorsService.activeTextEditor;
79
if (!editor) {
80
return;
81
}
82
83
selection = editor.selection;
84
document = TextDocumentSnapshot.create(editor.document);
85
}
86
87
// This component should not return anything if we have a real selection
88
if (!selection.isEmpty) {
89
return;
90
}
91
92
const range = document.getWordRangeAtPosition(selection.active) ?? selection;
93
const selectedText = document.getText(range);
94
return { selectedText, document, range };
95
}
96
97
static async getDefinitionAtRange(ignoreService: IIgnoreService, parserService: IParserService, document: TextDocumentSnapshot, range: Range, preferDefinitions: boolean) {
98
const fileIsIgnored = await ignoreService.isCopilotIgnored(document.uri);
99
if (fileIsIgnored) {
100
return undefined;
101
}
102
103
const treeSitterAST = parserService.getTreeSitterAST(document);
104
if (treeSitterAST === undefined) {
105
return undefined;
106
}
107
108
const treeSitterOffsetRange = vscodeToTreeSitterOffsetRange(range, document);
109
let nodeContext;
110
if (preferDefinitions) {
111
nodeContext = await treeSitterAST.getNodeToExplain(treeSitterOffsetRange);
112
}
113
nodeContext ??= await treeSitterAST.getNodeToDocument(treeSitterOffsetRange);
114
const { startIndex, endIndex } = 'nodeToDocument' in nodeContext ? nodeContext.nodeToDocument : nodeContext.nodeToExplain;
115
const expandedRange = treeSitterOffsetRangeToVSCodeRange(document, { startIndex, endIndex });
116
117
return { identifier: nodeContext.nodeIdentifier, text: document.getText(expandedRange), range: expandedRange, uri: document.uri, startIndex, endIndex };
118
}
119
120
static async getSelectedScope(ignoreService: IIgnoreService, configurationService: IConfigurationService, tabsAndEditorsService: ITabsAndEditorsService, scopeSelector: IScopeSelector, parserService: IParserService, props: Omit<SymbolAtCursorProps, 'priority'>): Promise<SelectedScope> {
121
if (!props.document || await ignoreService.isCopilotIgnored(props.document.uri)) {
122
return undefined;
123
}
124
125
const symbolAtCursor = SymbolAtCursor.getSymbolAtCursor(tabsAndEditorsService, props);
126
let symbolAtCursorState: SymbolAtCursorState | undefined;
127
const definition = symbolAtCursor ? await SymbolAtCursor.getDefinitionAtRange(ignoreService, parserService, symbolAtCursor.document, symbolAtCursor.range, false) : undefined;
128
const isExplicitScopeSelectionEnabled = configurationService.getConfig(ConfigKey.ExplainScopeSelection);
129
if (isExplicitScopeSelectionEnabled || symbolAtCursor && (definition?.identifier !== symbolAtCursor.selectedText || !symbolAtCursor.selectedText)) {
130
// The cursor wasn't somewhere that clearly indicates intent
131
const editor = tabsAndEditorsService.activeTextEditor;
132
if (!editor) {
133
return;
134
}
135
const rangeOfEnclosingSymbol = await scopeSelector.selectEnclosingScope(editor, { reason: l10n.t('Select an enclosing range to explain'), includeBlocks: true });
136
if (rangeOfEnclosingSymbol) {
137
const document = TextDocumentSnapshot.create(editor.document);
138
const definitionText = document.getText(rangeOfEnclosingSymbol);
139
if (!definitionText) {
140
return;
141
}
142
143
symbolAtCursorState = { codeAtCursor: definitionText, document, range: rangeOfEnclosingSymbol, definitions: [], references: [] };
144
}
145
}
146
147
return { symbolAtCursorState, definition, symbolAtCursor };
148
}
149
150
override async prepare(sizing: PromptSizing, progress: Progress<ChatResponseReferencePart | ChatResponseProgressPart>, token: CancellationToken): Promise<SymbolAtCursorState> {
151
const selectedScope = await SymbolAtCursor.getSelectedScope(this._ignoreService, this._configurationService, this._tabsAndEditorsService, this._scopeSelector, this._parserService, this.props).catch(() => undefined);
152
if (!selectedScope) {
153
return;
154
}
155
let { symbolAtCursorState, definition, symbolAtCursor } = selectedScope;
156
if (!symbolAtCursor) {
157
return;
158
}
159
160
if (definition?.identifier === symbolAtCursor?.selectedText) {
161
// If the cursor is on a symbol reference, include the line of code that the cursor is on and the definition
162
symbolAtCursorState ??= {
163
codeAtCursor: definition.text,
164
document: symbolAtCursor.document,
165
range: definition.range,
166
definitions: [],
167
references: [],
168
};
169
} else {
170
// Use the current line of code that the cursor is on
171
symbolAtCursorState ??= {
172
codeAtCursor: symbolAtCursor.document.lineAt(symbolAtCursor.range.start).text,
173
document: symbolAtCursor.document,
174
range: symbolAtCursor.document.lineAt(symbolAtCursor.range.start).range,
175
definitions: [],
176
references: []
177
};
178
}
179
180
// Enrich symbol state with definitions
181
progress.report(new ChatResponseProgressPart(l10n.t("Searching for relevant definitions...")));
182
try {
183
for (const link of await this._languageFeaturesService.getDefinitions(symbolAtCursor.document.uri, symbolAtCursor.range.start)) {
184
const { uri, range } = isLocationLink(link) ? { uri: link.targetUri, range: link.targetRange } : link;
185
if (range.isEqual(symbolAtCursor.range)) {
186
continue;
187
}
188
const textDocument = await this._workspaceService.openTextDocumentAndSnapshot(uri);
189
const definition = await SymbolAtCursor.getDefinitionAtRange(this._ignoreService, this._parserService, textDocument, range, true);
190
if (definition) {
191
symbolAtCursorState.definitions.push(definition);
192
}
193
}
194
} catch { }
195
196
// Enrich symbol state with references
197
progress.report(new ChatResponseProgressPart(l10n.t("Searching for relevant references...")));
198
try {
199
const seenReferences = new Set<string>();
200
for (const link of await this._languageFeaturesService.getReferences(symbolAtCursor.document.uri, symbolAtCursor.range.start)) {
201
const { uri, range } = isLocationLink(link) ? { uri: link.targetUri, range: link.targetRange } : link;
202
if (range.isEqual(symbolAtCursor.range)) {
203
continue;
204
}
205
const key = `${uri.toString()}-${range.start.line}-${range.start.character}-${range.end.line}-${range.end.character}`;
206
if (seenReferences.has(key)) {
207
continue;
208
}
209
seenReferences.add(key);
210
const textDocument = await this._workspaceService.openTextDocumentAndSnapshot(uri);
211
const reference = await SymbolAtCursor.getDefinitionAtRange(this._ignoreService, this._parserService, textDocument, range, false);
212
if (reference) {
213
symbolAtCursorState.references.push(reference);
214
}
215
}
216
} catch { }
217
218
return symbolAtCursorState;
219
}
220
221
override render(state: SymbolAtCursorState, sizing: PromptSizing): PromptPiece<any, any> | undefined {
222
if (!state) {
223
return;
224
}
225
226
// Include a reference for the code on the line that the cursor is on
227
const info = [...state.definitions, ...state.references];
228
if (state.codeAtCursor) {
229
const { startIndex, endIndex } = vscodeToTreeSitterOffsetRange(state.range, state.document);
230
info.push({ version: state.document.version, uri: state.document.uri, range: state.range, text: state.codeAtCursor, startIndex, endIndex });
231
}
232
233
const { references } = treeSitterInfoToContext(state.document, info);
234
235
return (<>
236
<references value={references} />
237
<UserMessage>
238
I have the following code in the active editor:<br />
239
<CodeBlock uri={state.document.uri} languageId={state.document.languageId} code={state.codeAtCursor} />
240
<br />
241
{Boolean(state.definitions.length) && <>Here are some relevant definitions for the symbols in my code:<br /></>}
242
{Boolean(state.definitions.length) && <PrioritizedList priority={this.props.priority} descending={true}>{state.definitions.map(def => <CodeBlock uri={state.document.uri} languageId={state.document.languageId} code={def.text} />)}</PrioritizedList >}
243
{/* Priority for references should be lower than priority for definitions */}
244
{Boolean(state.references.length) && <><br />Here are some places where the the symbols in my code are referenced:<br /></>}
245
{Boolean(state.references.length) &&
246
<PrioritizedList priority={this.props.priority - state.definitions.length} descending={true}>
247
{state.references.map(ref => <>
248
{Boolean(ref.uri) && <>From the file {basename(ref.uri!.toString())}:<br /></>}
249
<CodeBlock uri={state.document.uri} languageId={state.document.languageId} code={ref.text} /><br />
250
</>)}
251
</PrioritizedList>}
252
</UserMessage>
253
</>);
254
}
255
}
256
257