Skip to content

Commit 5bb20de

Browse files
calculate snapshot id of symbol at reading to prevent unexpected behavior if modifying at inspecting (#1336)
* fix: instrumentSimpleOperation swallows exception * adjust the position and initial range of inline chat for fixing. * adjust the selection of revealing the first inspection. * calculate snapshot id at reading symbol, because symbol may change. * rename versionId to snapshotId * fix messages. * refine prompt * prevent duplicate inspecting. * refine prompt * handle error `response got filtered` * handle invalid inspection comments * return original java version value getting from ls. * refactor: Improve inspection message handling and error handling * Refactor InspectionCache to handle snapshot inspections with non inspections. * remove `Ignore ...` code action. * add support for inspections of fields * fix messages. * enhance prompts to better support context info and refactor * fix error message to provide more details. * fix prompt
1 parent 613de16 commit 5bb20de

File tree

9 files changed

+239
-194
lines changed

9 files changed

+239
-194
lines changed

src/copilot/Copilot.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { LanguageModelChatMessage, lm, Disposable, CancellationToken, LanguageModelChatRequestOptions, LanguageModelChatMessageRole, LanguageModelChatSelector } from "vscode";
2-
import { instrumentSimpleOperation, sendInfo } from "vscode-extension-telemetry-wrapper";
3-
import { logger } from "./utils";
2+
import { fixedInstrumentSimpleOperation, logger } from "./utils";
3+
import { sendInfo } from "vscode-extension-telemetry-wrapper";
44

55
export default class Copilot {
66
public static readonly DEFAULT_END_MARK = '<|endofresponse|>';
@@ -10,6 +10,7 @@ export default class Copilot {
1010
public static readonly NOT_CANCELLABEL: CancellationToken = { isCancellationRequested: false, onCancellationRequested: () => Disposable.from() };
1111

1212
public constructor(
13+
private readonly systemMessagesOrSamples: LanguageModelChatMessage[],
1314
private readonly modelSelector: LanguageModelChatSelector = Copilot.DEFAULT_MODEL,
1415
private readonly modelOptions: LanguageModelChatRequestOptions = Copilot.DEFAULT_MODEL_OPTIONS,
1516
private readonly maxRounds: number = Copilot.DEFAULT_MAX_ROUNDS,
@@ -18,14 +19,13 @@ export default class Copilot {
1819
}
1920

2021
private async doSend(
21-
systemMessagesOrSamples: LanguageModelChatMessage[],
2222
userMessage: string,
2323
modelOptions: LanguageModelChatRequestOptions = Copilot.DEFAULT_MODEL_OPTIONS,
2424
cancellationToken: CancellationToken = Copilot.NOT_CANCELLABEL
2525
): Promise<string> {
2626
let answer: string = '';
2727
let rounds: number = 0;
28-
const messages = [...systemMessagesOrSamples];
28+
const messages = [...this.systemMessagesOrSamples];
2929
const _send = async (message: string): Promise<boolean> => {
3030
rounds++;
3131
logger.debug(`User: \n`, message);
@@ -37,15 +37,18 @@ export default class Copilot {
3737
try {
3838
const model = (await lm.selectChatModels(this.modelSelector))?.[0];
3939
if (!model) {
40-
throw new Error('No model selected');
40+
const models = await lm.selectChatModels();
41+
throw new Error(`No suitable model, available models: [${models.map(m => m.name).join(', ')}]. Please make sure you have installed the latest "GitHub Copilot Chat" (v0.16.0 or later).`);
4142
}
4243
const response = await model.sendRequest(messages, modelOptions ?? this.modelOptions, cancellationToken);
4344
for await (const item of response.text) {
4445
rawAnswer += item;
4546
}
4647
} catch (e) {
47-
logger.error(`Failed to send request to copilot`, e);
48-
throw new Error(`Failed to send request to copilot: ${e}`);
48+
//@ts-ignore
49+
const cause = e.cause || e;
50+
logger.error(`Failed to chat with copilot`, cause);
51+
throw cause;
4952
}
5053
messages.push(new LanguageModelChatMessage(LanguageModelChatMessageRole.Assistant, rawAnswer));
5154
logger.debug(`Copilot: \n`, rawAnswer);
@@ -63,11 +66,10 @@ export default class Copilot {
6366
}
6467

6568
public async send(
66-
systemMessagesOrSamples: LanguageModelChatMessage[],
6769
userMessage: string,
6870
modelOptions: LanguageModelChatRequestOptions = Copilot.DEFAULT_MODEL_OPTIONS,
6971
cancellationToken: CancellationToken = Copilot.NOT_CANCELLABEL
7072
): Promise<string> {
71-
return instrumentSimpleOperation("java.copilot.sendRequest", this.doSend.bind(this))(systemMessagesOrSamples, userMessage, modelOptions, cancellationToken);
73+
return fixedInstrumentSimpleOperation("java.copilot.sendRequest", this.doSend.bind(this))(userMessage, modelOptions, cancellationToken);
7274
}
7375
}

src/copilot/inspect/InspectActionCodeLensProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class InspectActionCodeLensProvider implements CodeLensProvider {
2222
const topLevelCodeLenses: CodeLens[] = [];
2323
const classes = await getTopLevelClassesOfDocument(document);
2424
classes.map(clazz => new CodeLens(clazz.range, {
25-
title: "Rewrite with new Java syntax",
25+
title: "Rewrite with new Java syntax",
2626
command: COMMAND_INSPECT_CLASS,
2727
arguments: [document, clazz]
2828
})).forEach(codeLens => topLevelCodeLenses.push(codeLens));

src/copilot/inspect/Inspection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export namespace Inspection {
3939
export function revealFirstLineOfInspection(inspection: Inspection) {
4040
inspection.document && void workspace.openTextDocument(inspection.document.uri).then(document => {
4141
void window.showTextDocument(document).then(editor => {
42-
const range = document.lineAt(inspection.problem.position.line).range;
42+
const range = getIndicatorRangeOfInspection(inspection.problem);
4343
editor.selection = new Selection(range.start, range.end);
4444
editor.revealRange(range);
4545
});
Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { SymbolKind, TextDocument } from 'vscode';
2-
import { METHOD_KINDS, getClassesAndMethodsContainedInRange, getClassesAndMethodsOfDocument, logger } from '../utils';
2+
import { METHOD_KINDS, getSymbolsContainedInRange, getSymbolsOfDocument, logger } from '../utils';
33
import { Inspection } from './Inspection';
4-
import * as crypto from "crypto";
54
import { SymbolNode } from './SymbolNode';
65

76
/**
87
* A map based cache for inspections of a document.
9-
* format: `Map<documentKey, Map<symbolQualifiedName, [symbolVersionId, Inspection[]]`
8+
* format: `Map<documentKey, Map<symbolQualifiedName, [symbolSnapshotId, Inspection[]]`
109
*/
11-
const DOC_SYMBOL_VERSION_INSPECTIONS: Map<string, Map<string, [string, Inspection[]]>> = new Map();
10+
const DOC_SYMBOL_SNAPSHOT_INSPECTIONS: Map<string, Map<string, [string, Inspection[]]>> = new Map();
1211

1312
export default class InspectionCache {
1413
/**
@@ -18,15 +17,14 @@ export default class InspectionCache {
1817
public static async hasCache(document: TextDocument, symbol?: SymbolNode): Promise<boolean> {
1918
const documentKey = document.uri.fsPath;
2019
if (!symbol) {
21-
return DOC_SYMBOL_VERSION_INSPECTIONS.has(documentKey);
20+
return DOC_SYMBOL_SNAPSHOT_INSPECTIONS.has(documentKey);
2221
}
23-
const symbolInspections = DOC_SYMBOL_VERSION_INSPECTIONS.get(documentKey);
22+
const symbolInspections = DOC_SYMBOL_SNAPSHOT_INSPECTIONS.get(documentKey);
2423
// check if the symbol or its contained symbols are cached
25-
const symbols = await getClassesAndMethodsContainedInRange(symbol.range, document);
24+
const symbols = await getSymbolsContainedInRange(symbol.range, document);
2625
for (const s of symbols) {
27-
const versionInspections = symbolInspections?.get(s.qualifiedName);
28-
const symbolVersionId = InspectionCache.calculateSymbolVersionId(document, s);
29-
if (versionInspections?.[0] === symbolVersionId) {
26+
const snapshotInspections = symbolInspections?.get(s.qualifiedName);
27+
if (snapshotInspections?.[0] === s.snapshotId && snapshotInspections[1].length > 0) {
3028
return true;
3129
}
3230
}
@@ -39,7 +37,7 @@ export default class InspectionCache {
3937
* their content has changed.
4038
*/
4139
public static async getCachedInspectionsOfDoc(document: TextDocument): Promise<Inspection[]> {
42-
const symbols: SymbolNode[] = await getClassesAndMethodsOfDocument(document);
40+
const symbols: SymbolNode[] = await getSymbolsOfDocument(document);
4341
const inspections: Inspection[] = [];
4442
// we don't get cached inspections directly from the cache, because we need to filter out outdated symbols
4543
for (const symbol of symbols) {
@@ -54,12 +52,11 @@ export default class InspectionCache {
5452
*/
5553
public static getCachedInspectionsOfSymbol(document: TextDocument, symbol: SymbolNode): Inspection[] {
5654
const documentKey = document.uri.fsPath;
57-
const symbolInspections = DOC_SYMBOL_VERSION_INSPECTIONS.get(documentKey);
58-
const versionInspections = symbolInspections?.get(symbol.qualifiedName);
59-
const symbolVersionId = InspectionCache.calculateSymbolVersionId(document, symbol);
60-
if (versionInspections?.[0] === symbolVersionId) {
55+
const symbolInspections = DOC_SYMBOL_SNAPSHOT_INSPECTIONS.get(documentKey);
56+
const snapshotInspections = symbolInspections?.get(symbol.qualifiedName);
57+
if (snapshotInspections?.[0] === symbol.snapshotId) {
6158
logger.debug(`cache hit for ${SymbolKind[symbol.kind]} ${symbol.qualifiedName} of ${document.uri.fsPath}`);
62-
const inspections = versionInspections[1];
59+
const inspections = snapshotInspections[1];
6360
inspections.forEach(s => {
6461
s.document = document;
6562
s.problem.position.line = s.problem.position.relativeLine + symbol.range.start.line;
@@ -78,7 +75,7 @@ export default class InspectionCache {
7875
return isMethod ?
7976
// NOTE: method inspections are inspections whose `position.line` is within the method's range
8077
inspectionLine >= symbol.range.start.line && inspectionLine <= symbol.range.end.line :
81-
// NOTE: class inspections are inspections whose `position.line` is exactly the first line number of the class
78+
// NOTE: class/field inspections are inspections whose `position.line` is exactly the first line number of the class/field
8279
inspectionLine === symbol.range.start.line;
8380
});
8481
// re-calculate `relativeLine` of method inspections, `relativeLine` is the relative line number to the start of the method
@@ -93,13 +90,13 @@ export default class InspectionCache {
9390
*/
9491
public static invalidateInspectionCache(document?: TextDocument, symbol?: SymbolNode, inspeciton?: Inspection): void {
9592
if (!document) {
96-
DOC_SYMBOL_VERSION_INSPECTIONS.clear();
93+
DOC_SYMBOL_SNAPSHOT_INSPECTIONS.clear();
9794
} else if (!symbol) {
9895
const documentKey = document.uri.fsPath;
99-
DOC_SYMBOL_VERSION_INSPECTIONS.delete(documentKey);
96+
DOC_SYMBOL_SNAPSHOT_INSPECTIONS.delete(documentKey);
10097
} else if (!inspeciton) {
10198
const documentKey = document.uri.fsPath;
102-
const symbolInspections = DOC_SYMBOL_VERSION_INSPECTIONS.get(documentKey);
99+
const symbolInspections = DOC_SYMBOL_SNAPSHOT_INSPECTIONS.get(documentKey);
103100
// remove the cached inspections of the symbol
104101
symbolInspections?.delete(symbol.qualifiedName);
105102
// remove the cached inspections of contained symbols
@@ -110,11 +107,10 @@ export default class InspectionCache {
110107
});
111108
} else {
112109
const documentKey = document.uri.fsPath;
113-
const symbolInspections = DOC_SYMBOL_VERSION_INSPECTIONS.get(documentKey);
114-
const versionInspections = symbolInspections?.get(symbol.qualifiedName);
115-
const symbolVersionId = InspectionCache.calculateSymbolVersionId(document, symbol);
116-
if (versionInspections?.[0] === symbolVersionId) {
117-
const inspections = versionInspections[1];
110+
const symbolInspections = DOC_SYMBOL_SNAPSHOT_INSPECTIONS.get(documentKey);
111+
const snapshotInspections = symbolInspections?.get(symbol.qualifiedName);
112+
if (snapshotInspections?.[0] === symbol.snapshotId) {
113+
const inspections = snapshotInspections[1];
118114
// remove the inspection
119115
inspections.splice(inspections.indexOf(inspeciton), 1);
120116
}
@@ -124,22 +120,13 @@ export default class InspectionCache {
124120
private static cacheSymbolInspections(document: TextDocument, symbol: SymbolNode, inspections: Inspection[]): void {
125121
logger.debug(`cache ${inspections.length} inspections for ${SymbolKind[symbol.kind]} ${symbol.qualifiedName} of ${document.uri.fsPath}`);
126122
const documentKey = document.uri.fsPath;
127-
const symbolVersionId = InspectionCache.calculateSymbolVersionId(document, symbol);
128-
const cachedSymbolInspections = DOC_SYMBOL_VERSION_INSPECTIONS.get(documentKey) ?? new Map();
123+
const cachedSymbolInspections = DOC_SYMBOL_SNAPSHOT_INSPECTIONS.get(documentKey) ?? new Map();
129124
inspections.forEach(s => {
130125
s.document = document;
131126
s.symbol = symbol;
132127
});
133128
// use qualified name to prevent conflicts between symbols with the same signature in same document
134-
cachedSymbolInspections.set(symbol.qualifiedName, [symbolVersionId, inspections]);
135-
DOC_SYMBOL_VERSION_INSPECTIONS.set(documentKey, cachedSymbolInspections);
136-
}
137-
138-
/**
139-
* generate a unique id for the symbol based on its content, so that we can detect if the symbol has changed
140-
*/
141-
private static calculateSymbolVersionId(document: TextDocument, symbol: SymbolNode): string {
142-
const body = document.getText(symbol.range);
143-
return crypto.createHash('md5').update(body).digest("hex")
129+
cachedSymbolInspections.set(symbol.qualifiedName, [symbol.snapshotId, inspections]);
130+
DOC_SYMBOL_SNAPSHOT_INSPECTIONS.set(documentKey, cachedSymbolInspections);
144131
}
145132
}

0 commit comments

Comments
 (0)