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