Skip to content

Commit a9a864e

Browse files
authored
Sync Pull Requests view with currently open PR overview/description (#7123)
* Initial plan * Implement PR overview and tree view sync feature Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add comprehensive tests for PR overview sync functionality Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Change event nalme * Don't move selection if already correct and avoid unneeded refresh * Add sync when tree view becomes visible with active PR Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Delete tests * Delete tests * Logger, not console
1 parent 80d3eb9 commit a9a864e

6 files changed

Lines changed: 126 additions & 5 deletions

File tree

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- **Activation Events**: Only add new activation events if absolutely necessary.
1313
- **Webviews**: Place webview code in the `webviews/` directory. Use the shared `common/` code where possible.
1414
- **Commands**: Register new commands in `package.json` and implement them in `src/commands.ts` or a relevant module.
15+
- **Logging**: Use the `Logger` utility for all logging purposes. Don't use console.log or similar methods directly.
1516

1617
---
1718
_Last updated: 2025-06-20_

.vscode/launch.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"--extensionDevelopmentPath=${workspaceFolder}",
9393
"--extensionTestsPath=${workspaceFolder}/out/src/test",
9494
"--disable-extensions",
95+
"--user-data-dir=${workspaceFolder}/.vscode-test"
9596
],
9697
"preLaunchTask": "npm: test:preprocess",
9798
"smartStep": true,

src/github/pullRequestOverview.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
4545
*/
4646
public static override currentPanel?: PullRequestOverviewPanel;
4747

48+
/**
49+
* Event emitter for when a PR overview becomes active
50+
*/
51+
private static _onVisible = new vscode.EventEmitter<PullRequestModel>();
52+
public static readonly onVisible = PullRequestOverviewPanel._onVisible.event;
53+
4854
private _repositoryDefaultBranch: string;
4955
private _existingReviewers: ReviewState[] = [];
5056
private _teamsCount = 0;
@@ -100,6 +106,13 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
100106
}
101107
}
102108

109+
/**
110+
* Get the currently active pull request from the current panel
111+
*/
112+
public static getCurrentPullRequest(): PullRequestModel | undefined {
113+
return this.currentPanel?._item;
114+
}
115+
103116
protected constructor(
104117
telemetry: ITelemetry,
105118
extensionUri: vscode.Uri,
@@ -157,6 +170,11 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
157170
protected override onDidChangeViewState(e: vscode.WebviewPanelOnDidChangeViewStateEvent): void {
158171
super.onDidChangeViewState(e);
159172
this.setVisibilityContext();
173+
174+
// If the panel becomes visible and we have an item, notify that this PR is active
175+
if (this._panel.visible && this._item) {
176+
PullRequestOverviewPanel._onVisible.fire(this._item);
177+
}
160178
}
161179

162180
private setVisibilityContext() {
@@ -308,7 +326,12 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
308326
this._panel.webview.html = this.getHtmlForWebview();
309327
}
310328

311-
return vscode.window.withProgress({ location: { viewId: 'pr:github' } }, () => this.updateItem(pullRequestModel));
329+
const result = vscode.window.withProgress({ location: { viewId: 'pr:github' } }, () => this.updateItem(pullRequestModel));
330+
331+
// Notify that this PR overview is now active
332+
PullRequestOverviewPanel._onVisible.fire(pullRequestModel);
333+
334+
return result;
312335
}
313336

314337
protected override async _onDidReceiveMessage(message: IRequestMessage<any>) {
@@ -803,6 +826,13 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
803826
super.dispose();
804827
disposeAll(this._prListeners);
805828
}
829+
830+
/**
831+
* Static dispose method to clean up static resources
832+
*/
833+
public static dispose() {
834+
PullRequestOverviewPanel._onVisible.dispose();
835+
}
806836
}
807837

808838
export function getDefaultMergeMethod(

src/view/prsTreeDataProvider.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { PRType } from '../github/interface';
1818
import { issueMarkdown } from '../github/markdownUtils';
1919
import { NotificationProvider } from '../github/notifications';
2020
import { PullRequestModel } from '../github/pullRequestModel';
21+
import { PullRequestOverviewPanel } from '../github/pullRequestOverview';
2122
import { RepositoriesManager } from '../github/repositoriesManager';
2223
import { findDotComAndEnterpriseRemotes } from '../github/utils';
2324
import { PRStatusDecorationProvider } from './prStatusDecorationProvider';
@@ -29,6 +30,7 @@ import { PRNode } from './treeNodes/pullRequestNode';
2930
import { BaseTreeNode, TreeNode } from './treeNodes/treeNode';
3031
import { TreeUtils } from './treeNodes/treeUtils';
3132
import { WorkspaceFolderNode } from './treeNodes/workspaceFolderNode';
33+
import Logger from '../common/logger';
3234

3335
export class PullRequestsTreeDataProvider extends Disposable implements vscode.TreeDataProvider<TreeNode>, BaseTreeNode {
3436
private _onDidChangeTreeData = new vscode.EventEmitter<TreeNode | void>();
@@ -81,6 +83,12 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T
8183
this._view.badge = undefined;
8284
this._notificationClearTimeout = undefined;
8385
}, 5000);
86+
87+
// Sync with currently active PR when view becomes visible
88+
const currentPR = PullRequestOverviewPanel.getCurrentPullRequest();
89+
if (currentPR) {
90+
this.syncWithActivePullRequest(currentPR);
91+
}
8492
}
8593
}));
8694

