Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLIPromptReferences.ts
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 type { Attachment } from '@github/copilot/sdk';6import * as l10n from '@vscode/l10n';7import type { ChatPromptReference } from 'vscode';8import { isLocation } from '../../../../util/common/types';9import { coalesce } from '../../../../util/vs/base/common/arrays';10import { Codicon } from '../../../../util/vs/base/common/codicons';11import { ResourceSet } from '../../../../util/vs/base/common/map';12import { basename } from '../../../../util/vs/base/common/resources';13import { isNumber, isString } from '../../../../util/vs/base/common/types';14import { URI } from '../../../../util/vs/base/common/uri';15import { Range as InternalRange } from '../../../../util/vs/editor/common/core/range';16import { SymbolKind } from '../../../../util/vs/workbench/api/common/extHostTypes/symbolInformation';17import { ChatReferenceDiagnostic, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, Range, Uri } from '../../../../vscodeTypes';18import { PromptFileIdPrefix } from '../../../prompt/common/chatVariablesCollection';1920/**21* Converts a ChatPromptReference into a PromptVariable entry that is used in VS code.22*/23export function convertReferenceToVariable(ref: ChatPromptReference, attachments: readonly Attachment[]) {24const value = ref.value;25const range = ref.range ? { start: ref.range[0], endExclusive: ref.range[1] } : undefined;2627if (value && value instanceof ChatReferenceDiagnostic && Array.isArray(value.diagnostics) && value.diagnostics.length && value.diagnostics[0][1].length) {28const marker = DiagnosticConverter.from(value.diagnostics[0][1][0]);29const refValue = {30filterRange: { startLineNumber: marker.startLineNumber, startColumn: marker.startColumn, endLineNumber: marker.endLineNumber, endColumn: marker.endColumn },31filterSeverity: marker.severity,32filterUri: value.diagnostics[0][0],33problemMessage: value.diagnostics[0][1][0].message34};35return IDiagnosticVariableEntryFilterData.toEntry(refValue);36}3738if (isLocation(ref.value) && ref.name.startsWith(`sym:`)) {39return {40id: ref.id,41name: ref.name,42fullName: ref.name.substring(4),43value: { uri: ref.value.uri, range: toInternalRange(ref.value.range) },44// We never send this information to extensions, so default to Property45symbolKind: SymbolKind.Property,46// We never send this information to extensions, so default to Property47icon: Codicon.symbolProperty,48kind: 'symbol',49range,50};51}5253if (URI.isUri(value) && ref.name.startsWith(`prompt:`) &&54ref.id.startsWith(PromptFileIdPrefix) &&55ref.id.endsWith(value.toString())) {56return {57id: ref.id,58name: `prompt:${basename(value)}`,59value,60kind: 'promptFile',61modelDescription: 'Prompt instructions file',62isRoot: true,63automaticallyAdded: false,64range,65};66}6768const folders = new ResourceSet(attachments.filter(att => att.type === 'directory').map(att => URI.file(att.path)));69const isFile = URI.isUri(value) || isLocation(value);70const isFolder = URI.isUri(value) && (value.path.endsWith('/') || folders.has(value));71return {72id: ref.id,73name: ref.name,74value,75modelDescription: ref.modelDescription,76range,77kind: isFolder ? 'directory' as const : isFile ? 'file' as const : 'generic' as const78};79}8081function toInternalRange(range: Range): InternalRange {82return new InternalRange(range.start.line + 1, range.start.character + 1, range.end.line + 1, range.end.character + 1);83}8485namespace DiagnosticTagConverter {8687/**88* Additional metadata about the type of a diagnostic.89*/90export enum DiagnosticTag {91/**92* Unused or unnecessary code.93*94* Diagnostics with this tag are rendered faded out. The amount of fading95* is controlled by the `"editorUnnecessaryCode.opacity"` theme color. For96* example, `"editorUnnecessaryCode.opacity": "#000000c0"` will render the97* code with 75% opacity. For high contrast themes, use the98* `"editorUnnecessaryCode.border"` theme color to underline unnecessary code99* instead of fading it out.100*/101Unnecessary = 1,102103/**104* Deprecated or obsolete code.105*106* Diagnostics with this tag are rendered with a strike through.107*/108Deprecated = 2,109}110export const enum MarkerTag {111Unnecessary = 1,112Deprecated = 2113}114115116export function from(value: DiagnosticTag) {117118switch (value) {119case DiagnosticTag.Unnecessary:120return MarkerTag.Unnecessary;121case DiagnosticTag.Deprecated:122return MarkerTag.Deprecated;123default:124return undefined;125}126}127}128129130namespace IDiagnosticVariableEntryFilterData {131export const icon = Codicon.error;132133export function fromMarker(marker: Record<string, unknown>) {134return {135filterUri: marker.resource,136owner: marker.owner,137problemMessage: marker.message,138filterRange: { startLineNumber: marker.startLineNumber, endLineNumber: marker.endLineNumber, startColumn: marker.startColumn, endColumn: marker.endColumn }139};140}141142export function toEntry(data: Record<string, unknown>) {143return {144id: id(data),145name: label(data),146icon,147value: data,148kind: 'diagnostic',149...data,150};151}152153export function id(data: Record<string, unknown> & { filterRange?: InternalRange }) {154return [data.filterUri, data.owner, data.filterSeverity, data.filterRange?.startLineNumber, data.filterRange?.startColumn].join(':');155}156157export function label(data: Record<string, unknown> & { problemMessage?: string; filterUri?: Uri }) {158const enum TrimThreshold {159MaxChars = 30,160MaxSpaceLookback = 10,161}162if (data.problemMessage) {163if (data.problemMessage.length < TrimThreshold.MaxChars) {164return data.problemMessage;165}166167// Trim the message, on a space if it would not lose too much168// data (MaxSpaceLookback) or just blindly otherwise.169const lastSpace = data.problemMessage.lastIndexOf(' ', TrimThreshold.MaxChars);170if (lastSpace === -1 || lastSpace + TrimThreshold.MaxSpaceLookback < TrimThreshold.MaxChars) {171return data.problemMessage.substring(0, TrimThreshold.MaxChars) + '…';172}173return data.problemMessage.substring(0, lastSpace) + '…';174}175let labelStr = l10n.t("All Problems");176if (data.filterUri) {177labelStr = l10n.t("Problems in {0}", basename(data.filterUri));178}179180return labelStr;181}182}183184namespace DiagnosticConverter {185export function from(value: Diagnostic) {186let code: string | { value: string; target: Uri } | undefined;187188if (value.code) {189if (isString(value.code) || isNumber(value.code)) {190code = String(value.code);191} else {192code = {193value: String(value.code.value),194target: value.code.target,195};196}197}198199return {200...toInternalRange(value.range),201message: value.message,202source: value.source,203code,204severity: DiagnosticSeverityConverter.from(value.severity),205relatedInformation: value.relatedInformation && value.relatedInformation.map(DiagnosticRelatedInformationConverter.from),206tags: Array.isArray(value.tags) ? coalesce(value.tags.map(DiagnosticTagConverter.from)) : undefined,207};208}209}210211namespace DiagnosticRelatedInformationConverter {212export function from(value: DiagnosticRelatedInformation) {213return {214...toInternalRange(value.location.range),215message: value.message,216resource: value.location.uri217};218}219}220221namespace DiagnosticSeverityConverter {222export enum MarkerSeverity {223Hint = 1,224Info = 2,225Warning = 4,226Error = 8,227}228229export function from(value: number): MarkerSeverity {230switch (value) {231case DiagnosticSeverity.Error:232return MarkerSeverity.Error;233case DiagnosticSeverity.Warning:234return MarkerSeverity.Warning;235case DiagnosticSeverity.Information:236return MarkerSeverity.Info;237case DiagnosticSeverity.Hint:238return MarkerSeverity.Hint;239}240return MarkerSeverity.Error;241}242}243244245