Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/inlineEdit/inlineEditScoringService.ts
13394 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { mkdir } from 'fs/promises';
7
import { dirname } from 'path';
8
import { IRecordingInformation } from '../../../src/extension/inlineEdits/common/observableWorkspaceRecordingReplayer';
9
import { DocumentId } from '../../../src/platform/inlineEdits/common/dataTypes/documentId';
10
import { RootedEdit } from '../../../src/platform/inlineEdits/common/dataTypes/edit';
11
import { deserializeStringEdit, serializeStringEdit } from '../../../src/platform/inlineEdits/common/dataTypes/editUtils';
12
import { ISerializedEdit } from '../../../src/platform/workspaceRecorder/common/workspaceLog';
13
import { JSONFile } from '../../../src/util/node/jsonFile';
14
import { CachedFunction } from '../../../src/util/vs/base/common/cache';
15
import { equalsIfDefined, thisEqualsC } from '../../../src/util/vs/base/common/equals';
16
import { isDefined } from '../../../src/util/vs/base/common/types';
17
import { StringEdit } from '../../../src/util/vs/editor/common/core/edits/stringEdit';
18
import { StringText } from '../../../src/util/vs/editor/common/core/text/abstractText';
19
20
export interface IInlineEditScoringService {
21
scoreEdit(scoredEditsFilePath: string, context: ScoringContext, docId: DocumentId, editDocumentValue: StringText, edit: RootedEdit | undefined): Promise<EditScoreResult | undefined>;
22
}
23
24
/** JSON Serializable */
25
export type ScoringContext = { kind: 'unknown'; documentValueBeforeEdit: string } | { kind: 'recording'; recording: IRecordingInformation };
26
27
export type EditScoreResultCategory = 'bad' | 'valid' | 'nextEdit';
28
29
const USE_SIMPLE_SCORING = true;
30
31
export class EditScoreResult {
32
constructor(
33
public readonly category: EditScoreResultCategory,
34
/**
35
* When comparing two edits with the same scoreCategory, the one with the higher score is considered better.
36
* The score does not convey any other meaning (such as its absolute value).
37
* Should be below 100.
38
*/
39
public readonly score: number,
40
) { }
41
42
toString() {
43
return `${this.category}#${this.score}`;
44
}
45
46
getScoreValue(): number {
47
if (USE_SIMPLE_SCORING) {
48
switch (this.category) {
49
case 'bad': return 0;
50
case 'valid': return 0.1;
51
case 'nextEdit': return 1;
52
}
53
} else {
54
const getVal = () => {
55
switch (this.category) {
56
case 'bad': return 0;
57
case 'valid': return 10 + (this.score / 100) * 3;
58
case 'nextEdit': return 100 + 10 * (this.score / 100);
59
}
60
};
61
const maxValue = 110;
62
return Math.round(Math.min(getVal() / maxValue, maxValue) * 1000) / 1000;
63
}
64
}
65
}
66
67
class InlineEditScoringService implements IInlineEditScoringService {
68
private readonly _scoredEdits = new CachedFunction(async (path: string) => {
69
await mkdir(dirname(path), { recursive: true });
70
const file = await JSONFile.readOrCreate<IScoredEdits | null>(path, null, '\t');
71
72
return {
73
scoredEdits: undefined as undefined | ScoredEdits,
74
file,
75
};
76
});
77
78
async scoreEdit(scoredEditsFilePath: string, context: ScoringContext, docId: DocumentId, editDocumentValue: StringText, edit: RootedEdit | undefined): Promise<EditScoreResult | undefined> {
79
const existing = await this._scoredEdits.get(scoredEditsFilePath);
80
81
let shouldWrite = false;
82
83
if (!existing.scoredEdits) {
84
const value = existing.file.value;
85
if (!value) {
86
existing.scoredEdits = ScoredEdits.create(context);
87
shouldWrite = true; // first test run
88
} else {
89
existing.scoredEdits = ScoredEdits.fromJson(value, context);
90
shouldWrite = existing.scoredEdits.removeUnscored(); // we deleted all unscored edits (might be re-added though)
91
const shouldNormalizeExisting = false; // Edits are now normalized before adding to the score database.
92
if (shouldNormalizeExisting) {
93
shouldWrite = existing.scoredEdits.normalizeEdits(editDocumentValue.value) || shouldWrite;
94
}
95
}
96
}
97
98
const result = existing.scoredEdits.getScoreOrAddAsUnscored(docId, edit);
99
if (!result) {
100
shouldWrite = true; // edit was added as unscored
101
}
102
103
if (shouldWrite) {
104
const newData = existing.scoredEdits.serialize();
105
await existing.file.setValue(newData);
106
}
107
108
return result;
109
}
110
}
111
112
class ScoredEdits {
113
public static fromJson(data: IScoredEdits, scoringContext: ScoringContext): ScoredEdits {
114
// TOD check if context matches!
115
return new ScoredEdits(scoringContext, data.edits);
116
}
117
118
public static create(scoringContext: ScoringContext): ScoredEdits {
119
return new ScoredEdits(scoringContext, []);
120
}
121
122
private _edits: IScoredEdit[];
123
private _editMatchers: EditMatcher[] = [];
124
125
private constructor(
126
private readonly _scoringContext: ScoringContext,
127
edits: IScoredEdit[],
128
) {
129
this._edits = edits;
130
this._editMatchers = edits.map(e => new EditMatcher(e));
131
}
132
133
hasUnscored(): boolean {
134
return this._edits.some(e => !isScoredEdit(e));
135
}
136
137
normalizeEdits(source: string): boolean {
138
const existing = new Set<string>();
139
140
this._edits = this._edits.map(e => {
141
let n = e.edit ? deserializeStringEdit(e.edit).normalizeOnSource(source) : undefined;
142
if (n?.isEmpty()) {
143
n = undefined;
144
}
145
const key = e.documentUri + '#' + JSON.stringify(n?.toJson());
146
if (existing.has(key)) {
147
return null;
148
}
149
existing.add(key);
150
151
return {
152
...e,
153
edit: n ? serializeStringEdit(n) : null,
154
};
155
}).filter(isDefined);
156
157
this._editMatchers = this._edits.map(e => new EditMatcher(e));
158
159
return true;
160
}
161
162
removeUnscored(): boolean {
163
if (!this.hasUnscored()) {
164
return false;
165
}
166
this._edits = this._edits.filter(e => isScoredEdit(e));
167
this._editMatchers = this._editMatchers.filter(e => e.isScored());
168
return true;
169
}
170
171
getScoreOrAddAsUnscored(docId: DocumentId, edit: RootedEdit | undefined): EditScoreResult | undefined {
172
edit = edit?.normalize();
173
if (edit?.edit.isEmpty()) {
174
edit = undefined;
175
}
176
177
const documentUri = docId.uri;
178
179
let existingEdit = this._editMatchers.find(e => e.matches(documentUri, edit));
180
if (!existingEdit) {
181
const e: IScoredEdit = {
182
documentUri: documentUri,
183
edit: edit ? serializeStringEdit(edit.edit) : null,
184
score: 'unscored',
185
scoreCategory: 'unscored',
186
};
187
const m = new EditMatcher(e);
188
this._edits.push(e);
189
this._editMatchers.push(m);
190
191
existingEdit = m;
192
}
193
return existingEdit.getScore();
194
}
195
196
serialize(): IScoredEdits {
197
return {
198
...{
199
'$web-editor.format-json': true,
200
'$web-editor.default-url': 'https://microsoft.github.io/vscode-workbench-recorder-viewer/?editRating',
201
},
202
edits: this._edits,
203
// Last, so that it is easier to review the file
204
scoringContext: this._scoringContext,
205
};
206
}
207
}
208
209
class EditMatcher {
210
public readonly documentUri = this.data.documentUri;
211
public readonly edit: StringEdit | undefined;
212
213
constructor(
214
private readonly data: IScoredEdit,
215
) {
216
this.edit = data.edit ? deserializeStringEdit(data.edit) : undefined;
217
}
218
219
isScored(): boolean {
220
return isScoredEdit(this.data);
221
}
222
223
getScore(): EditScoreResult | undefined {
224
if (!isScoredEdit(this.data)) {
225
return undefined;
226
}
227
return new EditScoreResult(this.data.scoreCategory, this.data.score);
228
}
229
230
matches(editDocumentUri: string, edit: RootedEdit | undefined): boolean {
231
if (editDocumentUri !== this.documentUri) {
232
return false;
233
}
234
// TODO improve! (check if strings after applied the edits are the same)
235
return equalsIfDefined(this.edit, edit?.edit, thisEqualsC());
236
}
237
}
238
239
/** JSON Serializable */
240
interface IScoredEdits {
241
edits: IScoredEdit[];
242
scoringContext: ScoringContext;
243
}
244
245
/** JSON Serializable */
246
interface IScoredEdit<TUnscored = 'unscored'> {
247
documentUri: string;
248
edit: ISerializedEdit | null;
249
scoreCategory: EditScoreResultCategory | TUnscored;
250
251
/**
252
* When comparing two edits with the same scoreCategory, the one with the higher score is considered better.
253
* The score does not convey any other meaning (such as its absolute value).
254
*/
255
score: number | TUnscored;
256
}
257
258
function isScoredEdit(edit: IScoredEdit<any>): edit is IScoredEdit<never> {
259
return edit.score !== 'unscored' && edit.scoreCategory !== 'unscored';
260
}
261
262
// Has to be a singleton to avoid writing race conditions
263
export const inlineEditScoringService = new InlineEditScoringService();
264
265