Skip to content
26 changes: 10 additions & 16 deletions packages/vscode-ide-companion/src/diff-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import * as vscode from 'vscode';
import { DIFF_SCHEME } from './extension.js';
import {
findLeftGroupOfChatWebview,
ensureLeftGroupOfChatWebview,
findRightGroupOfChatWebview,
} from './utils/editorGroupUtils.js';

export class DiffContentProvider implements vscode.TextDocumentContentProvider {
Expand Down Expand Up @@ -222,28 +222,22 @@ export class DiffManager {
true,
);

// Prefer opening the diff adjacent to the chat webview (so we don't
// replace content inside the locked webview group). We try the group to
// the left of the chat webview first; if none exists we fall back to
// ViewColumn.Beside. With the chat locked in the leftmost group, this
// fallback opens diffs to the right of the chat.
let targetViewColumn = findLeftGroupOfChatWebview();
if (targetViewColumn === undefined) {
// If there is no left neighbor, create one to satisfy the requirement of
// opening diffs to the left of the chat webview.
targetViewColumn = await ensureLeftGroupOfChatWebview();
}
// Prefer opening the diff in the group to the left of the chat webview.
// When that isn't available (e.g. chat is in the leftmost group), try the
// group to the right so we reuse existing layout. Only fall back to
// ViewColumn.Beside when neither neighbor exists or the webview is missing.
const targetViewColumn =
findLeftGroupOfChatWebview() ??
findRightGroupOfChatWebview() ??
vscode.ViewColumn.Beside;

Comment thread
yiliang114 marked this conversation as resolved.
await vscode.commands.executeCommand(
'vscode.diff',
leftDocUri,
rightDocUri,
diffTitle,
{
// If a left-of-webview group was found, target it explicitly so the
// diff opens there while keeping focus on the webview. Otherwise, use
// the default "open to side" behavior.
viewColumn: targetViewColumn ?? vscode.ViewColumn.Beside,
viewColumn: targetViewColumn,
preview: false,
preserveFocus: true,
},
Expand Down
175 changes: 175 additions & 0 deletions packages/vscode-ide-companion/src/utils/editorGroupUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

import { beforeEach, describe, expect, it, vi } from 'vitest';

const chatViewType = 'mainThreadWebview-qwenCode.chat';

const vscodeMock = vi.hoisted(() => ({
ViewColumn: { One: 1, Two: 2, Three: 3, Four: 4 },
window: {
tabGroups: {
all: [] as Array<{ tabs: Array<{ input: unknown }>; viewColumn: number }>,
},
},
}));

vi.mock('vscode', () => vscodeMock);

import {
findLeftGroupOfChatWebview,
findRightGroupOfChatWebview,
} from './editorGroupUtils.js';

function chatTab() {
return { input: { viewType: chatViewType } };
}

function regularTab() {
return { input: { viewType: 'default' } };
}

describe('findLeftGroupOfChatWebview', () => {
beforeEach(() => {
vi.clearAllMocks();
vscodeMock.window.tabGroups.all = [];
});

it('returns the nearest left neighbor when chat webview has a group to its left', () => {
vscodeMock.window.tabGroups.all = [
{ tabs: [regularTab()], viewColumn: 1 },
{ tabs: [chatTab()], viewColumn: 2 },
{ tabs: [regularTab()], viewColumn: 3 },
];

expect(findLeftGroupOfChatWebview()).toBe(1);
});

it('returns the closest left neighbor when multiple left groups exist', () => {
vscodeMock.window.tabGroups.all = [
{ tabs: [regularTab()], viewColumn: 1 },
{ tabs: [regularTab()], viewColumn: 2 },
{ tabs: [chatTab()], viewColumn: 4 },
{ tabs: [regularTab()], viewColumn: 5 },
];

// closest left is group 2, not group 1
expect(findLeftGroupOfChatWebview()).toBe(2);
});

it('returns undefined when chat webview is in the leftmost group', () => {
vscodeMock.window.tabGroups.all = [
{ tabs: [chatTab()], viewColumn: 1 },
{ tabs: [regularTab()], viewColumn: 2 },
];

expect(findLeftGroupOfChatWebview()).toBeUndefined();
});

it('returns undefined when no chat webview is found', () => {
vscodeMock.window.tabGroups.all = [
{ tabs: [regularTab()], viewColumn: 1 },
{ tabs: [regularTab()], viewColumn: 2 },
];

expect(findLeftGroupOfChatWebview()).toBeUndefined();
});

it('returns undefined when there are no tab groups', () => {
vscodeMock.window.tabGroups.all = [];

expect(findLeftGroupOfChatWebview()).toBeUndefined();
});

it('returns undefined when tabGroups access throws', () => {
// make .all throw on access
Object.defineProperty(vscodeMock.window.tabGroups, 'all', {
get: () => {
throw new Error('unexpected error');
},
configurable: true,
});

expect(findLeftGroupOfChatWebview()).toBeUndefined();

// restore
Object.defineProperty(vscodeMock.window.tabGroups, 'all', {
value: [],
configurable: true,
writable: true,
});
});
});

describe('findRightGroupOfChatWebview', () => {
beforeEach(() => {
vi.clearAllMocks();
vscodeMock.window.tabGroups.all = [];
});

it('returns the nearest right neighbor when chat webview has a group to its right', () => {
vscodeMock.window.tabGroups.all = [
{ tabs: [regularTab()], viewColumn: 1 },
{ tabs: [chatTab()], viewColumn: 2 },
{ tabs: [regularTab()], viewColumn: 3 },
];

expect(findRightGroupOfChatWebview()).toBe(3);
});

it('returns the closest right neighbor when multiple right groups exist', () => {
vscodeMock.window.tabGroups.all = [
{ tabs: [regularTab()], viewColumn: 1 },
{ tabs: [chatTab()], viewColumn: 2 },
{ tabs: [regularTab()], viewColumn: 3 },
{ tabs: [regularTab()], viewColumn: 5 },
];

// closest right is group 3, not group 5
expect(findRightGroupOfChatWebview()).toBe(3);
});

it('returns undefined when chat webview is in the rightmost group', () => {
vscodeMock.window.tabGroups.all = [
{ tabs: [regularTab()], viewColumn: 1 },
{ tabs: [chatTab()], viewColumn: 3 },
];

expect(findRightGroupOfChatWebview()).toBeUndefined();
});

it('returns undefined when no chat webview is found', () => {
vscodeMock.window.tabGroups.all = [
{ tabs: [regularTab()], viewColumn: 1 },
{ tabs: [regularTab()], viewColumn: 2 },
];

expect(findRightGroupOfChatWebview()).toBeUndefined();
});

it('returns undefined when there are no tab groups', () => {
vscodeMock.window.tabGroups.all = [];

expect(findRightGroupOfChatWebview()).toBeUndefined();
});

it('returns undefined when tabGroups access throws', () => {
Object.defineProperty(vscodeMock.window.tabGroups, 'all', {
get: () => {
throw new Error('unexpected error');
},
configurable: true,
});

expect(findRightGroupOfChatWebview()).toBeUndefined();

Object.defineProperty(vscodeMock.window.tabGroups, 'all', {
value: [],
configurable: true,
writable: true,
});
});
});
Loading
Loading