Skip to content

Commit 8d1436d

Browse files
committed
Fix various things in the webview that don't work correctly in Agents window
- commits - copy links - open on .com
1 parent c0f7541 commit 8d1436d

10 files changed

Lines changed: 144 additions & 12 deletions

File tree

src/commands.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { PullRequestModel } from './github/pullRequestModel';
3333
import { PullRequestOverviewPanel } from './github/pullRequestOverview';
3434
import { chooseItem } from './github/quickPicks';
3535
import { RepositoriesManager } from './github/repositoriesManager';
36-
import { codespacesPrLink, getIssuesUrl, getPullsUrl, isInCodespaces, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, vscodeDevPrLink } from './github/utils';
36+
import { codespacesPrLink, getIssuesUrl, getPullsUrl, isInCodespaces, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, vscodeDevPrLink, vscodeDevPrLinkFromUrl } from './github/utils';
3737
import { BaseContext, OverviewContext } from './github/views';
3838
import { checkoutPRInWorktree } from './github/worktree';
3939
import { IssueChatContextItem } from './lm/issueContextProvider';
@@ -1915,6 +1915,9 @@ ${contents}
19151915

19161916
context.subscriptions.push(
19171917
vscode.commands.registerCommand('pr.copyVscodeDevPrLink', async (params: BaseContext | undefined) => {
1918+
if (params?.url) {
1919+
return vscode.env.clipboard.writeText(vscodeDevPrLinkFromUrl(params.url));
1920+
}
19181921
let pr: PullRequestModel | undefined;
19191922
if (params) {
19201923
pr = await reposManager.getManagerForRepository(params.owner, params.repo)?.resolvePullRequest(params.owner, params.repo, params.number, true);
@@ -1935,6 +1938,9 @@ ${contents}
19351938

19361939
context.subscriptions.push(
19371940
vscode.commands.registerCommand('pr.copyPrLink', async (params: BaseContext | undefined) => {
1941+
if (params?.url) {
1942+
return vscode.env.clipboard.writeText(params.url);
1943+
}
19381944
let item: PullRequestModel | IssueModel | undefined;
19391945
if (params) {
19401946
const folderManager = reposManager.getManagerForRepository(params.owner, params.repo);

src/extension.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import { CompareChanges } from './view/compareChangesTreeDataProvider';
4949
import { CreatePullRequestHelper } from './view/createPullRequestHelper';
5050
import { EmojiCompletionProvider } from './view/emojiCompletionProvider';
5151
import { FileTypeDecorationProvider } from './view/fileTypeDecorationProvider';
52-
import { GitHubCommitFileSystemProvider } from './view/githubFileContentProvider';
52+
import { getGitHubCommitFileSystemProvider } from './view/githubFileContentProvider';
5353
import { getInMemPRFileSystemProvider } from './view/inMemPRContentProvider';
5454
import { PullRequestChangesTreeDataProvider } from './view/prChangesTreeDataProvider';
5555
import { PullRequestsTreeDataProvider } from './view/prsTreeDataProvider';
@@ -495,7 +495,7 @@ async function deferredActivate(context: vscode.ExtensionContext, showPRControll
495495
const readOnlyMessage = new vscode.MarkdownString(vscode.l10n.t('Cannot edit this pull request file. [Check out](command:pr.checkoutFromReadonlyFile) this pull request to edit.'));
496496
readOnlyMessage.isTrusted = { enabledCommands: ['pr.checkoutFromReadonlyFile'] };
497497
context.subscriptions.push(vscode.workspace.registerFileSystemProvider(Schemes.Pr, inMemPRFileSystemProvider, { isReadonly: readOnlyMessage }));
498-
const githubFilesystemProvider = new GitHubCommitFileSystemProvider(reposManager, apiImpl, credentialStore);
498+
const githubFilesystemProvider = getGitHubCommitFileSystemProvider({ reposManager, gitAPI: apiImpl, credentialStore })!;
499499
context.subscriptions.push(vscode.workspace.registerFileSystemProvider(Schemes.GitHubCommit, githubFilesystemProvider, { isReadonly: new vscode.MarkdownString(vscode.l10n.t('GitHub commits cannot be edited')) }));
500500
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(Schemes.CheckRunLog, new CheckRunLogContentProvider(reposManager)));
501501

src/github/pullRequestOverview.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { toOpenIssueWebviewUri } from '../common/uri';
4444
import { asPromise, formatError } from '../common/utils';
4545
import { IRequestMessage, PULL_REQUEST_OVERVIEW_VIEW_TYPE } from '../common/webview';
4646
import { toCheckRunLogUri } from '../view/checkRunLogContentProvider';
47+
import { getGitHubCommitFileSystemProvider } from '../view/githubFileContentProvider';
4748

4849
export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestModel> {
4950
public static override ID: string = 'PullRequestOverviewPanel';
@@ -928,11 +929,17 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
928929
private async openCommitChanges(message: IRequestMessage<OpenCommitChangesArgs>): Promise<void> {
929930
try {
930931
const { commitSha } = message.args;
932+
const fileSystemProvider = getGitHubCommitFileSystemProvider();
933+
if (!fileSystemProvider) {
934+
throw new Error('GitHub commit file system provider is not initialized.');
935+
}
936+
fileSystemProvider.registerGitHubRepository(this._item.githubRepository);
931937
await PullRequestModel.openCommitChanges(this._extensionUri, this._item.githubRepository, commitSha);
932-
this._replyMessage(message, {});
938+
await this._replyMessage(message, {});
933939
} catch (error) {
934940
Logger.error(`Failed to open commit changes: ${formatError(error)}`, PullRequestOverviewPanel.ID);
935941
vscode.window.showErrorMessage(vscode.l10n.t('Failed to open commit changes: {0}', formatError(error)));
942+
await this._throwError(message, formatError(error));
936943
}
937944
}
938945

src/github/utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1974,7 +1974,11 @@ export async function findDotComAndEnterpriseRemotes(folderManagers: FolderRepos
19741974
}
19751975

19761976
export function vscodeDevPrLink(pullRequest: IssueModel) {
1977-
const itemUri = vscode.Uri.parse(pullRequest.html_url);
1977+
return vscodeDevPrLinkFromUrl(pullRequest.html_url);
1978+
}
1979+
1980+
export function vscodeDevPrLinkFromUrl(url: string) {
1981+
const itemUri = vscode.Uri.parse(url);
19781982
return `https://${vscode.env.appName.toLowerCase().includes('insider') ? 'insiders.' : ''}vscode.dev/github${itemUri.path}`;
19791983
}
19801984

src/github/views.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,8 @@ export interface BaseContext {
219219
owner: string;
220220
repo: string;
221221
number: number;
222-
[key: string]: boolean | string | number;
222+
url?: string;
223+
[key: string]: boolean | string | number | undefined;
223224
}
224225

225226
export interface OverviewContext extends BaseContext {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { default as assert } from 'assert';
7+
import { createSandbox, SinonSandbox } from 'sinon';
8+
import { GitApiImpl } from '../../api/api1';
9+
import { toGitHubCommitUri } from '../../common/uri';
10+
import { CredentialStore } from '../../github/credentials';
11+
import { GitHubRepository } from '../../github/githubRepository';
12+
import { RepositoriesManager } from '../../github/repositoriesManager';
13+
import { GitHubCommitFileSystemProvider } from '../../view/githubFileContentProvider';
14+
15+
describe('GitHubCommitFileSystemProvider', function () {
16+
let sandbox: SinonSandbox;
17+
18+
beforeEach(function () {
19+
sandbox = createSandbox();
20+
});
21+
22+
afterEach(function () {
23+
sandbox.restore();
24+
});
25+
26+
it('reads from a registered repository without a workspace repository manager', async function () {
27+
const credentialStore = {
28+
isAnyAuthenticated: () => true,
29+
} as unknown as CredentialStore;
30+
const provider = new GitHubCommitFileSystemProvider(
31+
{} as RepositoriesManager,
32+
{} as GitApiImpl,
33+
credentialStore,
34+
);
35+
const content = new TextEncoder().encode('content');
36+
const getFile = sandbox.stub().resolves(content);
37+
const repository = {
38+
remote: {
39+
owner: 'Owner',
40+
repositoryName: 'Repo',
41+
},
42+
getFile,
43+
} as unknown as GitHubRepository;
44+
provider.registerGitHubRepository(repository);
45+
46+
const result = await provider.readFile(toGitHubCommitUri('file.ts', {
47+
commit: 'commit',
48+
owner: 'owner',
49+
repo: 'repo',
50+
}));
51+
52+
assert.deepStrictEqual(result, content);
53+
assert.strictEqual(getFile.calledOnceWithExactly('/file.ts', 'commit'), true);
54+
});
55+
});

src/view/githubFileContentProvider.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,34 @@ import { RepositoryFileSystemProvider } from './repositoryFileSystemProvider';
88
import { GitApiImpl } from '../api/api1';
99
import { fromGitHubCommitUri } from '../common/uri';
1010
import { CredentialStore } from '../github/credentials';
11+
import { GitHubRepository } from '../github/githubRepository';
1112
import { RepositoriesManager } from '../github/repositoriesManager';
1213

1314
export class GitHubCommitFileSystemProvider extends RepositoryFileSystemProvider {
15+
private readonly _gitHubRepositories = new Map<string, GitHubRepository>();
16+
1417
constructor(private readonly repos: RepositoriesManager, gitAPI: GitApiImpl, credentialStore: CredentialStore) {
1518
super(gitAPI, credentialStore);
1619
}
1720

21+
registerGitHubRepository(repository: GitHubRepository): void {
22+
this._gitHubRepositories.set(this.repositoryKey(repository.remote.owner, repository.remote.repositoryName), repository);
23+
}
24+
1825
override async readFile(uri: vscode.Uri): Promise<Uint8Array> {
1926
await this.waitForAuth();
20-
await this.waitForAnyGitHubRepos(this.repos);
2127

2228
const params = fromGitHubCommitUri(uri);
2329
if (!params) {
2430
throw new Error(`Invalid GitHub commit URI: ${uri.toString()}`);
2531
}
2632

33+
const registeredRepository = this._gitHubRepositories.get(this.repositoryKey(params.owner, params.repo));
34+
if (registeredRepository) {
35+
return registeredRepository.getFile(uri.path, params.commit);
36+
}
37+
38+
await this.waitForAnyGitHubRepos(this.repos);
2739
const folderManager = this.repos.getManagerForRepository(params.owner, params.repo);
2840
if (!folderManager) {
2941
throw new Error(`Repository not found for owner: ${params.owner}, repo: ${params.repo}`);
@@ -36,4 +48,17 @@ export class GitHubCommitFileSystemProvider extends RepositoryFileSystemProvider
3648

3749
return githubRepo.getFile(uri.path, params.commit);
3850
}
51+
52+
private repositoryKey(owner: string, repo: string): string {
53+
return `${owner.toLowerCase()}/${repo.toLowerCase()}`;
54+
}
55+
}
56+
57+
let githubCommitFileSystemProvider: GitHubCommitFileSystemProvider | undefined;
58+
59+
export function getGitHubCommitFileSystemProvider(initialize?: { reposManager: RepositoriesManager, gitAPI: GitApiImpl, credentialStore: CredentialStore }): GitHubCommitFileSystemProvider | undefined {
60+
if (!githubCommitFileSystemProvider && initialize) {
61+
githubCommitFileSystemProvider = new GitHubCommitFileSystemProvider(initialize.reposManager, initialize.gitAPI, initialize.credentialStore);
62+
}
63+
return githubCommitFileSystemProvider;
3964
}

webviews/components/header.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ interface TitleProps {
8484
}
8585

8686
function Title({ title, titleHTML, number, url, inEditMode, setEditMode, setCurrentTitle, canEdit, owner, repo }: TitleProps): JSX.Element {
87-
const { setTitle, copyPrLink } = useContext(PullRequestContext);
87+
const { setTitle, copyPrLink, openOnGitHub } = useContext(PullRequestContext);
8888

8989
const titleForm = (
9090
<form
@@ -116,7 +116,8 @@ function Title({ title, titleHTML, number, url, inEditMode, setEditMode, setCurr
116116
'preventDefaultContextMenuItems': true,
117117
owner,
118118
repo,
119-
number
119+
number,
120+
url,
120121
};
121122
context['github:copyMenu'] = true;
122123

@@ -125,7 +126,15 @@ function Title({ title, titleHTML, number, url, inEditMode, setEditMode, setCurr
125126
<h2>
126127
<span dangerouslySetInnerHTML={{ __html: titleHTML }} />
127128
{' '}
128-
<a href={url} title={url} data-vscode-context={JSON.stringify(context)}>
129+
<a
130+
href={url}
131+
title={url}
132+
data-vscode-context={JSON.stringify(context)}
133+
onClick={event => {
134+
event.preventDefault();
135+
void openOnGitHub();
136+
}}
137+
>
129138
#{number}
130139
</a>
131140
</h2>

webviews/components/stickyHeader.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function useStickyHeader(titleRef: React.RefObject<HTMLDivElement | null>
3232

3333
export function StickyHeader({ pr, visible }: { pr: PullRequest; visible: boolean }): JSX.Element {
3434
const { text, color, icon } = getStatus(pr.state, !!pr.isDraft, pr.isIssue, pr.stateReason);
35-
const { copyPrLink } = React.useContext(PullRequestContext);
35+
const { copyPrLink, openOnGitHub } = React.useContext(PullRequestContext);
3636

3737
const stickyRef = React.useCallback((node: HTMLDivElement | null) => {
3838
if (node) {
@@ -64,6 +64,10 @@ export function StickyHeader({ pr, visible }: { pr: PullRequest; visible: boolea
6464
number: pr.number,
6565
'github:copyMenu': true,
6666
})}
67+
onClick={event => {
68+
event.preventDefault();
69+
void openOnGitHub();
70+
}}
6771
>
6872
#{pr.number}
6973
</a>

webviews/editorWebview/test/overview.test.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { default as assert } from 'assert';
77
import * as React from 'react';
8-
import { cleanup, render } from 'react-testing-library';
8+
import { cleanup, fireEvent, render } from 'react-testing-library';
99
import { createSandbox, SinonSandbox } from 'sinon';
1010

1111
import { PRContext, default as PullRequestContext } from '../../common/context';
@@ -38,6 +38,27 @@ describe('Overview', function () {
3838
assert(out.container.querySelector('.overview-title'));
3939
});
4040

41+
it('opens PR number links on GitHub', function () {
42+
const pr = new PullRequestBuilder().build();
43+
const context = new PRContext(pr);
44+
const openOnGitHub = sinon.stub(context, 'openOnGitHub');
45+
46+
const out = render(
47+
<PullRequestContext.Provider value={context}>
48+
<Overview {...pr} />
49+
</PullRequestContext.Provider>,
50+
);
51+
52+
const numberLinks = out.container.querySelectorAll('.overview-title a, .sticky-header-number');
53+
assert.strictEqual(numberLinks.length, 2);
54+
numberLinks.forEach(link => {
55+
const contextData = JSON.parse(link.getAttribute('data-vscode-context')!);
56+
assert.strictEqual(contextData.url, pr.url);
57+
fireEvent.click(link);
58+
});
59+
assert.strictEqual(openOnGitHub.callCount, 2);
60+
});
61+
4162
it('applies sticky class when scrolled', function () {
4263
const pr = new PullRequestBuilder().build();
4364
const context = new PRContext(pr);

0 commit comments

Comments
 (0)