chat: avoid splitting surrogate pairs in read_file (#331005)

This commit is contained in:
Martin Check
2026-08-16 07:59:52 +00:00
committed by GitHub
parent 70698c24b3
commit 09b07fe400
2 changed files with 21 additions and 2 deletions
@@ -22,6 +22,7 @@ import { IWorkspaceService } from '../../../platform/workspace/common/workspaceS
import { getCachedSha256Hash } from '../../../util/common/crypto';
import { clamp } from '../../../util/vs/base/common/numbers';
import { dirname, extUriBiasedIgnorePathCase } from '../../../util/vs/base/common/resources';
import { isHighSurrogate, isLowSurrogate } from '../../../util/vs/base/common/strings';
import { sendSkillContentReadTelemetry } from '../common/skillTelemetry';
import { URI } from '../../../util/vs/base/common/uri';
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
@@ -432,7 +433,11 @@ class ReadFileResult extends PromptElement<ReadFileResultProps> {
let contents = rawContents.split('\n').map(line => {
if (line.length > MAX_LINE_LENGTH) {
hadLongLines = true;
return line.slice(0, MAX_LINE_LENGTH) + ' [truncated]';
let end = MAX_LINE_LENGTH;
if (isHighSurrogate(line.charCodeAt(end - 1)) && isLowSurrogate(line.charCodeAt(end))) {
end--;
}
return line.slice(0, end) + ' [truncated]';
}
return line;
}).join('\n');
@@ -42,13 +42,15 @@ suite('ReadFile', () => {
const longLine = 'x'.repeat(2500);
const longLinesContent = `normal line\n${longLine}\nanother normal line\n${longLine}`;
const longLinesDoc = createTextDocumentData(URI.file('/workspace/longlines.ts'), longLinesContent, 'ts').document;
const surrogateBoundaryLine = 'x'.repeat(1999) + '\u{1F6E1}' + 'tail';
const surrogateBoundaryDoc = createTextDocumentData(URI.file('/workspace/surrogate-boundary.ts'), surrogateBoundaryLine, 'ts').document;
const services = createExtensionUnitTestingServices();
services.define(IWorkspaceService, new SyncDescriptor(
TestWorkspaceService,
[
[URI.file('/workspace')],
[testDoc, emptyDoc, whitespaceDoc, singleLineDoc, largeDoc, longLinesDoc],
[testDoc, emptyDoc, whitespaceDoc, singleLineDoc, largeDoc, longLinesDoc, surrogateBoundaryDoc],
]
));
accessor = services.createTestingAccessor();
@@ -208,6 +210,18 @@ suite('ReadFile', () => {
}
});
test('long line truncation does not split surrogate pairs', async () => {
const toolsService = accessor.get(IToolsService);
const input: IReadFileParamsV2 = {
filePath: '/workspace/surrogate-boundary.ts'
};
const result = await toolsService.invokeTool(ToolName.ReadFile, { input, toolInvocationToken: null as never }, CancellationToken.None);
const resultString = await toolResultToString(accessor, result);
const truncatedLine = resultString.split('\n').find(line => line.endsWith(' [truncated]'));
expect(truncatedLine).toBe('x'.repeat(1999) + ' [truncated]');
});
test('read file with offset beyond file line count should throw error', async () => {
const toolsService = accessor.get(IToolsService);