-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Expand file tree
/
Copy pathxtabCustomDiffPatchResponseHandler.ts
More file actions
150 lines (136 loc) · 5.19 KB
/
xtabCustomDiffPatchResponseHandler.ts
File metadata and controls
150 lines (136 loc) · 5.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DocumentId } from '../../../platform/inlineEdits/common/dataTypes/documentId';
import { NoNextEditReason, StreamedEdit } from '../../../platform/inlineEdits/common/statelessNextEditProvider';
import { ILogger } from '../../../platform/log/common/logService';
import { ErrorUtils } from '../../../util/common/errors';
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
import { isAbsolute } from '../../../util/vs/base/common/path';
import { URI } from '../../../util/vs/base/common/uri';
import { LineReplacement } from '../../../util/vs/editor/common/core/edits/lineEdit';
import { LineRange } from '../../../util/vs/editor/common/core/ranges/lineRange';
import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange';
import { toUniquePath } from '../common/promptCraftingUtils';
import { ResponseTags } from '../common/tags';
import { CurrentDocument } from '../common/xtabCurrentDocument';
class Patch {
public removedLines: string[] = [];
public addedLines: string[] = [];
private constructor(
/**
* Expected to be file path relative to workspace root.
*/
public readonly filePath: string,
public readonly lineNumZeroBased: number,
) { }
public static ofLine(line: string): Patch | null {
const match = line.match(/^(.+):(\d+)$/);
if (!match) {
return null;
}
const [, filename, lineNumber] = match;
return new Patch(filename, parseInt(lineNumber, 10));
}
addLine(line: string) {
const contentLine = line.slice(1);
if (line.startsWith('-')) {
this.removedLines.push(contentLine);
return true;
} else if (line.startsWith('+')) {
this.addedLines.push(contentLine);
return true;
} else {
return false;
}
}
public toString(): string {
return [
`${this.filePath}:${this.lineNumZeroBased}`,
...this.removedLines.map(l => `-${l}`),
...this.addedLines.map(l => `+${l}`),
].join('\n');
}
}
export class XtabCustomDiffPatchResponseHandler {
public static async *handleResponse(
linesStream: AsyncIterable<string>,
currentDocument: CurrentDocument,
activeDocumentId: DocumentId,
workspaceRoot: URI | undefined,
window: OffsetRange | undefined,
parentTracer: ILogger,
getFetchFailure?: () => NoNextEditReason | undefined,
cancellationToken: CancellationToken = CancellationToken.None,
): AsyncGenerator<StreamedEdit, NoNextEditReason, void> {
const tracer = parentTracer.createSubLogger(['XtabCustomDiffPatchResponseHandler', 'handleResponse']);
const activeDocRelativePath = toUniquePath(activeDocumentId, workspaceRoot?.path);
try {
for await (const edit of XtabCustomDiffPatchResponseHandler.extractEdits(linesStream)) {
if (cancellationToken.isCancellationRequested) {
return new NoNextEditReason.GotCancelled('duringStreaming');
}
const fetchFailure = getFetchFailure?.();
if (fetchFailure) {
return fetchFailure;
}
const targetDocument = edit.filePath === activeDocRelativePath
? activeDocumentId
: XtabCustomDiffPatchResponseHandler.resolveTargetDocument(edit.filePath, workspaceRoot);
if (!targetDocument) {
tracer.error(`Could not resolve target document for edit: ${edit.toString()}`);
continue;
}
yield {
edit: XtabCustomDiffPatchResponseHandler.resolveEdit(edit),
isFromCursorJump: false,
targetDocument,
window,
} satisfies StreamedEdit;
}
} catch (e: unknown) {
const err = ErrorUtils.fromUnknown(e);
return new NoNextEditReason.Unexpected(err);
}
return new NoNextEditReason.NoSuggestions(currentDocument.content, window, undefined);
}
private static resolveEdit(patch: Patch): LineReplacement {
return new LineReplacement(new LineRange(patch.lineNumZeroBased + 1, patch.lineNumZeroBased + 1 + patch.removedLines.length), patch.addedLines);
}
private static resolveTargetDocument(filePath: string, workspaceRoot: URI | undefined): DocumentId | undefined {
if (isAbsolute(filePath)) {
return DocumentId.create(URI.file(filePath).toString());
}
if (workspaceRoot) {
return DocumentId.create(URI.joinPath(workspaceRoot, filePath).toString());
}
// Relative path with no workspace root — cannot resolve to a valid URI
return undefined;
}
public static async *extractEdits(linesStream: AsyncIterable<string>): AsyncGenerator<Patch> {
let currentPatch: Patch | null = null;
for await (const line of linesStream) {
// if no current patch, try to parse a new one
if (line.trim() === ResponseTags.NO_EDIT) {
break;
}
if (currentPatch === null) {
currentPatch = Patch.ofLine(line);
continue;
}
// try to add line to current patch
if (currentPatch.addLine(line)) {
continue;
} else { // line does not belong to current patch, yield current and start new
if (currentPatch) {
yield currentPatch;
}
currentPatch = Patch.ofLine(line);
}
}
if (currentPatch) {
yield currentPatch;
}
}
}