Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts
3296 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 { URI } from '../../../../base/common/uri.js';
7
import { Emitter, Event } from '../../../../base/common/event.js';
8
import { IIdentifiedSingleEditOperation, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js';
9
import { CTX_INLINE_CHAT_HAS_STASHED_SESSION } from '../common/inlineChat.js';
10
import { IRange, Range } from '../../../../editor/common/core/range.js';
11
import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js';
12
import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js';
13
import { DetailedLineRangeMapping, LineRangeMapping, RangeMapping } from '../../../../editor/common/diff/rangeMapping.js';
14
import { IInlineChatSessionService } from './inlineChatSessionService.js';
15
import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js';
16
import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js';
17
import { coalesceInPlace } from '../../../../base/common/arrays.js';
18
import { Iterable } from '../../../../base/common/iterator.js';
19
import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js';
20
import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
21
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
22
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
23
import { ILogService } from '../../../../platform/log/common/log.js';
24
import { ChatModel, IChatRequestModel, IChatTextEditGroupState } from '../../chat/common/chatModel.js';
25
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
26
import { IChatAgent } from '../../chat/common/chatAgents.js';
27
import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js';
28
29
30
export type TelemetryData = {
31
extension: string;
32
rounds: string;
33
undos: string;
34
unstashed: number;
35
edits: number;
36
finishedByEdit: boolean;
37
startTime: string;
38
endTime: string;
39
acceptedHunks: number;
40
discardedHunks: number;
41
responseTypes: string;
42
};
43
44
export type TelemetryDataClassification = {
45
owner: 'jrieken';
46
comment: 'Data about an interaction editor session';
47
extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension providing the data' };
48
rounds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of request that were made' };
49
undos: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Requests that have been undone' };
50
edits: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Did edits happen while the session was active' };
51
unstashed: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How often did this session become stashed and resumed' };
52
finishedByEdit: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Did edits cause the session to terminate' };
53
startTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When the session started' };
54
endTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When the session ended' };
55
acceptedHunks: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of accepted hunks' };
56
discardedHunks: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of discarded hunks' };
57
responseTypes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Comma separated list of response types like edits, message, mixed' };
58
};
59
60
61
export class SessionWholeRange {
62
63
private static readonly _options: IModelDecorationOptions = ModelDecorationOptions.register({ description: 'inlineChat/session/wholeRange' });
64
65
private readonly _onDidChange = new Emitter<this>();
66
readonly onDidChange: Event<this> = this._onDidChange.event;
67
68
private _decorationIds: string[] = [];
69
70
constructor(private readonly _textModel: ITextModel, wholeRange: IRange) {
71
this._decorationIds = _textModel.deltaDecorations([], [{ range: wholeRange, options: SessionWholeRange._options }]);
72
}
73
74
dispose() {
75
this._onDidChange.dispose();
76
if (!this._textModel.isDisposed()) {
77
this._textModel.deltaDecorations(this._decorationIds, []);
78
}
79
}
80
81
fixup(changes: readonly DetailedLineRangeMapping[]): void {
82
const newDeco: IModelDeltaDecoration[] = [];
83
for (const { modified } of changes) {
84
const modifiedRange = this._textModel.validateRange(modified.isEmpty
85
? new Range(modified.startLineNumber, 1, modified.startLineNumber, Number.MAX_SAFE_INTEGER)
86
: new Range(modified.startLineNumber, 1, modified.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER));
87
88
newDeco.push({ range: modifiedRange, options: SessionWholeRange._options });
89
}
90
const [first, ...rest] = this._decorationIds; // first is the original whole range
91
const newIds = this._textModel.deltaDecorations(rest, newDeco);
92
this._decorationIds = [first].concat(newIds);
93
this._onDidChange.fire(this);
94
}
95
96
get trackedInitialRange(): Range {
97
const [first] = this._decorationIds;
98
return this._textModel.getDecorationRange(first) ?? new Range(1, 1, 1, 1);
99
}
100
101
get value(): Range {
102
let result: Range | undefined;
103
for (const id of this._decorationIds) {
104
const range = this._textModel.getDecorationRange(id);
105
if (range) {
106
if (!result) {
107
result = range;
108
} else {
109
result = Range.plusRange(result, range);
110
}
111
}
112
}
113
return result!;
114
}
115
}
116
117
export class Session {
118
119
private _isUnstashed: boolean = false;
120
private readonly _startTime = new Date();
121
private readonly _teldata: TelemetryData;
122
123
private readonly _versionByRequest = new Map<string, number>();
124
125
constructor(
126
readonly headless: boolean,
127
/**
128
* The URI of the document which is being EditorEdit
129
*/
130
readonly targetUri: URI,
131
/**
132
* A copy of the document at the time the session was started
133
*/
134
readonly textModel0: ITextModel,
135
/**
136
* The model of the editor
137
*/
138
readonly textModelN: ITextModel,
139
readonly agent: IChatAgent,
140
readonly wholeRange: SessionWholeRange,
141
readonly hunkData: HunkData,
142
readonly chatModel: ChatModel,
143
versionsByRequest?: [string, number][], // DEBT? this is needed when a chat model is "reused" for a new chat session
144
) {
145
146
this._teldata = {
147
extension: ExtensionIdentifier.toKey(agent.extensionId),
148
startTime: this._startTime.toISOString(),
149
endTime: this._startTime.toISOString(),
150
edits: 0,
151
finishedByEdit: false,
152
rounds: '',
153
undos: '',
154
unstashed: 0,
155
acceptedHunks: 0,
156
discardedHunks: 0,
157
responseTypes: ''
158
};
159
if (versionsByRequest) {
160
this._versionByRequest = new Map(versionsByRequest);
161
}
162
}
163
164
get isUnstashed(): boolean {
165
return this._isUnstashed;
166
}
167
168
markUnstashed() {
169
this._teldata.unstashed! += 1;
170
this._isUnstashed = true;
171
}
172
173
markModelVersion(request: IChatRequestModel) {
174
this._versionByRequest.set(request.id, this.textModelN.getAlternativeVersionId());
175
}
176
177
get versionsByRequest() {
178
return Array.from(this._versionByRequest);
179
}
180
181
async undoChangesUntil(requestId: string): Promise<boolean> {
182
183
const targetAltVersion = this._versionByRequest.get(requestId);
184
if (targetAltVersion === undefined) {
185
return false;
186
}
187
// undo till this point
188
this.hunkData.ignoreTextModelNChanges = true;
189
try {
190
while (targetAltVersion < this.textModelN.getAlternativeVersionId() && this.textModelN.canUndo()) {
191
await this.textModelN.undo();
192
}
193
} finally {
194
this.hunkData.ignoreTextModelNChanges = false;
195
}
196
return true;
197
}
198
199
get hasChangedText(): boolean {
200
return !this.textModel0.equalsTextBuffer(this.textModelN.getTextBuffer());
201
}
202
203
asChangedText(changes: readonly LineRangeMapping[]): string | undefined {
204
if (changes.length === 0) {
205
return undefined;
206
}
207
208
let startLine = Number.MAX_VALUE;
209
let endLine = Number.MIN_VALUE;
210
for (const change of changes) {
211
startLine = Math.min(startLine, change.modified.startLineNumber);
212
endLine = Math.max(endLine, change.modified.endLineNumberExclusive);
213
}
214
215
return this.textModelN.getValueInRange(new Range(startLine, 1, endLine, Number.MAX_VALUE));
216
}
217
218
recordExternalEditOccurred(didFinish: boolean) {
219
this._teldata.edits += 1;
220
this._teldata.finishedByEdit = didFinish;
221
}
222
223
asTelemetryData(): TelemetryData {
224
225
for (const item of this.hunkData.getInfo()) {
226
switch (item.getState()) {
227
case HunkState.Accepted:
228
this._teldata.acceptedHunks += 1;
229
break;
230
case HunkState.Rejected:
231
this._teldata.discardedHunks += 1;
232
break;
233
}
234
}
235
236
this._teldata.endTime = new Date().toISOString();
237
return this._teldata;
238
}
239
}
240
241
242
export class StashedSession {
243
244
private readonly _listener: IDisposable;
245
private readonly _ctxHasStashedSession: IContextKey<boolean>;
246
private _session: Session | undefined;
247
248
constructor(
249
editor: ICodeEditor,
250
session: Session,
251
private readonly _undoCancelEdits: IValidEditOperation[],
252
@IContextKeyService contextKeyService: IContextKeyService,
253
@IInlineChatSessionService private readonly _sessionService: IInlineChatSessionService,
254
@ILogService private readonly _logService: ILogService
255
) {
256
this._ctxHasStashedSession = CTX_INLINE_CHAT_HAS_STASHED_SESSION.bindTo(contextKeyService);
257
258
// keep session for a little bit, only release when user continues to work (type, move cursor, etc.)
259
this._session = session;
260
this._ctxHasStashedSession.set(true);
261
this._listener = Event.once(Event.any(editor.onDidChangeCursorSelection, editor.onDidChangeModelContent, editor.onDidChangeModel, editor.onDidBlurEditorWidget))(() => {
262
this._session = undefined;
263
this._sessionService.releaseSession(session);
264
this._ctxHasStashedSession.reset();
265
});
266
}
267
268
dispose() {
269
this._listener.dispose();
270
this._ctxHasStashedSession.reset();
271
if (this._session) {
272
this._sessionService.releaseSession(this._session);
273
}
274
}
275
276
unstash(): Session | undefined {
277
if (!this._session) {
278
return undefined;
279
}
280
this._listener.dispose();
281
const result = this._session;
282
result.markUnstashed();
283
result.hunkData.ignoreTextModelNChanges = true;
284
result.textModelN.pushEditOperations(null, this._undoCancelEdits, () => null);
285
result.hunkData.ignoreTextModelNChanges = false;
286
this._session = undefined;
287
this._logService.debug('[IE] Unstashed session');
288
return result;
289
}
290
}
291
292
// ---
293
294
function lineRangeAsRange(lineRange: LineRange, model: ITextModel): Range {
295
return lineRange.isEmpty
296
? new Range(lineRange.startLineNumber, 1, lineRange.startLineNumber, Number.MAX_SAFE_INTEGER)
297
: new Range(lineRange.startLineNumber, 1, lineRange.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER);
298
}
299
300
export class HunkData {
301
302
private static readonly _HUNK_TRACKED_RANGE = ModelDecorationOptions.register({
303
description: 'inline-chat-hunk-tracked-range',
304
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges
305
});
306
307
private static readonly _HUNK_THRESHOLD = 8;
308
309
private readonly _store = new DisposableStore();
310
private readonly _data = new Map<RawHunk, RawHunkData>();
311
private _ignoreChanges: boolean = false;
312
313
constructor(
314
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
315
private readonly _textModel0: ITextModel,
316
private readonly _textModelN: ITextModel,
317
) {
318
319
this._store.add(_textModelN.onDidChangeContent(e => {
320
if (!this._ignoreChanges) {
321
this._mirrorChanges(e);
322
}
323
}));
324
}
325
326
dispose(): void {
327
if (!this._textModelN.isDisposed()) {
328
this._textModelN.changeDecorations(accessor => {
329
for (const { textModelNDecorations } of this._data.values()) {
330
textModelNDecorations.forEach(accessor.removeDecoration, accessor);
331
}
332
});
333
}
334
if (!this._textModel0.isDisposed()) {
335
this._textModel0.changeDecorations(accessor => {
336
for (const { textModel0Decorations } of this._data.values()) {
337
textModel0Decorations.forEach(accessor.removeDecoration, accessor);
338
}
339
});
340
}
341
this._data.clear();
342
this._store.dispose();
343
}
344
345
set ignoreTextModelNChanges(value: boolean) {
346
this._ignoreChanges = value;
347
}
348
349
get ignoreTextModelNChanges(): boolean {
350
return this._ignoreChanges;
351
}
352
353
private _mirrorChanges(event: IModelContentChangedEvent) {
354
355
// mirror textModelN changes to textModel0 execept for those that
356
// overlap with a hunk
357
358
type HunkRangePair = { rangeN: Range; range0: Range; markAccepted: () => void };
359
const hunkRanges: HunkRangePair[] = [];
360
361
const ranges0: Range[] = [];
362
363
for (const entry of this._data.values()) {
364
365
if (entry.state === HunkState.Pending) {
366
// pending means the hunk's changes aren't "sync'd" yet
367
for (let i = 1; i < entry.textModelNDecorations.length; i++) {
368
const rangeN = this._textModelN.getDecorationRange(entry.textModelNDecorations[i]);
369
const range0 = this._textModel0.getDecorationRange(entry.textModel0Decorations[i]);
370
if (rangeN && range0) {
371
hunkRanges.push({
372
rangeN, range0,
373
markAccepted: () => entry.state = HunkState.Accepted
374
});
375
}
376
}
377
378
} else if (entry.state === HunkState.Accepted) {
379
// accepted means the hunk's changes are also in textModel0
380
for (let i = 1; i < entry.textModel0Decorations.length; i++) {
381
const range = this._textModel0.getDecorationRange(entry.textModel0Decorations[i]);
382
if (range) {
383
ranges0.push(range);
384
}
385
}
386
}
387
}
388
389
hunkRanges.sort((a, b) => Range.compareRangesUsingStarts(a.rangeN, b.rangeN));
390
ranges0.sort(Range.compareRangesUsingStarts);
391
392
const edits: IIdentifiedSingleEditOperation[] = [];
393
394
for (const change of event.changes) {
395
396
let isOverlapping = false;
397
398
let pendingChangesLen = 0;
399
400
for (const entry of hunkRanges) {
401
if (entry.rangeN.getEndPosition().isBefore(Range.getStartPosition(change.range))) {
402
// pending hunk _before_ this change. When projecting into textModel0 we need to
403
// subtract that. Because diffing is relaxed it might include changes that are not
404
// actual insertions/deletions. Therefore we need to take the length of the original
405
// range into account.
406
pendingChangesLen += this._textModelN.getValueLengthInRange(entry.rangeN);
407
pendingChangesLen -= this._textModel0.getValueLengthInRange(entry.range0);
408
409
} else if (Range.areIntersectingOrTouching(entry.rangeN, change.range)) {
410
// an edit overlaps with a (pending) hunk. We take this as a signal
411
// to mark the hunk as accepted and to ignore the edit. The range of the hunk
412
// will be up-to-date because of decorations created for them
413
entry.markAccepted();
414
isOverlapping = true;
415
break;
416
417
} else {
418
// hunks past this change aren't relevant
419
break;
420
}
421
}
422
423
if (isOverlapping) {
424
// hunk overlaps, it grew
425
continue;
426
}
427
428
const offset0 = change.rangeOffset - pendingChangesLen;
429
const start0 = this._textModel0.getPositionAt(offset0);
430
431
let acceptedChangesLen = 0;
432
for (const range of ranges0) {
433
if (range.getEndPosition().isBefore(start0)) {
434
// accepted hunk _before_ this projected change. When projecting into textModel0
435
// we need to add that
436
acceptedChangesLen += this._textModel0.getValueLengthInRange(range);
437
}
438
}
439
440
const start = this._textModel0.getPositionAt(offset0 + acceptedChangesLen);
441
const end = this._textModel0.getPositionAt(offset0 + acceptedChangesLen + change.rangeLength);
442
edits.push(EditOperation.replace(Range.fromPositions(start, end), change.text));
443
}
444
445
this._textModel0.pushEditOperations(null, edits, () => null);
446
}
447
448
async recompute(editState: IChatTextEditGroupState, diff?: IDocumentDiff | null) {
449
450
diff ??= await this._editorWorkerService.computeDiff(this._textModel0.uri, this._textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: false }, 'advanced');
451
452
let mergedChanges: DetailedLineRangeMapping[] = [];
453
454
if (diff && diff.changes.length > 0) {
455
// merge changes neighboring changes
456
mergedChanges = [diff.changes[0]];
457
for (let i = 1; i < diff.changes.length; i++) {
458
const lastChange = mergedChanges[mergedChanges.length - 1];
459
const thisChange = diff.changes[i];
460
if (thisChange.modified.startLineNumber - lastChange.modified.endLineNumberExclusive <= HunkData._HUNK_THRESHOLD) {
461
mergedChanges[mergedChanges.length - 1] = new DetailedLineRangeMapping(
462
lastChange.original.join(thisChange.original),
463
lastChange.modified.join(thisChange.modified),
464
(lastChange.innerChanges ?? []).concat(thisChange.innerChanges ?? [])
465
);
466
} else {
467
mergedChanges.push(thisChange);
468
}
469
}
470
}
471
472
const hunks = mergedChanges.map(change => new RawHunk(change.original, change.modified, change.innerChanges ?? []));
473
474
editState.applied = hunks.length;
475
476
this._textModelN.changeDecorations(accessorN => {
477
478
this._textModel0.changeDecorations(accessor0 => {
479
480
// clean up old decorations
481
for (const { textModelNDecorations, textModel0Decorations } of this._data.values()) {
482
textModelNDecorations.forEach(accessorN.removeDecoration, accessorN);
483
textModel0Decorations.forEach(accessor0.removeDecoration, accessor0);
484
}
485
486
this._data.clear();
487
488
// add new decorations
489
for (const hunk of hunks) {
490
491
const textModelNDecorations: string[] = [];
492
const textModel0Decorations: string[] = [];
493
494
textModelNDecorations.push(accessorN.addDecoration(lineRangeAsRange(hunk.modified, this._textModelN), HunkData._HUNK_TRACKED_RANGE));
495
textModel0Decorations.push(accessor0.addDecoration(lineRangeAsRange(hunk.original, this._textModel0), HunkData._HUNK_TRACKED_RANGE));
496
497
for (const change of hunk.changes) {
498
textModelNDecorations.push(accessorN.addDecoration(change.modifiedRange, HunkData._HUNK_TRACKED_RANGE));
499
textModel0Decorations.push(accessor0.addDecoration(change.originalRange, HunkData._HUNK_TRACKED_RANGE));
500
}
501
502
this._data.set(hunk, {
503
editState,
504
textModelNDecorations,
505
textModel0Decorations,
506
state: HunkState.Pending
507
});
508
}
509
});
510
});
511
}
512
513
get size(): number {
514
return this._data.size;
515
}
516
517
get pending(): number {
518
return Iterable.reduce(this._data.values(), (r, { state }) => r + (state === HunkState.Pending ? 1 : 0), 0);
519
}
520
521
private _discardEdits(item: HunkInformation): ISingleEditOperation[] {
522
const edits: ISingleEditOperation[] = [];
523
const rangesN = item.getRangesN();
524
const ranges0 = item.getRanges0();
525
for (let i = 1; i < rangesN.length; i++) {
526
const modifiedRange = rangesN[i];
527
528
const originalValue = this._textModel0.getValueInRange(ranges0[i]);
529
edits.push(EditOperation.replace(modifiedRange, originalValue));
530
}
531
return edits;
532
}
533
534
discardAll() {
535
const edits: ISingleEditOperation[][] = [];
536
for (const item of this.getInfo()) {
537
if (item.getState() === HunkState.Pending) {
538
edits.push(this._discardEdits(item));
539
}
540
}
541
const undoEdits: IValidEditOperation[][] = [];
542
this._textModelN.pushEditOperations(null, edits.flat(), (_undoEdits) => {
543
undoEdits.push(_undoEdits);
544
return null;
545
});
546
return undoEdits.flat();
547
}
548
549
getInfo(): HunkInformation[] {
550
551
const result: HunkInformation[] = [];
552
553
for (const [hunk, data] of this._data.entries()) {
554
const item: HunkInformation = {
555
getState: () => {
556
return data.state;
557
},
558
isInsertion: () => {
559
return hunk.original.isEmpty;
560
},
561
getRangesN: () => {
562
const ranges = data.textModelNDecorations.map(id => this._textModelN.getDecorationRange(id));
563
coalesceInPlace(ranges);
564
return ranges;
565
},
566
getRanges0: () => {
567
const ranges = data.textModel0Decorations.map(id => this._textModel0.getDecorationRange(id));
568
coalesceInPlace(ranges);
569
return ranges;
570
},
571
discardChanges: () => {
572
// DISCARD: replace modified range with original value. The modified range is retrieved from a decoration
573
// which was created above so that typing in the editor keeps discard working.
574
if (data.state === HunkState.Pending) {
575
const edits = this._discardEdits(item);
576
this._textModelN.pushEditOperations(null, edits, () => null);
577
data.state = HunkState.Rejected;
578
if (data.editState.applied > 0) {
579
data.editState.applied -= 1;
580
}
581
}
582
},
583
acceptChanges: () => {
584
// ACCEPT: replace original range with modified value. The modified value is retrieved from the model via
585
// its decoration and the original range is retrieved from the hunk.
586
if (data.state === HunkState.Pending) {
587
const edits: ISingleEditOperation[] = [];
588
const rangesN = item.getRangesN();
589
const ranges0 = item.getRanges0();
590
for (let i = 1; i < ranges0.length; i++) {
591
const originalRange = ranges0[i];
592
const modifiedValue = this._textModelN.getValueInRange(rangesN[i]);
593
edits.push(EditOperation.replace(originalRange, modifiedValue));
594
}
595
this._textModel0.pushEditOperations(null, edits, () => null);
596
data.state = HunkState.Accepted;
597
}
598
}
599
};
600
result.push(item);
601
}
602
603
return result;
604
}
605
}
606
607
class RawHunk {
608
constructor(
609
readonly original: LineRange,
610
readonly modified: LineRange,
611
readonly changes: RangeMapping[]
612
) { }
613
}
614
615
type RawHunkData = {
616
textModelNDecorations: string[];
617
textModel0Decorations: string[];
618
state: HunkState;
619
editState: IChatTextEditGroupState;
620
};
621
622
export const enum HunkState {
623
Pending = 0,
624
Accepted = 1,
625
Rejected = 2
626
}
627
628
export interface HunkInformation {
629
/**
630
* The first element [0] is the whole modified range and subsequent elements are word-level changes
631
*/
632
getRangesN(): Range[];
633
634
getRanges0(): Range[];
635
636
isInsertion(): boolean;
637
638
discardChanges(): void;
639
640
/**
641
* Accept the hunk. Applies the corresponding edits into textModel0
642
*/
643
acceptChanges(): void;
644
645
getState(): HunkState;
646
}
647
648