Path: blob/main/extensions/copilot/src/extension/prompts/node/panel/definitionAtPosition.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 { PromptElement, PromptElementProps, PromptPiece, PromptSizing } from '@vscode/prompt-tsx';6import type * as vscode from 'vscode';7import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';8import { isScenarioAutomation } from '../../../../platform/env/common/envService';9import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext';10import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';11import { ILanguageFeaturesService, isLocationLink } from '../../../../platform/languages/common/languageFeaturesService';12import { ILogService } from '../../../../platform/log/common/logService';13import { IParserService } from '../../../../platform/parser/node/parserService';14import { getWasmLanguage } from '../../../../platform/parser/node/treeSitterLanguages';15import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';16import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';17import { getLanguage } from '../../../../util/common/languages';18import * as arrays from '../../../../util/vs/base/common/arrays';19import { ExtensionMode, Selection, Uri } from '../../../../vscodeTypes';20import { asyncComputeWithTimeBudget } from '../../../context/node/resolvers/selectionContextHelpers';21import { determineNodeToDocument } from '../../../prompt/node/definitionAroundCursor';22import { CodeBlock } from './safeElements';2324type Props = PromptElementProps<{25document: TextDocumentSnapshot;26/**27* Range of interest for which definitions are to be found.28* @remark if not provided, will use active selection in currently active editor29*/30position: vscode.Position;31/**32* Timeout for finding implementations in milliseconds. Defaults to 200ms.33*/34timeoutMs?: number;35}>;3637type CodeExcerpt = {38languageId: string;39uri: Uri;40code: string;41excerptRange: vscode.Range;42};4344export type State =45| {46k: 'found';47definitions: CodeExcerpt[];48}49| {50/** Document should be ignored by Copilot */51k: 'ignored';52}53;545556/**57* @remark Excludes definitions that are in copilot-ignored files.58*/59export class DefinitionAtPosition extends PromptElement<Props, State> {6061private static DEFAULT_TIMEOUT_MS = 200;6263constructor(64props: Props,65@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,66@IWorkspaceService private readonly _workspaceService: IWorkspaceService,67@IVSCodeExtensionContext private readonly _vscodeExtensionCtxService: IVSCodeExtensionContext,68@IIgnoreService private readonly _ignoreService: IIgnoreService,69@ILogService private readonly _logService: ILogService,70@IParserService private readonly _parserService: IParserService,71@ITelemetryService private readonly _telemetryService: ITelemetryService,72) {73super(props);74}7576override async prepare(): Promise<State> {77if (await this._ignoreService.isCopilotIgnored(this.props.document.uri)) {78return { k: 'ignored' };79}8081const timeout = this._vscodeExtensionCtxService.extensionMode === ExtensionMode.Test && !isScenarioAutomation82? 083: (this.props.timeoutMs === undefined ? DefinitionAtPosition.DEFAULT_TIMEOUT_MS : this.props.timeoutMs);8485const definitions = await this.findDefinition(timeout);8687this._logService.debug(`Found ${definitions.length} implementation(s)/definition(s)`);88if (definitions.length > 0) {89this._logService.debug(`Implementation(s)/definition(s) found:` + JSON.stringify(definitions, null, '\t'));90}9192return {93k: 'found',94definitions95};96}9798override render(state: State, sizing: PromptSizing): PromptPiece<any, any> | undefined {99100if (state.k === 'ignored') {101return <ignoredFiles value={[this.props.document.uri]} />;102}103104const { document, position } = this.props;105106const { definitions } = state;107108if (definitions.length === 0) {109const line = document.lineAt(position.line);110definitions.push({111languageId: document.languageId,112uri: document.uri,113code: line.text,114excerptRange: line.range,115});116}117118return (119<>120Relevant definition{definitions.length > 1 ? 's' : ''}: <br />121<br />122{definitions.map(codeBlock => {123const lineCommentStart = getLanguage(codeBlock.languageId).lineComment.start;124const filePath = `${lineCommentStart} FILEPATH: ${codeBlock.uri.path}`;125const code = `${filePath}\n${codeBlock.code}`;126return (127<CodeBlock uri={codeBlock.uri} languageId={codeBlock.languageId} code={code} />128);129})}130</>131);132}133134135private async findDefinition(timeoutMs: number): Promise<CodeExcerpt[]> {136const { document, position } = this.props;137138// find implementation or, if not found, definition139const findImplOrDefinition = async (position: vscode.Position) => {140try {141const impls = await this._languageFeaturesService.getImplementations(document.uri, position);142143this._logService.debug(`Found ${impls.length} implementations` + JSON.stringify(impls, null, '\t'));144145if (impls.length > 0) {146return impls;147}148} catch { }149150try {151const defs = await this._languageFeaturesService.getDefinitions(document.uri, position);152153this._logService.debug(`Found ${defs.length} definitions` + JSON.stringify(defs, null, '\t'));154155if (defs.length > 0) {156return defs;157}158} catch { }159160this._logService.debug(`No definitions or implementations found`);161162return [];163};164165const foundDefs = await asyncComputeWithTimeBudget(this._logService, this._telemetryService, document, timeoutMs * 3, () => findImplOrDefinition(position), []);166167const nonIgnoredDefs = arrays.coalesce(168await Promise.all(169foundDefs.map(async def => {170const uri = isLocationLink(def) ? def.targetUri : document.uri;171return await this._ignoreService.isCopilotIgnored(uri) ? undefined : def;172})173)174);175176// since language service gives us only links to identifiers, expand to whole implementation/definition using tree-sitter177178return Promise.all(179nonIgnoredDefs.map(async def => {180181const { uri, range } = isLocationLink(def) ? { uri: def.targetUri, range: def.targetRange } : def;182183const docContainingDef = await this._workspaceService.openTextDocumentAndSnapshot(uri);184185const wasmLanguage = getWasmLanguage(docContainingDef.languageId);186187let code: string;188let excerptRange: vscode.Range;189if (wasmLanguage === undefined) { // capture at least the line of the definition190const line = docContainingDef.lineAt(range.start.line);191code = line.text;192excerptRange = line.range;193} else {194const nodeToDocument = await determineNodeToDocument(this._parserService, this._telemetryService, {195document: docContainingDef,196language: getLanguage(document.languageId),197wholeRange: range,198selection: new Selection(range.start, range.end),199fileIndentInfo: undefined,200});201excerptRange = nodeToDocument.range;202code = docContainingDef.getText(excerptRange);203}204205return {206languageId: docContainingDef.languageId,207uri: docContainingDef.uri,208code,209excerptRange,210} satisfies CodeExcerpt;211})212);213}214}215216217