From aeddb98376bc0c41f1b3e91c6524bd4682e0a5af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E3=80=80=E6=98=8E=E5=A3=AE?= Date: Fri, 5 Jun 2026 14:27:44 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20Update=20GetErrorsTool=20to?= =?UTF-8?q?=20handle=20Windows=20drive=20letter=20casing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extension/tools/node/getErrorsTool.tsx | 11 ++++-- .../tools/node/test/getErrorsTool.spec.tsx | 39 ++++++++++++++++++- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/extensions/copilot/src/extension/tools/node/getErrorsTool.tsx b/extensions/copilot/src/extension/tools/node/getErrorsTool.tsx index ab4f8bac1b2ab8..5aea314c3ee7ad 100644 --- a/extensions/copilot/src/extension/tools/node/getErrorsTool.tsx +++ b/extensions/copilot/src/extension/tools/node/getErrorsTool.tsx @@ -18,7 +18,7 @@ import { coalesce } from '../../../util/vs/base/common/arrays'; import { CancellationToken } from '../../../util/vs/base/common/cancellation'; import { Disposable } from '../../../util/vs/base/common/lifecycle'; import { ResourceSet } from '../../../util/vs/base/common/map'; -import { isEqualOrParent } from '../../../util/vs/base/common/resources'; +import { extUriBiasedIgnorePathCase } from '../../../util/vs/base/common/resources'; import { URI } from '../../../util/vs/base/common/uri'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { DiagnosticSeverity, ExtendedLanguageModelToolResult, LanguageModelPromptTsxPart, MarkdownString, Range } from '../../../vscodeTypes'; @@ -97,11 +97,14 @@ export class GetErrorsTool extends Disposable implements ICopilotTool; } -} \ No newline at end of file +} diff --git a/extensions/copilot/src/extension/tools/node/test/getErrorsTool.spec.tsx b/extensions/copilot/src/extension/tools/node/test/getErrorsTool.spec.tsx index 96630579f59b6b..f95205b1f02313 100644 --- a/extensions/copilot/src/extension/tools/node/test/getErrorsTool.spec.tsx +++ b/extensions/copilot/src/extension/tools/node/test/getErrorsTool.spec.tsx @@ -14,6 +14,7 @@ import { TestWorkspaceService } from '../../../../platform/test/node/testWorkspa import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService'; import { createTextDocumentData } from '../../../../util/common/test/shims/textDocument'; import { CancellationToken } from '../../../../util/vs/base/common/cancellation'; +import { isWindows } from '../../../../util/vs/base/common/platform'; import { URI } from '../../../../util/vs/base/common/uri'; import { SyncDescriptor } from '../../../../util/vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation'; @@ -38,6 +39,8 @@ suite('GetErrorsTool - Tool Invocation', () => { const noErrorFile = URI.file('/test/workspace/src/noErrorFile.ts'); const eslintErrorFile = URI.file('/test/workspace/eslint/eslint_unexpected_constant_condition_1.ts'); const emptyLineErrorFile = URI.file('/test/workspace/emptyLineError.ts'); + const upperCaseDriveFile = URI.file('C:/test/workspace/src/windowsDriveFile.ts'); + const lowerCaseDriveFile = URI.file('c:/test/workspace/src/windowsDriveFile.ts'); beforeEach(() => { collection = createExtensionUnitTestingServices(); @@ -50,8 +53,9 @@ suite('GetErrorsTool - Tool Invocation', () => { const eslintErrorDoc = createTextDocumentData(eslintErrorFile, 'if (true) {\n console.log("This is a constant condition");\n}', 'ts').document; // File with a trailing empty line where the error is reported const emptyLineErrorDoc = createTextDocumentData(emptyLineErrorFile, 'codeunit 50100 MyCU {\n procedure Foo() {\n\n', 'ts').document; + const upperCaseDriveDoc = createTextDocumentData(upperCaseDriveFile, 'const drive = "C";\n', 'ts').document; - collection.define(IWorkspaceService, new SyncDescriptor(TestWorkspaceService, [[workspaceFolder], [tsDoc1, tsDoc2, jsDoc, noErrorDoc, eslintErrorDoc, emptyLineErrorDoc]])); + collection.define(IWorkspaceService, new SyncDescriptor(TestWorkspaceService, [[workspaceFolder], [tsDoc1, tsDoc2, jsDoc, noErrorDoc, eslintErrorDoc, emptyLineErrorDoc, upperCaseDriveDoc]])); // Set up diagnostics service diagnosticsService = new TestLanguageDiagnosticsService(); @@ -138,6 +142,21 @@ suite('GetErrorsTool - Tool Invocation', () => { ]); }); + test.skipIf(!isWindows)('getDiagnostics - matches Windows file path with different drive letter casing', () => { + const diagnostic = { + message: 'Drive letter casing diagnostic', + range: new Range(0, 6, 0, 11), + severity: DiagnosticSeverity.Error + }; + diagnosticsService.setDiagnostics(upperCaseDriveFile, [diagnostic]); + + const results = tool.getDiagnostics([{ uri: lowerCaseDriveFile, range: undefined }]); + + expect(results).toEqual([ + { uri: upperCaseDriveFile, diagnostics: [diagnostic] } + ]); + }); + test('getDiagnostics - filters by folder path', () => { // Test with folder path const srcFolder = URI.file('/test/workspace/src'); @@ -211,6 +230,22 @@ suite('GetErrorsTool - Tool Invocation', () => { expect(msg).toMatchSnapshot(); }); + test.skipIf(!isWindows)('Tool invocation - filePath matches diagnostics when Windows drive letter casing differs', async () => { + diagnosticsService.setDiagnostics(upperCaseDriveFile, [ + { + message: 'Drive letter casing diagnostic', + range: new Range(0, 6, 0, 11), + severity: DiagnosticSeverity.Error + } + ]); + + const pathRep = accessor.get(IPromptPathRepresentationService); + const result = await tool.invoke({ input: { filePaths: [lowerCaseDriveFile.fsPath] }, toolInvocationToken: null! }, CancellationToken.None); + const msg = await toolResultToString(accessor, result); + expect(msg).toContain(``); + expect(msg).toContain('Drive letter casing diagnostic'); + }); + test('Tool invocation - with folder path includes diagnostics from contained files', async () => { const pathRep = accessor.get(IPromptPathRepresentationService); const srcFolderUri = URI.file('/test/workspace/src'); @@ -260,4 +295,4 @@ suite('GetErrorsTool - Tool Invocation', () => { const msg = await toolResultToString(accessor, result); expect(msg).toMatchSnapshot(); }); -}); \ No newline at end of file +}); From 7eb20bbcb6b059ca51158480fadfe68486afcf43 Mon Sep 17 00:00:00 2001 From: Wangmz-1211 Date: Sat, 6 Jun 2026 14:17:03 +0900 Subject: [PATCH 2/3] fix(getErrors): normalize only Windows drive letter for diagnostic URI matching (Issue #319858) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix with an explicit normalizeDriveLetter() that lower-cases only the drive letter component (e.g. C: → c:) before comparison, then uses the case-sensitive isEqual/isEqualOrParent helpers. This matches the Windows semantics where the drive letter is case-insensitive but the rest of the path may not be (see microsoft/vscode#319858). Add a dedicated unit-test suite covering all branches of normalizeDriveLetter: non-Windows identity, uppercase/lowercase drive letters, path components beyond the drive, short paths, root paths, UNC-style paths, and non-letter characters at the drive position. --- .../extension/tools/node/getErrorsTool.tsx | 58 +++++++++++--- .../tools/node/test/getErrorsTool.spec.tsx | 78 +++++++++++++++++++ 2 files changed, 126 insertions(+), 10 deletions(-) diff --git a/extensions/copilot/src/extension/tools/node/getErrorsTool.tsx b/extensions/copilot/src/extension/tools/node/getErrorsTool.tsx index 5aea314c3ee7ad..74ef508044ec0f 100644 --- a/extensions/copilot/src/extension/tools/node/getErrorsTool.tsx +++ b/extensions/copilot/src/extension/tools/node/getErrorsTool.tsx @@ -18,7 +18,8 @@ import { coalesce } from '../../../util/vs/base/common/arrays'; import { CancellationToken } from '../../../util/vs/base/common/cancellation'; import { Disposable } from '../../../util/vs/base/common/lifecycle'; import { ResourceSet } from '../../../util/vs/base/common/map'; -import { extUriBiasedIgnorePathCase } from '../../../util/vs/base/common/resources'; +import { isWindows } from '../../../util/vs/base/common/platform'; +import { isEqual, isEqualOrParent } from '../../../util/vs/base/common/resources'; import { URI } from '../../../util/vs/base/common/uri'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { DiagnosticSeverity, ExtendedLanguageModelToolResult, LanguageModelPromptTsxPart, MarkdownString, Range } from '../../../vscodeTypes'; @@ -95,16 +96,15 @@ export class GetErrorsTool extends Disposable implements ICopilotTool= 3 && path[0] === '/' && path[2] === ':') { + const lowerDrive = path[1].toLowerCase(); + if (lowerDrive !== path[1]) { + return uri.with({ path: `/${lowerDrive}${path.slice(2)}` }); + } + } + return uri; + } + async invoke(options: vscode.LanguageModelToolInvocationOptions, token: CancellationToken) { const getAll = () => this.languageDiagnosticsService.getAllDiagnostics() .map(d => ({ uri: d[0], diagnostics: d[1].filter(e => e.severity <= DiagnosticSeverity.Warning), inputUri: undefined })) @@ -159,10 +201,6 @@ export class GetErrorsTool extends Disposable implements ICopilotTool { const uri = resolveToolInputPath(filePath, this.promptPathRepresentationService); const range = options.input.ranges?.[i]; - if (!uri) { - throw new Error(`Invalid input path ${filePath}`); - } - return { uri, range: range ? new Range(...range) : undefined }; })); diff --git a/extensions/copilot/src/extension/tools/node/test/getErrorsTool.spec.tsx b/extensions/copilot/src/extension/tools/node/test/getErrorsTool.spec.tsx index f95205b1f02313..a5846ffac16c65 100644 --- a/extensions/copilot/src/extension/tools/node/test/getErrorsTool.spec.tsx +++ b/extensions/copilot/src/extension/tools/node/test/getErrorsTool.spec.tsx @@ -296,3 +296,81 @@ suite('GetErrorsTool - Tool Invocation', () => { expect(msg).toMatchSnapshot(); }); }); + +suite('GetErrorsTool - normalizeDriveLetter', () => { + let accessor: ITestingServicesAccessor; + let collection: TestingServiceCollection; + let tool: GetErrorsTool; + + beforeEach(() => { + collection = createExtensionUnitTestingServices(); + const workspaceFolder = URI.file('/test/workspace'); + const tsDoc = createTextDocumentData(URI.file('/test/workspace/file.ts'), '', 'ts').document; + collection.define(IWorkspaceService, new SyncDescriptor(TestWorkspaceService, [[workspaceFolder], [tsDoc]])); + collection.define(ILanguageDiagnosticsService, new TestLanguageDiagnosticsService()); + const fileSystemService = new MockFileSystemService(); + collection.define(IFileSystemService, fileSystemService); + accessor = collection.createTestingAccessor(); + tool = accessor.get(IInstantiationService).createInstance(GetErrorsTool); + }); + + afterEach(() => { + accessor.dispose(); + }); + + test.skipIf(isWindows)('non-Windows: returns the same URI instance for any path shape', () => { + const uris = [ + URI.parse('file:///C:/foo/bar.ts'), + URI.parse('file:///c:/foo/bar.ts'), + URI.parse('file:///foo/bar.ts'), + URI.parse('file:///'), + ]; + for (const uri of uris) { + expect(tool.normalizeDriveLetter(uri)).toBe(uri); + } + }); + + test.skipIf(!isWindows)('Windows: lowercases an uppercase drive letter', () => { + const uri = URI.parse('file:///C:/foo/bar.ts'); + const result = tool.normalizeDriveLetter(uri); + expect(result.path).toBe('/c:/foo/bar.ts'); + }); + + test.skipIf(!isWindows)('Windows: returns same URI instance when drive letter is already lowercase', () => { + const uri = URI.parse('file:///c:/foo/bar.ts'); + expect(tool.normalizeDriveLetter(uri)).toBe(uri); + }); + + test.skipIf(!isWindows)('Windows: preserves case of path components after the drive letter', () => { + const uri = URI.parse('file:///C:/MyProject/SrcFile.ts'); + const result = tool.normalizeDriveLetter(uri); + expect(result.path).toBe('/c:/MyProject/SrcFile.ts'); + }); + + test.skipIf(!isWindows)('Windows: returns URI unchanged when path has no drive letter', () => { + const uri = URI.parse('file:///foo/bar.ts'); + expect(tool.normalizeDriveLetter(uri)).toBe(uri); + }); + + test.skipIf(!isWindows)('Windows: returns URI unchanged when path is too short to contain a drive letter', () => { + const uri = URI.parse('file:///c'); + expect(tool.normalizeDriveLetter(uri)).toBe(uri); + }); + + test.skipIf(!isWindows)('Windows: returns URI unchanged for root path', () => { + const uri = URI.parse('file:///'); + expect(tool.normalizeDriveLetter(uri)).toBe(uri); + }); + + test.skipIf(!isWindows)('Windows: normalizes drive letter when path ends right after the colon', () => { + const uri = URI.parse('file:///C:'); + const result = tool.normalizeDriveLetter(uri); + expect(result.path).toBe('/c:'); + }); + + test.skipIf(!isWindows)('Windows: returns URI unchanged when drive position holds a non-letter character', () => { + // digit at drive position — `toLowerCase()` is a no-op so the URI is returned as-is + const uri = URI.parse('file:///1:/foo/bar.ts'); + expect(tool.normalizeDriveLetter(uri)).toBe(uri); + }); +}); From cd65ddfe70d52a38eed9694438f5977023049b3c Mon Sep 17 00:00:00 2001 From: Wangmz-1211 Date: Sat, 6 Jun 2026 14:19:10 +0900 Subject: [PATCH 3/3] revert a mistaken deletion --- extensions/copilot/src/extension/tools/node/getErrorsTool.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extensions/copilot/src/extension/tools/node/getErrorsTool.tsx b/extensions/copilot/src/extension/tools/node/getErrorsTool.tsx index 74ef508044ec0f..55dfc9e7e71c36 100644 --- a/extensions/copilot/src/extension/tools/node/getErrorsTool.tsx +++ b/extensions/copilot/src/extension/tools/node/getErrorsTool.tsx @@ -201,6 +201,10 @@ export class GetErrorsTool extends Disposable implements ICopilotTool { const uri = resolveToolInputPath(filePath, this.promptPathRepresentationService); const range = options.input.ranges?.[i]; + if (!uri) { + throw new Error(`Invalid input path ${filePath}`); + } + return { uri, range: range ? new Range(...range) : undefined }; }));