@@ -107,6 +115,14 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T
107115

108116
this._register(this._copilotManager.onDidCreatePullRequest(() => this.refresh()));
109117

118+
// Listen for PR overview panel changes to sync the tree view
119+
this._register(PullRequestOverviewPanel.onVisible(pullRequest => {
120+
// Only sync if view is already visible (don't open the view)
121+
if (this._view.visible) {
122+
this.syncWithActivePullRequest(pullRequest);
123+
}
124+
}));
125+
110126
this._children = [];
111127

112128
this._register(vscode.commands.registerCommand('pr.configurePRViewlet', async () => {
@@ -174,6 +190,73 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T
174190
return this._view.reveal(element, options);
175191
}
176192

193+
/**
194+
* Sync the tree view with the currently active PR overview
195+
*/
196+
private async syncWithActivePullRequest(pullRequest: PullRequestModel): Promise<void> {
197+
const alreadySelected = this._view.selection.find(child => child instanceof PRNode && (child.pullRequestModel.number === pullRequest.number) && (child.pullRequestModel.remote.owner === pullRequest.remote.owner) && (child.pullRequestModel.remote.repositoryName === pullRequest.remote.repositoryName));
198+
if (alreadySelected) {
199+
return;
200+
}
201+
try {
202+
// Find the PR node in the tree and reveal it
203+
const prNode = await this.findPRNode(pullRequest);
204+
if (prNode) {
205+
await this.reveal(prNode, { select: true, focus: false, expand: false });
206+
}
207+
} catch (error) {
208+
// Silently ignore errors to avoid disrupting the user experience
209+
Logger.warn(`Failed to sync tree view with active PR: ${error}`);
210+
}
211+
}
212+
213+
/**
214+
* Find a PR node in the tree structure
215+
*/
216+
private async findPRNode(pullRequest: PullRequestModel): Promise<PRNode | undefined> {
217+
if (this._children.length === 0) {
218+
await this.getChildren();
219+
}
220+
221+
for (const child of this._children) {
222+
if (child instanceof WorkspaceFolderNode) {
223+
const found = await this.findPRNodeInWorkspaceFolder(child, pullRequest);
224+
if (found) return found;
225+
} else if (child instanceof CategoryTreeNode) {
226+
const found = await this.findPRNodeInCategory(child, pullRequest);
227+
if (found) return found;
228+
}
229+
}
230+
return undefined;
231+
}
232+
233+
/**
234+
* Search for PR node within a workspace folder node
235+
*/
236+
private async findPRNodeInWorkspaceFolder(workspaceNode: WorkspaceFolderNode, pullRequest: PullRequestModel): Promise<PRNode | undefined> {
237+
const children = await workspaceNode.getChildren(false);
238+
for (const child of children) {
239+
if (child instanceof CategoryTreeNode) {
240+
const found = await this.findPRNodeInCategory(child, pullRequest);
241+
if (found) return found;
242+
}
243+
}
244+
return undefined;
245+
}
246+
247+
/**
248+
* Search for PR node within a category node
249+
*/
250+
private async findPRNodeInCategory(categoryNode: CategoryTreeNode, pullRequest: PullRequestModel): Promise<PRNode | undefined> {
251+
const children = await categoryNode.getChildren(false);
252+
for (const child of children) {
253+
if (child instanceof PRNode && (child.pullRequestModel.number === pullRequest.number) && (child.pullRequestModel.remote.owner === pullRequest.remote.owner) && (child.pullRequestModel.remote.repositoryName === pullRequest.remote.repositoryName)) {
254+
return child;
255+
}
256+
}
257+
return undefined;
258+
}
259+
177260
initialize(reviewModels: ReviewModel[], credentialStore: CredentialStore) {
178261
if (this._initialized) {
179262
throw new Error('Tree has already been initialized!');

src/view/treeNodes/categoryNode.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,11 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem {
207207
return false;
208208
}
209209

210-
override async getChildren(): Promise<TreeNode[]> {
211-
await super.getChildren();
210+
override async getChildren(shouldDispose: boolean = true): Promise<TreeNode[]> {
211+
await super.getChildren(shouldDispose);
212+
if (!shouldDispose && this.children) {
213+
return this.children;
214+
}
212215
const isFirstLoad = !this._firstLoad;
213216
if (isFirstLoad) {
214217
this._firstLoad = this.doGetChildren();

src/view/treeNodes/workspaceFolderNode.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,11 @@ export class WorkspaceFolderNode extends TreeNode implements vscode.TreeItem {
6363
return this;
6464
}
6565

66-
override async getChildren(): Promise<TreeNode[]> {
67-
super.getChildren();
66+
override async getChildren(shouldDispose: boolean = true): Promise<TreeNode[]> {
67+
super.getChildren(shouldDispose);
68+
if (!shouldDispose && this.children) {
69+
return this.children;
70+
}
6871
this.children = await WorkspaceFolderNode.getCategoryTreeNodes(this.folderManager, this.telemetry, this, this.notificationProvider, this.context, this._prsTreeModel, this._copilotMananger);
6972
return this.children;
7073
}

0 commit comments

Comments
 (0)