Skip to content

Commit d6f749c

Browse files
add id to Inspection and migrate to stable vscode.llm api. (#1332)
* add `id` to `Inspection` * migrate to stable APIs
1 parent d6921eb commit d6f749c

13 files changed

+19329
-351
lines changed

package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,6 @@
3838
"capabilities": {
3939
"virtualWorkspaces": false
4040
},
41-
"enabledApiProposals": [
42-
"languageModels"
43-
],
4441
"activationEvents": [
4542
"onLanguage:java",
4643
"workspaceContains:pom.xml",

src/copilot/Copilot.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { LanguageModelChatMessage, LanguageModelChatUserMessage, LanguageModelChatAssistantMessage, lm, Disposable, CancellationToken, LanguageModelChatRequestOptions } from "vscode";
1+
import { LanguageModelChatMessage, lm, Disposable, CancellationToken, LanguageModelChatRequestOptions, LanguageModelChatMessageRole, LanguageModelChatSelector } from "vscode";
22
import { instrumentSimpleOperation, sendInfo } from "vscode-extension-telemetry-wrapper";
33
import { logger } from "./utils";
44

55
export default class Copilot {
66
public static readonly DEFAULT_END_MARK = '<|endofresponse|>';
77
public static readonly DEFAULT_MAX_ROUNDS = 10;
8-
public static readonly DEFAULT_MODEL = 'copilot-gpt-4';
8+
public static readonly DEFAULT_MODEL = { family: 'gpt-4' };
99
public static readonly DEFAULT_MODEL_OPTIONS: LanguageModelChatRequestOptions = { modelOptions: {} };
1010
public static readonly NOT_CANCELLABEL: CancellationToken = { isCancellationRequested: false, onCancellationRequested: () => Disposable.from() };
1111

1212
public constructor(
13-
private readonly model: string = Copilot.DEFAULT_MODEL,
13+
private readonly modelSelector: LanguageModelChatSelector = Copilot.DEFAULT_MODEL,
1414
private readonly modelOptions: LanguageModelChatRequestOptions = Copilot.DEFAULT_MODEL_OPTIONS,
1515
private readonly maxRounds: number = Copilot.DEFAULT_MAX_ROUNDS,
1616
private readonly endMark: string = Copilot.DEFAULT_END_MARK
@@ -29,20 +29,24 @@ export default class Copilot {
2929
const _send = async (message: string): Promise<boolean> => {
3030
rounds++;
3131
logger.info(`User: \n`, message);
32-
messages.push(new LanguageModelChatUserMessage(message));
32+
messages.push(new LanguageModelChatMessage(LanguageModelChatMessageRole.User, message));
3333
logger.info('Copilot: thinking...');
3434

3535
let rawAnswer: string = '';
3636
try {
37-
const response = await lm.sendChatRequest(this.model, messages, modelOptions ?? this.modelOptions, cancellationToken);
38-
for await (const item of response.stream) {
37+
const model = (await lm.selectChatModels(this.modelSelector))?.[0];
38+
if (!model) {
39+
throw new Error('No model selected');
40+
}
41+
const response = await model.sendRequest(messages, modelOptions ?? this.modelOptions, cancellationToken);
42+
for await (const item of response.text) {
3943
rawAnswer += item;
4044
}
4145
} catch (e) {
4246
logger.error(`Failed to send request to copilot`, e);
4347
throw new Error(`Failed to send request to copilot: ${e}`);
4448
}
45-
messages.push(new LanguageModelChatAssistantMessage(rawAnswer));
49+
messages.push(new LanguageModelChatMessage(LanguageModelChatMessageRole.Assistant, rawAnswer));
4650
logger.info(`Copilot: \n`, rawAnswer);
4751
answer += rawAnswer;
4852
return answer.trim().endsWith(this.endMark);

src/copilot/inspect/DocumentRenderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import InspectionCache from "./InspectionCache";
1313

1414
/**
1515
* `DocumentRenderer` is responsible for
16-
* - managing `Rewrite with new syntax` code lenses renderer
16+
* - managing `Rewrite with new Java syntax` code lenses renderer
1717
* - managing inspection renderers based on settings
1818
* - rendering inspections for a document
1919
*/

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 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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface InspectionProblem {
2727
}
2828

2929
export interface Inspection {
30+
id: string;
3031
document?: TextDocument;
3132
symbol?: SymbolNode;
3233
problem: InspectionProblem;

src/copilot/inspect/InspectionCopilot.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import Copilot from "../Copilot";
33
import { getClassesContainedInRange, getInnermostClassContainsRange, getIntersectionMethodsOfRange, getProjectJavaVersion, getUnionRange, logger } from "../utils";
44
import { Inspection } from "./Inspection";
55
import path from "path";
6-
import { TextDocument, SymbolKind, ProgressLocation, commands, Position, Range, Selection, window, LanguageModelChatSystemMessage, LanguageModelChatMessage, LanguageModelChatUserMessage, LanguageModelChatAssistantMessage } from "vscode";
6+
import { TextDocument, SymbolKind, ProgressLocation, commands, Position, Range, Selection, window, LanguageModelChatMessage, LanguageModelChatMessageRole } from "vscode";
77
import { COMMAND_FIX_INSPECTION } from "./commands";
88
import InspectionCache from "./InspectionCache";
99
import { SymbolNode } from "./SymbolNode";
10+
import { randomUUID } from "crypto";
1011

1112
export default class InspectionCopilot extends Copilot {
1213

@@ -242,9 +243,9 @@ export default class InspectionCopilot extends Copilot {
242243
}
243244

244245
const messages: LanguageModelChatMessage[] = [
245-
new LanguageModelChatSystemMessage(InspectionCopilot.SYSTEM_MESSAGE(context)),
246-
new LanguageModelChatUserMessage(InspectionCopilot.EXAMPLE_USER_MESSAGE),
247-
new LanguageModelChatAssistantMessage(InspectionCopilot.EXAMPLE_ASSISTANT_MESSAGE),
246+
LanguageModelChatMessage.User(InspectionCopilot.SYSTEM_MESSAGE(context)),
247+
LanguageModelChatMessage.User(InspectionCopilot.EXAMPLE_USER_MESSAGE),
248+
LanguageModelChatMessage.Assistant(InspectionCopilot.EXAMPLE_ASSISTANT_MESSAGE),
248249
];
249250
const codeWithInspectionComments = await this.send(messages, codeLinesContent);
250251
const inspections = this.extractInspections(codeWithInspectionComments, codeLines);
@@ -254,7 +255,10 @@ export default class InspectionCopilot extends Copilot {
254255
codeLength: code.length,
255256
codeLines: codeLines.length,
256257
insectionsCount: inspections.length,
257-
problems: inspections.map(i => i.problem.description).join(',')
258+
inspections: `[${inspections.map(i => JSON.stringify({
259+
problem: i.problem.description,
260+
solution: i.solution,
261+
})).join(',')}]`,
258262
});
259263
return inspections;
260264
}
@@ -296,6 +300,7 @@ export default class InspectionCopilot extends Copilot {
296300
*/
297301
private extractInspection(index: number, lines: string[]): Inspection {
298302
const inspection: Inspection = {
303+
id: randomUUID().toString(),
299304
problem: {
300305
description: '',
301306
position: { line: -1, relativeLine: -1, code: '' },

src/copilot/inspect/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function doActivate(context: ExtensionContext): void {
3535

3636
async function rewrite(document: TextDocument, range: Range | Selection, _context: CodeActionContext, _token: CancellationToken): Promise<CodeAction[]> {
3737
const action: CodeAction = {
38-
title: "Rewrite with new syntax",
38+
title: "Rewrite with new Java syntax",
3939
kind: CodeActionKind.RefactorRewrite,
4040
command: {
4141
title: "Rewrite selected code",

src/copilot/inspect/render/CodeLensRenderer.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
/* eslint-disable @typescript-eslint/ban-ts-comment */
2-
import { CodeLens, CodeLensProvider, Disposable, Event, EventEmitter, ExtensionContext, TextDocument, Uri, languages } from "vscode";
2+
import { CodeLens, CodeLensProvider, Command, Range, Disposable, Event, EventEmitter, ExtensionContext, TextDocument, Uri, languages } from "vscode";
33
import { Inspection } from "../Inspection";
44
import { InspectionRenderer } from "./InspectionRenderer";
55
import { logger, uncapitalize } from "../../../copilot/utils";
66
import { COMMAND_IGNORE_INSPECTIONS, COMMAND_FIX_INSPECTION } from "../commands";
77
import { capitalize } from "lodash";
8+
import _ from "lodash";
89

910
export class CodeLensRenderer implements InspectionRenderer {
10-
private readonly codeLenses: Map<Uri, CodeLens[]> = new Map();
11+
private readonly codeLenses: Map<Uri, InspectionCodeLens[]> = new Map();
1112
private readonly provider = new InspectionCodeLensProvider(this.codeLenses);
1213
private disposableRegistry: Disposable | undefined;
1314

@@ -41,27 +42,28 @@ export class CodeLensRenderer implements InspectionRenderer {
4142
if (inspections.length < 1 || !this.codeLenses) {
4243
return;
4344
}
44-
const newCodeLenses: CodeLens[] = inspections.flatMap(s => CodeLensRenderer.toCodeLenses(document, s));
45-
const newCodeLensesMessages = newCodeLenses.map(c => c.command?.title.trim());
46-
const existingCodeLenses = this.codeLenses.get(document.uri) ?? [];
47-
const leftCodeLenses = existingCodeLenses.filter(c => !newCodeLensesMessages.includes(c.command?.title.trim()));
48-
newCodeLenses.push(...leftCodeLenses);
49-
this.codeLenses.set(document.uri, newCodeLenses);
45+
const oldItems = this.codeLenses.get(document.uri) ?? [];
46+
const oldIds: string[] = _.uniq(oldItems).map(c => c.inspection.id);
47+
const newIds: string[] = inspections.map(i => i.id);
48+
const toKeep: InspectionCodeLens[] = _.intersection(oldIds, newIds).map(id => oldItems.find(c => c.inspection.id === id)!) ?? [];
49+
const toAdd: InspectionCodeLens[] = _.difference(newIds, oldIds).map(id => inspections.find(i => i.id === id)!)
50+
.flatMap(i => CodeLensRenderer.toCodeLenses(document, i));
51+
this.codeLenses.set(document.uri, [...toKeep, ...toAdd]);
5052
this.provider.refresh();
5153
}
5254

53-
private static toCodeLenses(document: TextDocument, inspection: Inspection): CodeLens[] {
55+
private static toCodeLenses(document: TextDocument, inspection: Inspection): InspectionCodeLens[] {
5456
const codeLenses = [];
5557
const range = Inspection.getIndicatorRangeOfInspection(inspection.problem);
56-
const inspectionCodeLens = new CodeLens(range, {
58+
const inspectionCodeLens = new InspectionCodeLens(inspection, range, {
5759
title: capitalize(inspection.solution),
5860
tooltip: inspection.problem.description,
5961
command: COMMAND_FIX_INSPECTION,
6062
arguments: [inspection.problem, inspection.solution, 'codelenses']
6163
});
6264
codeLenses.push(inspectionCodeLens);
6365

64-
const ignoreCodeLens = new CodeLens(range, {
66+
const ignoreCodeLens = new InspectionCodeLens(inspection, range, {
6567
title: 'Ignore',
6668
tooltip: `Ignore "${uncapitalize(inspection.problem.description)}"`,
6769
command: COMMAND_IGNORE_INSPECTIONS,
@@ -72,6 +74,12 @@ export class CodeLensRenderer implements InspectionRenderer {
7274
}
7375
}
7476

77+
class InspectionCodeLens extends CodeLens {
78+
public constructor(public readonly inspection: Inspection, range: Range, command?: Command) {
79+
super(range, command);
80+
}
81+
}
82+
7583
class InspectionCodeLensProvider implements CodeLensProvider {
7684
private readonly emitter: EventEmitter<void> = new EventEmitter<void>();
7785
public readonly onDidChangeCodeLenses: Event<void> = this.emitter.event;

src/copilot/inspect/render/DiagnosticRenderer.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Inspection } from "../Inspection";
44
import { InspectionRenderer } from "./InspectionRenderer";
55
import { logger, uncapitalize } from "../../../copilot/utils";
66
import { COMMAND_IGNORE_INSPECTIONS, COMMAND_FIX_INSPECTION } from "../commands";
7+
import _ from "lodash";
78

89
const DIAGNOSTICS_GROUP = 'java.copilot.inspection.diagnostics';
910

@@ -38,22 +39,21 @@ export class DiagnosticRenderer implements InspectionRenderer {
3839
if (inspections.length < 1 || !this.diagnostics) {
3940
return;
4041
}
41-
const newDiagnostics: Diagnostic[] = inspections.map(s => DiagnosticRenderer.toDiagnostic(s));
42-
const newDiagnosticsMessages = newDiagnostics.map(d => d.message.trim());
43-
const existingDiagnostics = this.diagnostics.get(document.uri) ?? [];
44-
const leftDiagnostics = existingDiagnostics.filter(d => !newDiagnosticsMessages.includes(d.message.trim()));
45-
newDiagnostics.push(...leftDiagnostics);
46-
this.diagnostics.set(document.uri, newDiagnostics);
42+
const oldItems: readonly InspectionDiagnostic[] = (this.diagnostics.get(document.uri) ?? []) as InspectionDiagnostic[];
43+
const oldIds: string[] = _.uniq(oldItems).map(c => c.inspection.id);
44+
const newIds: string[] = inspections.map(i => i.id);
45+
const toKeep: InspectionDiagnostic[] = _.intersection(oldIds, newIds).map(id => oldItems.find(c => c.inspection.id === id)!) ?? [];
46+
const toAdd: InspectionDiagnostic[] = _.difference(newIds, oldIds).map(id => inspections.find(i => i.id === id)!).map(i => new InspectionDiagnostic(i));
47+
this.diagnostics.set(document.uri, [...toKeep, ...toAdd]);
4748
}
49+
}
4850

49-
private static toDiagnostic(inspection: Inspection): Diagnostic {
51+
class InspectionDiagnostic extends Diagnostic {
52+
public constructor(public readonly inspection: Inspection) {
5053
const range = Inspection.getIndicatorRangeOfInspection(inspection.problem);
5154
const severiy = inspection.severity.toUpperCase() === 'HIGH' ? DiagnosticSeverity.Information : DiagnosticSeverity.Hint;
52-
const diagnostic = new Diagnostic(range, inspection.problem.description, severiy);
53-
diagnostic.source = DIAGNOSTICS_GROUP;
54-
//@ts-ignore
55-
diagnostic.additional = inspection;
56-
return diagnostic;
55+
super(range, inspection.problem.description, severiy);
56+
this.source = DIAGNOSTICS_GROUP;
5757
}
5858
}
5959

@@ -66,9 +66,7 @@ export async function fixDiagnostic(document: TextDocument, _range: Range | Sele
6666
if (diagnostic.source !== DIAGNOSTICS_GROUP) {
6767
continue;
6868
}
69-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
70-
// @ts-ignore
71-
const inspection: Inspection = diagnostic.additional as Inspection;
69+
const inspection: Inspection = (diagnostic as InspectionDiagnostic).inspection as Inspection;
7270
const fixAction: CodeAction = {
7371
title: inspection.solution,
7472
diagnostics: [diagnostic],

src/copilot/inspect/render/GutterIconRenderer.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import { InspectionRenderer } from "./InspectionRenderer";
55
import { logger } from "../../../copilot/utils";
66
import path = require("path");
77
import { COMMAND_FIX_INSPECTION } from "../commands";
8+
import _ from "lodash";
89

910
export class GutterIconRenderer implements InspectionRenderer {
10-
private readonly gutterIcons: Map<Uri, GutterIcon[]> = new Map();
11+
private readonly gutterIcons: Map<Uri, InspectionGutterIcon[]> = new Map();
1112
private gutterIconDecorationType: TextEditorDecorationType | undefined;
1213

1314
public install(context: ExtensionContext): InspectionRenderer {
@@ -46,17 +47,19 @@ export class GutterIconRenderer implements InspectionRenderer {
4647
if (inspections.length < 1 || !editor || !this.gutterIconDecorationType) {
4748
return;
4849
}
49-
const newGutterIcons: GutterIcon[] = inspections.map(s => GutterIconRenderer.toGutterIcon(s));
50-
const newGutterIconsMessages = newGutterIcons.map(d => d.inspection.solution.trim());
51-
const existingGutterIcons = this.gutterIcons.get(document.uri) ?? [];
52-
const leftGutterIcons = existingGutterIcons.filter(d => !newGutterIconsMessages.includes(d.inspection.solution.trim()));
53-
newGutterIcons.push(...leftGutterIcons);
50+
51+
const oldItems: readonly InspectionGutterIcon[] = this.gutterIcons.get(document.uri) ?? [];
52+
const oldIds: string[] = _.uniq(oldItems).map(c => c.inspection.id);
53+
const newIds: string[] = inspections.map(i => i.id);
54+
const toKeep: InspectionGutterIcon[] = _.intersection(oldIds, newIds).map(id => oldItems.find(c => c.inspection.id === id)!) ?? [];
55+
const toAdd: InspectionGutterIcon[] = _.difference(newIds, oldIds).map(id => inspections.find(i => i.id === id)!).map(i => GutterIconRenderer.toGutterIcon(i));
56+
const newGutterIcons: InspectionGutterIcon[] = [...toKeep, ...toAdd];
5457
this.gutterIcons.set(document.uri, newGutterIcons);
5558

5659
editor.setDecorations(this.gutterIconDecorationType, newGutterIcons);
5760
}
5861

59-
private static toGutterIcon(inspection: Inspection): GutterIcon {
62+
private static toGutterIcon(inspection: Inspection): InspectionGutterIcon {
6063
const range = Inspection.getIndicatorRangeOfInspection(inspection.problem);
6164
const args = [inspection.problem, inspection.solution, 'guttericons'];
6265
const commandUri = Uri.parse(`command:${COMMAND_FIX_INSPECTION}?${encodeURIComponent(JSON.stringify(args))}`);
@@ -66,6 +69,6 @@ export class GutterIconRenderer implements InspectionRenderer {
6669
}
6770
}
6871

69-
interface GutterIcon extends DecorationOptions {
72+
interface InspectionGutterIcon extends DecorationOptions {
7073
inspection: Inspection;
7174
}

0 commit comments

Comments
 (0)