diff --git a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts index ff8f6161c1b..71e72d052ec 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts @@ -30,8 +30,42 @@ const vscodeMock = vi.hoisted(() => { } } + class Position { + line: number; + character: number; + constructor(line: number, character: number) { + if (line < 0 || character < 0) { + throw new Error('Position line and character must be non-negative'); + } + this.line = line; + this.character = character; + } + } + + class Selection { + anchor: Position; + active: Position; + constructor(anchor: Position, active: Position) { + this.anchor = anchor; + this.active = active; + } + } + + class Range { + start: Position; + end: Position; + constructor(start: Position, end: Position) { + this.start = start; + this.end = end; + } + } + return { Uri, + Position, + Selection, + Range, + TextEditorRevealType: { InCenter: 2 }, ViewColumn: { One: 1, Two: 2, Three: 3, Beside: -2 }, workspace: { findFiles: vi.fn(), @@ -50,6 +84,7 @@ const vscodeMock = vi.hoisted(() => { window: { activeTextEditor: undefined, showTextDocument: vi.fn(), + showErrorMessage: vi.fn(), tabGroups: { all: [] as Array<{ tabs: Array<{ input: unknown }>; @@ -252,6 +287,42 @@ describe('FileMessageHandler', () => { ); }); + it('openFile clamps zero line and column values to the file start', async () => { + vscodeMock.workspace.workspaceFolders = [ + { uri: vscode.Uri.file('/workspace'), name: 'workspace', index: 0 }, + ]; + vscodeMock.workspace.openTextDocument.mockResolvedValue({ + uri: vscode.Uri.file('/workspace/src/app.ts'), + }); + const editor = { selection: undefined, revealRange: vi.fn() }; + vscodeMock.window.showTextDocument.mockResolvedValue(editor); + + const handler = new FileMessageHandler( + {} as QwenAgentManager, + {} as ConversationStore, + null, + vi.fn(), + ); + + await handler.handle({ + type: 'openFile', + data: { path: 'src/app.ts:0:0' }, + }); + + expect(vscodeMock.window.showErrorMessage).not.toHaveBeenCalled(); + expect(editor.selection).toMatchObject({ + anchor: { line: 0, character: 0 }, + active: { line: 0, character: 0 }, + }); + expect(editor.revealRange).toHaveBeenCalledWith( + expect.objectContaining({ + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, + }), + vscodeMock.TextEditorRevealType.InCenter, + ); + }); + describe('createAndOpenTempFile viewColumn selection', () => { const chatViewType = 'mainThreadWebview-qwenCode.chat'; diff --git a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts index 9a50804378b..57459eb5284 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts @@ -123,6 +123,13 @@ export class FileMessageHandler extends BaseMessageHandler { ); } + private toZeroBasedEditorPosition(value: string | undefined): number { + if (!value) { + return 0; + } + return Math.max(0, parseInt(value, 10) - 1); + } + private createWatcherForFolder(folder: vscode.WorkspaceFolder): void { const rootPath = folder.uri.fsPath; @@ -554,8 +561,8 @@ export class FileMessageHandler extends BaseMessageHandler { } const [, path, lineStr, columnStr] = match; - const lineNumber = lineStr ? parseInt(lineStr, 10) - 1 : 0; // VS Code uses 0-based line numbers - const columnNumber = columnStr ? parseInt(columnStr, 10) - 1 : 0; // VS Code uses 0-based column numbers + const lineNumber = this.toZeroBasedEditorPosition(lineStr); + const columnNumber = this.toZeroBasedEditorPosition(columnStr); // Convert to absolute path if relative let absolutePath = path;