-
Notifications
You must be signed in to change notification settings - Fork 1.6k
nes: support patch-based model response format #2667
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||
| * Licensed under the MIT License. See License.txt in the project root for license information. | ||
| *--------------------------------------------------------------------------------------------*/ | ||
|
|
||
| import { NoNextEditReason, PushEdit, StreamedEdit } from '../../../platform/inlineEdits/common/statelessNextEditProvider'; | ||
| import { Result } from '../../../util/common/result'; | ||
| import { AsyncIterableObject } from '../../../util/vs/base/common/async'; | ||
| 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 { StringText } from '../../../util/vs/editor/common/core/text/abstractText'; | ||
| import { ResponseTags } from '../common/tags'; | ||
|
|
||
|
|
||
| class Patch { | ||
| public removedLines: string[] = []; | ||
| public addedLines: string[] = []; | ||
|
|
||
| private constructor( | ||
| public readonly filename: 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.filename}:${this.lineNumZeroBased}`, | ||
| ...this.removedLines.map(l => `-${l}`), | ||
| ...this.addedLines.map(l => `+${l}`), | ||
| ].join('\n'); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| export class XtabCustomDiffPatchResponseHandler { | ||
|
|
||
| public static async handleResponse( | ||
| pushEdit: PushEdit, | ||
| linesStream: AsyncIterableObject<string>, | ||
| documentBeforeEdits: StringText, | ||
| window: OffsetRange | undefined, | ||
| ): Promise<void> { | ||
| let editCount = 0; | ||
| for await (const edit of XtabCustomDiffPatchResponseHandler.extractEdits(linesStream)) { | ||
| editCount++; | ||
| pushEdit(Result.ok({ | ||
| edit: XtabCustomDiffPatchResponseHandler.resolveEdit(edit), | ||
| window, | ||
| // targetDocument, // TODO@ulugbekna: implement target document resolution | ||
| } satisfies StreamedEdit)); | ||
| } | ||
| if (editCount === 0) { | ||
| pushEdit(Result.error(new NoNextEditReason.NoSuggestions(documentBeforeEdits, window, undefined))); | ||
| } | ||
| } | ||
|
|
||
| private static resolveEdit(patch: Patch): LineReplacement { | ||
| return new LineReplacement(new LineRange(patch.lineNumZeroBased + 1, patch.lineNumZeroBased + 1 + patch.removedLines.length), patch.addedLines); | ||
| } | ||
|
|
||
| public static async *extractEdits(linesStream: AsyncIterableObject<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; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,96 @@ | ||||||
| /*--------------------------------------------------------------------------------------------- | ||||||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||||||
| * Licensed under the MIT License. See License.txt in the project root for license information. | ||||||
| *--------------------------------------------------------------------------------------------*/ | ||||||
|
|
||||||
| import { describe, expect, it } from 'vitest'; | ||||||
| import { AsyncIterableObject } from '../../../../util/vs/base/common/async'; | ||||||
| import { XtabCustomDiffPatchResponseHandler } from '../../node/xtabCustomDiffPatchResponseHandler'; | ||||||
|
|
||||||
| describe('XtabCustomDiffPatchResponseHandler', () => { | ||||||
|
|
||||||
| async function collectPatches(patchText: string): Promise<string> { | ||||||
| const linesStream = AsyncIterableObject.fromArray(patchText.split('\n')); | ||||||
| const patches: string[] = []; | ||||||
| for await (const patch of XtabCustomDiffPatchResponseHandler.extractEdits(linesStream)) { | ||||||
| patches.push(patch.toString()); | ||||||
| } | ||||||
| return patches.map(p => p.toString()).join('\n'); | ||||||
|
||||||
| return patches.map(p => p.toString()).join('\n'); | |
| return patches.join('\n'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point?
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test name is misleading and the behavior may not match the prompt description. The test name says "discard a patch has no removed lines" but the expectation shows the patch is NOT discarded. According to the prompt description in promptCrafting.ts line 133, patches must include "some non empty 'anchor lines' preceded by -", which suggests patches without removed lines should be invalid. Either the test expectations should show an empty result (if patches should be discarded), or the test name should be updated to reflect that such patches are accepted.
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test name is misleading. The test name says "discard a patch has no new lines" but the expectation shows the patch is NOT discarded - it's included in the output. Consider renaming to something like 'should parse patch with only removed lines (deletions)'.
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test coverage for the NO_EDIT tag scenario. The implementation on line 87 handles the case where the model outputs "<NO_EDIT>", but there's no test verifying this behavior. Consider adding a test case that verifies the handler correctly stops processing and returns no edits when it encounters the NO_EDIT tag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The prompt description states that each patch "must...include some non empty 'anchor lines' preceded by
-", which implies that patches without removed lines should be invalid. However, the implementation in xtabCustomDiffPatchResponseHandler.ts does not validate that patches have removed lines. Consider adding validation to ensure patches conform to the format described in the prompt, or update the prompt description if patches with only additions are intentionally supported.