Path: blob/main/extensions/copilot/test/simulation/fixtures/ghpr/commands.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4'use strict';56import * as pathLib from 'path';7import * as vscode from 'vscode';8import { Repository } from './api/api';9import { GitErrorCodes } from './api/api1';10import { CommentReply, resolveCommentHandler } from './commentHandlerResolver';11import { IComment } from './common/comment';12import Logger from './common/logger';13import { ITelemetry } from './common/telemetry';14import { Schemes, asImageDataURI, fromReviewUri } from './common/uri';15import { formatError } from './common/utils';16import { EXTENSION_ID } from './constants';17import { FolderRepositoryManager } from './github/folderRepositoryManager';18import { GitHubRepository } from './github/githubRepository';19import { PullRequest } from './github/interface';20import { NotificationProvider } from './github/notifications';21import { GHPRComment, GHPRCommentThread, TemporaryComment } from './github/prComment';22import { PullRequestModel } from './github/pullRequestModel';23import { PullRequestOverviewPanel } from './github/pullRequestOverview';24import { RepositoriesManager } from './github/repositoriesManager';25import { getIssuesUrl, getPullsUrl, isInCodespaces, vscodeDevPrLink } from './github/utils';26import { PullRequestsTreeDataProvider } from './view/prsTreeDataProvider';27import { ReviewCommentController } from './view/reviewCommentController';28import { ReviewManager } from './view/reviewManager';29import { CategoryTreeNode } from './view/treeNodes/categoryNode';30import { CommitNode } from './view/treeNodes/commitNode';31import { DescriptionNode } from './view/treeNodes/descriptionNode';32import {33FileChangeNode,34GitFileChangeNode,35InMemFileChangeNode,36RemoteFileChangeNode,37openFileCommand,38} from './view/treeNodes/fileChangeNode';39import { PRNode } from './view/treeNodes/pullRequestNode';4041const _onDidUpdatePR = new vscode.EventEmitter<PullRequest | void>();42export const onDidUpdatePR: vscode.Event<PullRequest | void> = _onDidUpdatePR.event;4344function ensurePR(folderRepoManager: FolderRepositoryManager, pr?: PRNode | PullRequestModel): PullRequestModel {45// If the command is called from the command palette, no arguments are passed.46if (!pr) {47if (!folderRepoManager.activePullRequest) {48vscode.window.showErrorMessage(vscode.l10n.t('Unable to find current pull request.'));49throw new Error('Unable to find current pull request.');50}5152return folderRepoManager.activePullRequest;53} else {54return pr instanceof PRNode ? pr.pullRequestModel : pr;55}56}5758export async function openDescription(59context: vscode.ExtensionContext,60telemetry: ITelemetry,61pullRequestModel: PullRequestModel,62descriptionNode: DescriptionNode | undefined,63folderManager: FolderRepositoryManager,64notificationProvider?: NotificationProvider65) {66const pullRequest = ensurePR(folderManager, pullRequestModel);67descriptionNode?.reveal(descriptionNode, { select: true, focus: true });68// Create and show a new webview69await PullRequestOverviewPanel.createOrShow(context.extensionUri, folderManager, pullRequest);7071if (notificationProvider?.hasNotification(pullRequest)) {72notificationProvider.markPrNotificationsAsRead(pullRequest);73}74}7576async function chooseItem<T>(77activePullRequests: T[],78propertyGetter: (itemValue: T) => string,79options?: vscode.QuickPickOptions,80): Promise<T | undefined> {81if (activePullRequests.length === 1) {82return activePullRequests[0];83}84interface Item extends vscode.QuickPickItem {85itemValue: T;86}87const items: Item[] = activePullRequests.map(currentItem => {88return {89label: propertyGetter(currentItem),90itemValue: currentItem,91};92});93return (await vscode.window.showQuickPick(items, options))?.itemValue;94}9596export async function openPullRequestOnGitHub(e: PRNode | DescriptionNode | PullRequestModel, telemetry: ITelemetry) {97if (e instanceof PRNode || e instanceof DescriptionNode) {98vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(e.pullRequestModel.html_url));99} else {100vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(e.html_url));101}102}103104export function registerCommands(105context: vscode.ExtensionContext,106reposManager: RepositoriesManager,107reviewManagers: ReviewManager[],108telemetry: ITelemetry,109tree: PullRequestsTreeDataProvider110) {111context.subscriptions.push(112vscode.commands.registerCommand(113'pr.openPullRequestOnGitHub',114async (e: PRNode | DescriptionNode | PullRequestModel | undefined) => {115if (!e) {116const activePullRequests: PullRequestModel[] = reposManager.folderManagers117.map(folderManager => folderManager.activePullRequest!)118.filter(activePR => !!activePR);119120if (activePullRequests.length >= 1) {121const result = await chooseItem<PullRequestModel>(122activePullRequests,123itemValue => itemValue.html_url,124);125if (result) {126openPullRequestOnGitHub(result, telemetry);127}128}129} else {130openPullRequestOnGitHub(e, telemetry);131}132},133),134);135136context.subscriptions.push(137vscode.commands.registerCommand(138'pr.openAllDiffs',139async () => {140const activePullRequestsWithFolderManager = reposManager.folderManagers141.filter(folderManager => folderManager.activePullRequest)142.map(folderManager => {143return (({ activePr: folderManager.activePullRequest!, folderManager }));144});145146const activePullRequestAndFolderManager = activePullRequestsWithFolderManager.length >= 1147? (148await chooseItem(149activePullRequestsWithFolderManager,150itemValue => itemValue.activePr.html_url,151)152)153: activePullRequestsWithFolderManager[0];154155if (!activePullRequestAndFolderManager) {156return;157}158159const { folderManager } = activePullRequestAndFolderManager;160const reviewManager = ReviewManager.getReviewManagerForFolderManager(reviewManagers, folderManager);161162if (!reviewManager) {163return;164}165166reviewManager.reviewModel.localFileChanges167.forEach(localFileChange => localFileChange.openDiff(folderManager, { preview: false }));168}169),170);171172context.subscriptions.push(173vscode.commands.registerCommand('review.suggestDiff', async e => {174try {175const folderManager = await chooseItem<FolderRepositoryManager>(176reposManager.folderManagers,177itemValue => pathLib.basename(itemValue.repository.rootUri.fsPath),178);179if (!folderManager || !folderManager.activePullRequest) {180return;181}182183const { indexChanges, workingTreeChanges } = folderManager.repository.state;184185if (!indexChanges.length) {186if (workingTreeChanges.length) {187const yes = vscode.l10n.t('Yes');188const stageAll = await vscode.window.showWarningMessage(189vscode.l10n.t('There are no staged changes to suggest.\n\nWould you like to automatically stage all your of changes and suggest them?'),190{ modal: true },191yes,192);193if (stageAll === yes) {194await vscode.commands.executeCommand('git.stageAll');195} else {196return;197}198} else {199vscode.window.showInformationMessage(vscode.l10n.t('There are no changes to suggest.'));200return;201}202}203204const diff = await folderManager.repository.diff(true);205206let suggestEditMessage = vscode.l10n.t('Suggested edit:\n');207if (e && e.inputBox && e.inputBox.value) {208suggestEditMessage = `${e.inputBox.value}\n`;209e.inputBox.value = '';210}211212const suggestEditText = `${suggestEditMessage}\`\`\`diff\n${diff}\n\`\`\``;213await folderManager.activePullRequest.createIssueComment(suggestEditText);214215// Reset HEAD and then apply reverse diff216await vscode.commands.executeCommand('git.unstageAll');217218const tempFilePath = pathLib.join(219folderManager.repository.rootUri.fsPath,220'.git',221`${folderManager.activePullRequest.number}.diff`,222);223const encoder = new TextEncoder();224const tempUri = vscode.Uri.file(tempFilePath);225226await vscode.workspace.fs.writeFile(tempUri, encoder.encode(diff));227await folderManager.repository.apply(tempFilePath, true);228await vscode.workspace.fs.delete(tempUri);229} catch (err) {230const moreError = `${err}${err.stderr ? `\n${err.stderr}` : ''}`;231Logger.error(`Applying patch failed: ${moreError}`);232vscode.window.showErrorMessage(vscode.l10n.t('Applying patch failed: {0}', formatError(err)));233}234}),235);236237context.subscriptions.push(238vscode.commands.registerCommand('pr.openFileOnGitHub', async (e: GitFileChangeNode | RemoteFileChangeNode) => {239if (e instanceof RemoteFileChangeNode) {240const choice = await vscode.window.showInformationMessage(241vscode.l10n.t('{0} can\'t be opened locally. Do you want to open it on GitHub?', e.changeModel.fileName),242vscode.l10n.t('Open'),243);244if (!choice) {245return;246}247}248if (e.changeModel.blobUrl) {249return vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(e.changeModel.blobUrl));250}251}),252);253254context.subscriptions.push(255vscode.commands.registerCommand('pr.copyCommitHash', (e: CommitNode) => {256vscode.env.clipboard.writeText(e.sha);257}),258);259260context.subscriptions.push(261vscode.commands.registerCommand('pr.openOriginalFile', async (e: GitFileChangeNode) => {262// if this is an image, encode it as a base64 data URI263const folderManager = reposManager.getManagerForIssueModel(e.pullRequest);264if (folderManager) {265const imageDataURI = await asImageDataURI(e.changeModel.parentFilePath, folderManager.repository);266vscode.commands.executeCommand('vscode.open', imageDataURI || e.changeModel.parentFilePath);267}268}),269);270271context.subscriptions.push(272vscode.commands.registerCommand('pr.openModifiedFile', (e: GitFileChangeNode | undefined) => {273let uri: vscode.Uri | undefined;274const tab = vscode.window.tabGroups.activeTabGroup.activeTab;275276if (e) {277uri = e.changeModel.filePath;278} else {279if (tab?.input instanceof vscode.TabInputTextDiff) {280uri = tab.input.modified;281}282}283if (uri) {284vscode.commands.executeCommand('vscode.open', uri, tab?.group.viewColumn);285}286}),287);288289async function openDiffView(fileChangeNode: GitFileChangeNode | InMemFileChangeNode | vscode.Uri | undefined) {290if (fileChangeNode && !(fileChangeNode instanceof vscode.Uri)) {291const folderManager = reposManager.getManagerForIssueModel(fileChangeNode.pullRequest);292if (!folderManager) {293return;294}295return fileChangeNode.openDiff(folderManager);296} else if (fileChangeNode || vscode.window.activeTextEditor) {297const editor = fileChangeNode ? vscode.window.visibleTextEditors.find(editor => editor.document.uri.toString() === fileChangeNode.toString())! : vscode.window.activeTextEditor!;298const visibleRanges = editor.visibleRanges;299const folderManager = reposManager.getManagerForFile(editor.document.uri);300if (!folderManager?.activePullRequest) {301return;302}303const reviewManager = ReviewManager.getReviewManagerForFolderManager(reviewManagers, folderManager);304if (!reviewManager) {305return;306}307const change = reviewManager.reviewModel.localFileChanges.find(change => change.resourceUri.with({ query: '' }).toString() === editor.document.uri.toString());308await change?.openDiff(folderManager);309const tabInput = vscode.window.tabGroups.activeTabGroup.activeTab?.input;310const diffEditor = (tabInput instanceof vscode.TabInputTextDiff && tabInput.modified.toString() === editor.document.uri.toString()) ? vscode.window.activeTextEditor : undefined;311if (diffEditor) {312diffEditor.revealRange(visibleRanges[0]);313}314}315}316317context.subscriptions.push(318vscode.commands.registerCommand(319'pr.openDiffView',320(fileChangeNode: GitFileChangeNode | InMemFileChangeNode | undefined) => {321return openDiffView(fileChangeNode);322},323),324);325326context.subscriptions.push(327vscode.commands.registerCommand(328'pr.openDiffViewFromEditor',329(uri: vscode.Uri) => {330return openDiffView(uri);331},332),333);334335context.subscriptions.push(336vscode.commands.registerCommand('pr.deleteLocalBranch', async (e: PRNode) => {337const folderManager = reposManager.getManagerForIssueModel(e.pullRequestModel);338if (!folderManager) {339return;340}341const pullRequestModel = ensurePR(folderManager, e);342const DELETE_BRANCH_FORCE = 'Delete Unmerged Branch';343let error = null;344345try {346await folderManager.deleteLocalPullRequest(pullRequestModel);347} catch (e) {348if (e.gitErrorCode === GitErrorCodes.BranchNotFullyMerged) {349const action = await vscode.window.showErrorMessage(350vscode.l10n.t('The local branch \'{0}\' is not fully merged. Are you sure you want to delete it?', pullRequestModel.localBranchName ?? 'unknown branch'),351DELETE_BRANCH_FORCE,352);353354if (action !== DELETE_BRANCH_FORCE) {355return;356}357358try {359await folderManager.deleteLocalPullRequest(pullRequestModel, true);360} catch (e) {361error = e;362}363} else {364error = e;365}366}367368if (error) {369await vscode.window.showErrorMessage(`Deleting local pull request branch failed: ${error}`);370} else {371// fire and forget372vscode.commands.executeCommand('pr.refreshList');373}374}),375);376377function chooseReviewManager(repoPath?: string) {378if (repoPath) {379const uri = vscode.Uri.file(repoPath).toString();380for (const mgr of reviewManagers) {381if (mgr.repository.rootUri.toString() === uri) {382return mgr;383}384}385}386return chooseItem<ReviewManager>(387reviewManagers,388itemValue => pathLib.basename(itemValue.repository.rootUri.fsPath),389{ placeHolder: vscode.l10n.t('Choose a repository to create a pull request in'), ignoreFocusOut: true },390);391}392393function isSourceControl(x: any): x is Repository {394return !!x?.rootUri;395}396397context.subscriptions.push(398vscode.commands.registerCommand(399'pr.create',400async (args?: { repoPath: string; compareBranch: string } | Repository) => {401// The arguments this is called with are either from the SCM view, or manually passed.402if (isSourceControl(args)) {403(await chooseReviewManager(args.rootUri.fsPath))?.createPullRequest();404} else {405(await chooseReviewManager(args?.repoPath))?.createPullRequest(args?.compareBranch);406}407},408),409);410411context.subscriptions.push(412vscode.commands.registerCommand(413'pr.pushAndCreate',414async (args?: any | Repository) => {415if (isSourceControl(args)) {416const reviewManager = await chooseReviewManager(args.rootUri.fsPath);417if (reviewManager) {418if (args.state.HEAD?.upstream) {419await args.push();420}421reviewManager.createPullRequest();422}423}424},425),426);427428context.subscriptions.push(429vscode.commands.registerCommand('pr.pick', async (pr: PRNode | DescriptionNode | PullRequestModel) => {430if (pr === undefined) {431// This is unexpected, but has happened a few times.432Logger.error('Unexpectedly received undefined when picking a PR.');433return vscode.window.showErrorMessage(vscode.l10n.t('No pull request was selected to checkout, please try again.'));434}435436let pullRequestModel: PullRequestModel;437let repository: Repository | undefined;438439if (pr instanceof PRNode || pr instanceof DescriptionNode) {440pullRequestModel = pr.pullRequestModel;441repository = pr.repository;442} else {443pullRequestModel = pr;444}445446const fromDescriptionPage = pr instanceof PullRequestModel;447448return vscode.window.withProgress(449{450location: vscode.ProgressLocation.SourceControl,451title: vscode.l10n.t('Switching to Pull Request #{0}', pullRequestModel.number),452},453async () => {454await ReviewManager.getReviewManagerForRepository(455reviewManagers,456pullRequestModel.githubRepository,457repository458)?.switch(pullRequestModel);459},460);461}),462);463464context.subscriptions.push(465vscode.commands.registerCommand('pr.pickOnVscodeDev', async (pr: PRNode | DescriptionNode | PullRequestModel) => {466if (pr === undefined) {467// This is unexpected, but has happened a few times.468Logger.error('Unexpectedly received undefined when picking a PR.');469return vscode.window.showErrorMessage(vscode.l10n.t('No pull request was selected to checkout, please try again.'));470}471472let pullRequestModel: PullRequestModel;473474if (pr instanceof PRNode || pr instanceof DescriptionNode) {475pullRequestModel = pr.pullRequestModel;476} else {477pullRequestModel = pr;478}479480return vscode.env.openExternal(vscode.Uri.parse(vscodeDevPrLink(pullRequestModel)));481}),482);483484context.subscriptions.push(485vscode.commands.registerCommand('pr.exit', async (pr: PRNode | DescriptionNode | PullRequestModel | undefined) => {486let pullRequestModel: PullRequestModel | undefined;487488if (pr instanceof PRNode || pr instanceof DescriptionNode) {489pullRequestModel = pr.pullRequestModel;490} else if (pr === undefined) {491pullRequestModel = await chooseItem<PullRequestModel>(reposManager.folderManagers492.map(folderManager => folderManager.activePullRequest!)493.filter(activePR => !!activePR),494itemValue => `${itemValue.number}: ${itemValue.title}`,495{ placeHolder: vscode.l10n.t('Choose the pull request to exit') });496} else {497pullRequestModel = pr;498}499500if (!pullRequestModel) {501return;502}503504const fromDescriptionPage = pr instanceof PullRequestModel;505506return vscode.window.withProgress(507{508location: vscode.ProgressLocation.SourceControl,509title: vscode.l10n.t('Exiting Pull Request'),510},511async () => {512const branch = await pullRequestModel!.githubRepository.getDefaultBranch();513const manager = reposManager.getManagerForIssueModel(pullRequestModel);514if (manager) {515const prBranch = manager.repository.state.HEAD?.name;516await manager.checkoutDefaultBranch(branch);517if (prBranch) {518await manager.cleanupAfterPullRequest(prBranch, pullRequestModel!);519}520}521},522);523}),524);525526context.subscriptions.push(527vscode.commands.registerCommand('pr.merge', async (pr?: PRNode) => {528const folderManager = reposManager.getManagerForIssueModel(pr?.pullRequestModel);529if (!folderManager) {530return;531}532const pullRequest = ensurePR(folderManager, pr);533// TODO check is codespaces534535const isCrossRepository =536pullRequest.base &&537pullRequest.head &&538!pullRequest.base.repositoryCloneUrl.equals(pullRequest.head.repositoryCloneUrl);539540const showMergeOnGitHub = isCrossRepository && isInCodespaces();541if (showMergeOnGitHub) {542return openPullRequestOnGitHub(pullRequest, telemetry);543}544545const yes = vscode.l10n.t('Yes');546return vscode.window547.showWarningMessage(548vscode.l10n.t('Are you sure you want to merge this pull request on GitHub?'),549{ modal: true },550yes,551)552.then(async value => {553let newPR;554if (value === yes) {555try {556newPR = await folderManager.mergePullRequest(pullRequest);557return newPR;558} catch (e) {559vscode.window.showErrorMessage(`Unable to merge pull request. ${formatError(e)}`);560return newPR;561}562}563});564}),565);566567context.subscriptions.push(568vscode.commands.registerCommand('pr.readyForReview', async (pr?: PRNode) => {569const folderManager = reposManager.getManagerForIssueModel(pr?.pullRequestModel);570if (!folderManager) {571return;572}573const pullRequest = ensurePR(folderManager, pr);574const yes = vscode.l10n.t('Yes');575return vscode.window576.showWarningMessage(577vscode.l10n.t('Are you sure you want to mark this pull request as ready to review on GitHub?'),578{ modal: true },579yes,580)581.then(async value => {582let isDraft;583if (value === yes) {584try {585isDraft = await pullRequest.setReadyForReview();586vscode.commands.executeCommand('pr.refreshList');587return isDraft;588} catch (e) {589vscode.window.showErrorMessage(590`Unable to mark pull request as ready to review. ${formatError(e)}`,591);592return isDraft;593}594}595});596}),597);598599context.subscriptions.push(600vscode.commands.registerCommand('pr.close', async (pr?: PRNode | PullRequestModel, message?: string) => {601let pullRequestModel: PullRequestModel | undefined;602if (pr) {603pullRequestModel = pr instanceof PullRequestModel ? pr : pr.pullRequestModel;604} else {605const activePullRequests: PullRequestModel[] = reposManager.folderManagers606.map(folderManager => folderManager.activePullRequest!)607.filter(activePR => !!activePR);608pullRequestModel = await chooseItem<PullRequestModel>(609activePullRequests,610itemValue => `${itemValue.number}: ${itemValue.title}`,611{ placeHolder: vscode.l10n.t('Pull request to close') },612);613}614if (!pullRequestModel) {615return;616}617const pullRequest: PullRequestModel = pullRequestModel;618const yes = vscode.l10n.t('Yes');619return vscode.window620.showWarningMessage(621vscode.l10n.t('Are you sure you want to close this pull request on GitHub? This will close the pull request without merging.'),622{ modal: true },623yes,624vscode.l10n.t('No'),625)626.then(async value => {627if (value === yes) {628try {629let newComment: IComment | undefined = undefined;630if (message) {631newComment = await pullRequest.createIssueComment(message);632}633634const newPR = await pullRequest.close();635vscode.commands.executeCommand('pr.refreshList');636_onDidUpdatePR.fire(newPR);637return newComment;638} catch (e) {639vscode.window.showErrorMessage(`Unable to close pull request. ${formatError(e)}`);640_onDidUpdatePR.fire();641}642}643644_onDidUpdatePR.fire();645});646}),647);648649context.subscriptions.push(650vscode.commands.registerCommand('pr.dismissNotification', node => {651if (node instanceof PRNode) {652tree.notificationProvider.markPrNotificationsAsRead(node.pullRequestModel).then(653() => tree.refresh(node)654);655656}657}),658);659660context.subscriptions.push(661vscode.commands.registerCommand(662'pr.openDescription',663async (argument: DescriptionNode | PullRequestModel | undefined) => {664let pullRequestModel: PullRequestModel | undefined;665if (!argument) {666const activePullRequests: PullRequestModel[] = reposManager.folderManagers667.map(manager => manager.activePullRequest!)668.filter(activePR => !!activePR);669if (activePullRequests.length >= 1) {670pullRequestModel = await chooseItem<PullRequestModel>(671activePullRequests,672itemValue => itemValue.title,673);674}675} else {676pullRequestModel = argument instanceof DescriptionNode ? argument.pullRequestModel : argument;677}678679if (!pullRequestModel) {680Logger.appendLine('No pull request found.');681return;682}683684const folderManager = reposManager.getManagerForIssueModel(pullRequestModel);685if (!folderManager) {686return;687}688689let descriptionNode: DescriptionNode | undefined;690if (argument instanceof DescriptionNode) {691descriptionNode = argument;692} else {693const reviewManager = ReviewManager.getReviewManagerForFolderManager(reviewManagers, folderManager);694if (!reviewManager) {695return;696}697698descriptionNode = reviewManager.changesInPrDataProvider.getDescriptionNode(folderManager);699}700701await openDescription(context, telemetry, pullRequestModel, descriptionNode, folderManager, tree.notificationProvider);702},703),704);705706context.subscriptions.push(707vscode.commands.registerCommand('pr.refreshDescription', async () => {708if (PullRequestOverviewPanel.currentPanel) {709PullRequestOverviewPanel.refresh();710}711}),712);713714context.subscriptions.push(715vscode.commands.registerCommand('pr.openDescriptionToTheSide', async (descriptionNode: DescriptionNode) => {716const folderManager = reposManager.getManagerForIssueModel(descriptionNode.pullRequestModel);717if (!folderManager) {718return;719}720const pr = descriptionNode.pullRequestModel;721const pullRequest = ensurePR(folderManager, pr);722descriptionNode.reveal(descriptionNode, { select: true, focus: true });723// Create and show a new webview724PullRequestOverviewPanel.createOrShow(context.extensionUri, folderManager, pullRequest, true);725726}),727);728729context.subscriptions.push(730vscode.commands.registerCommand('pr.showDiffSinceLastReview', async (descriptionNode: DescriptionNode) => {731descriptionNode.pullRequestModel.showChangesSinceReview = true;732}),733);734735context.subscriptions.push(736vscode.commands.registerCommand('pr.showDiffAll', async (descriptionNode: DescriptionNode) => {737descriptionNode.pullRequestModel.showChangesSinceReview = false;738}),739);740741context.subscriptions.push(742vscode.commands.registerCommand('pr.signin', async () => {743await reposManager.authenticate();744}),745);746747context.subscriptions.push(748vscode.commands.registerCommand('pr.signinNoEnterprise', async () => {749await reposManager.authenticate(false);750}),751);752753context.subscriptions.push(754vscode.commands.registerCommand('pr.signinenterprise', async () => {755await reposManager.authenticate(true);756}),757);758759context.subscriptions.push(760vscode.commands.registerCommand('pr.deleteLocalBranchesNRemotes', async () => {761for (const folderManager of reposManager.folderManagers) {762await folderManager.deleteLocalBranchesNRemotes();763}764}),765);766767context.subscriptions.push(768vscode.commands.registerCommand('pr.signinAndRefreshList', async () => {769if (await reposManager.authenticate()) {770vscode.commands.executeCommand('pr.refreshList');771}772}),773);774775context.subscriptions.push(776vscode.commands.registerCommand('pr.configureRemotes', async () => {777return vscode.commands.executeCommand('workbench.action.openSettings', `@ext:${EXTENSION_ID} remotes`);778}),779);780781context.subscriptions.push(782vscode.commands.registerCommand('pr.startReview', async (reply: CommentReply) => {783const handler = resolveCommentHandler(reply.thread);784785if (handler) {786handler.startReview(reply.thread, reply.text);787}788}),789);790791context.subscriptions.push(792vscode.commands.registerCommand('pr.openReview', async (thread: GHPRCommentThread) => {793const handler = resolveCommentHandler(thread);794795if (handler) {796await handler.openReview(thread);797}798}),799);800801function threadAndText(commentLike: CommentReply | GHPRCommentThread | GHPRComment | any): { thread: GHPRCommentThread, text: string } {802let thread: GHPRCommentThread;803let text: string = '';804if (commentLike instanceof GHPRComment) {805thread = commentLike.parent;806} else if (CommentReply.is(commentLike)) {807thread = commentLike.thread;808} else if (GHPRCommentThread.is(commentLike?.thread)) {809thread = commentLike.thread;810} else {811thread = commentLike;812}813return { thread, text };814}815816context.subscriptions.push(817vscode.commands.registerCommand('pr.resolveReviewThread', async (commentLike: CommentReply | GHPRCommentThread | GHPRComment) => {818const { thread, text } = threadAndText(commentLike);819const handler = resolveCommentHandler(thread);820821if (handler) {822await handler.resolveReviewThread(thread, text);823}824}),825);826827context.subscriptions.push(828vscode.commands.registerCommand('pr.unresolveReviewThread', async (commentLike: CommentReply | GHPRCommentThread | GHPRComment) => {829const { thread, text } = threadAndText(commentLike);830831const handler = resolveCommentHandler(thread);832833if (handler) {834await handler.unresolveReviewThread(thread, text);835}836}),837);838839context.subscriptions.push(840vscode.commands.registerCommand('pr.createComment', async (reply: CommentReply) => {841const handler = resolveCommentHandler(reply.thread);842843if (handler) {844handler.createOrReplyComment(reply.thread, reply.text, false);845}846}),847);848849context.subscriptions.push(850vscode.commands.registerCommand('pr.createSingleComment', async (reply: CommentReply) => {851const handler = resolveCommentHandler(reply.thread);852853if (handler) {854handler.createOrReplyComment(reply.thread, reply.text, true);855}856}),857);858859context.subscriptions.push(860vscode.commands.registerCommand('pr.makeSuggestion', async (reply: CommentReply | GHPRComment) => {861const thread = reply instanceof GHPRComment ? reply.parent : reply.thread;862const commentEditor = vscode.window.activeTextEditor?.document.uri.scheme === Schemes.Comment ? vscode.window.activeTextEditor863: vscode.window.visibleTextEditors.find(visible => (visible.document.uri.scheme === Schemes.Comment) && (visible.document.uri.query === ''));864if (!commentEditor) {865Logger.error('No comment editor visible for making a suggestion.');866vscode.window.showErrorMessage(vscode.l10n.t('No available comment editor to make a suggestion in.'));867return;868}869const editor = vscode.window.visibleTextEditors.find(editor => editor.document.uri.toString() === thread.uri.toString());870const contents = editor?.document.getText(new vscode.Range(thread.range.start.line, 0, thread.range.end.line, editor.document.lineAt(thread.range.end.line).text.length));871return commentEditor.edit((editBuilder) => {872editBuilder.insert(commentEditor.selection.end, `873\`\`\`suggestion874${contents}875\`\`\``);876});877})878);879880context.subscriptions.push(881vscode.commands.registerCommand('pr.editComment', async (comment: GHPRComment | TemporaryComment) => {882comment.startEdit();883}),884);885886context.subscriptions.push(887vscode.commands.registerCommand('pr.editQuery', (query: CategoryTreeNode) => {888return query.editQuery();889}),890);891892context.subscriptions.push(893vscode.commands.registerCommand('pr.cancelEditComment', async (comment: GHPRComment | TemporaryComment) => {894comment.cancelEdit();895}),896);897898context.subscriptions.push(899vscode.commands.registerCommand('pr.saveComment', async (comment: GHPRComment | TemporaryComment) => {900const handler = resolveCommentHandler(comment.parent);901902if (handler) {903await handler.editComment(comment.parent, comment);904}905}),906);907908context.subscriptions.push(909vscode.commands.registerCommand('pr.deleteComment', async (comment: GHPRComment | TemporaryComment) => {910const deleteOption = vscode.l10n.t('Delete');911const shouldDelete = await vscode.window.showWarningMessage(vscode.l10n.t('Delete comment?'), { modal: true }, deleteOption);912913if (shouldDelete === deleteOption) {914const handler = resolveCommentHandler(comment.parent);915916if (handler) {917await handler.deleteComment(comment.parent, comment);918}919}920}),921);922923context.subscriptions.push(924vscode.commands.registerCommand('review.openFile', (value: GitFileChangeNode | vscode.Uri) => {925const command = value instanceof GitFileChangeNode ? value.openFileCommand() : openFileCommand(value);926vscode.commands.executeCommand(command.command, ...(command.arguments ?? []));927}),928);929930context.subscriptions.push(931vscode.commands.registerCommand('review.openLocalFile', (value: vscode.Uri) => {932const { path, rootPath } = fromReviewUri(value.query);933const localUri = vscode.Uri.joinPath(vscode.Uri.file(rootPath), path);934const editor = vscode.window.visibleTextEditors.find(editor => editor.document.uri.toString() === value.toString());935const command = openFileCommand(localUri, editor ? { selection: editor.selection } : undefined);936vscode.commands.executeCommand(command.command, ...(command.arguments ?? []));937}),938);939940context.subscriptions.push(941vscode.commands.registerCommand('pr.refreshChanges', _ => {942reviewManagers.forEach(reviewManager => {943reviewManager.updateComments();944PullRequestOverviewPanel.refresh();945reviewManager.changesInPrDataProvider.refresh();946});947}),948);949950context.subscriptions.push(951vscode.commands.registerCommand('pr.setFileListLayoutAsTree', _ => {952vscode.workspace.getConfiguration('githubPullRequests').update('fileListLayout', 'tree', true);953}),954);955956context.subscriptions.push(957vscode.commands.registerCommand('pr.setFileListLayoutAsFlat', _ => {958vscode.workspace.getConfiguration('githubPullRequests').update('fileListLayout', 'flat', true);959}),960);961962context.subscriptions.push(963vscode.commands.registerCommand('pr.refreshPullRequest', (prNode: PRNode) => {964const folderManager = reposManager.getManagerForIssueModel(prNode.pullRequestModel);965if (folderManager && prNode.pullRequestModel.equals(folderManager?.activePullRequest)) {966ReviewManager.getReviewManagerForFolderManager(reviewManagers, folderManager)?.updateComments();967}968969PullRequestOverviewPanel.refresh();970tree.refresh(prNode);971}),972);973974context.subscriptions.push(975vscode.commands.registerCommand('pr.markFileAsViewed', async (treeNode: FileChangeNode | vscode.Uri | undefined) => {976try {977if (treeNode === undefined) {978// Use the active editor to enable keybindings979treeNode = vscode.window.activeTextEditor?.document.uri;980}981982if (treeNode instanceof FileChangeNode) {983await treeNode.markFileAsViewed();984} else if (treeNode) {985// When the argument is a uri it came from the editor menu and we should also close the file986// Do the close first to improve perceived performance of marking as viewed.987const tab = vscode.window.tabGroups.activeTabGroup.activeTab;988if (tab) {989let compareUri: vscode.Uri | undefined = undefined;990if (tab.input instanceof vscode.TabInputTextDiff) {991compareUri = tab.input.modified;992} else if (tab.input instanceof vscode.TabInputText) {993compareUri = tab.input.uri;994}995if (compareUri && treeNode.toString() === compareUri.toString()) {996vscode.window.tabGroups.close(tab);997}998}999const manager = reposManager.getManagerForFile(treeNode);1000await manager?.activePullRequest?.markFileAsViewed(treeNode.path);1001manager?.setFileViewedContext();1002}1003} catch (e) {1004vscode.window.showErrorMessage(`Marked file as viewed failed: ${e}`);1005}1006}),1007);10081009context.subscriptions.push(1010vscode.commands.registerCommand('pr.unmarkFileAsViewed', async (treeNode: FileChangeNode | vscode.Uri | undefined) => {1011try {1012if (treeNode === undefined) {1013// Use the active editor to enable keybindings1014treeNode = vscode.window.activeTextEditor?.document.uri;1015}10161017if (treeNode instanceof FileChangeNode) {1018treeNode.unmarkFileAsViewed();1019} else if (treeNode) {1020const manager = reposManager.getManagerForFile(treeNode);1021await manager?.activePullRequest?.unmarkFileAsViewed(treeNode.path);1022manager?.setFileViewedContext();1023}1024} catch (e) {1025vscode.window.showErrorMessage(`Marked file as not viewed failed: ${e}`);1026}1027}),1028);10291030context.subscriptions.push(1031vscode.commands.registerCommand('pr.resetViewedFiles', async () => {1032try {1033return reposManager.folderManagers.map(async (manager) => {1034await manager.activePullRequest?.unmarkAllFilesAsViewed();1035manager.setFileViewedContext();1036});1037} catch (e) {1038vscode.window.showErrorMessage(`Marked file as not viewed failed: ${e}`);1039}1040}),1041);10421043context.subscriptions.push(1044vscode.commands.registerCommand('pr.collapseAllComments', () => {1045return vscode.commands.executeCommand('workbench.action.collapseAllComments');1046}));10471048context.subscriptions.push(1049vscode.commands.registerCommand('pr.copyCommentLink', (comment) => {1050if (comment instanceof GHPRComment) {1051return vscode.env.clipboard.writeText(comment.rawComment.htmlUrl);1052}1053}));10541055context.subscriptions.push(1056vscode.commands.registerCommand('pr.copyVscodeDevPrLink', async () => {1057const activePullRequests: PullRequestModel[] = reposManager.folderManagers1058.map(folderManager => folderManager.activePullRequest!)1059.filter(activePR => !!activePR);1060const pr = await chooseItem<PullRequestModel>(1061activePullRequests,1062itemValue => `${itemValue.number}: ${itemValue.title}`,1063{ placeHolder: vscode.l10n.t('Pull request to create a link for') },1064);1065if (pr) {1066return vscode.env.clipboard.writeText(vscodeDevPrLink(pr));1067}1068}));10691070context.subscriptions.push(1071vscode.commands.registerCommand('pr.checkoutByNumber', async () => {10721073const githubRepositories: { manager: FolderRepositoryManager, repo: GitHubRepository }[] = [];1074reposManager.folderManagers.forEach(manager => {1075githubRepositories.push(...(manager.gitHubRepositories.map(repo => { return { manager, repo }; })));1076});1077const githubRepo = await chooseItem<{ manager: FolderRepositoryManager, repo: GitHubRepository }>(1078githubRepositories,1079itemValue => `${itemValue.repo.remote.owner}/${itemValue.repo.remote.repositoryName}`,1080{ placeHolder: vscode.l10n.t('Which GitHub repository do you want to checkout the pull request from?') }1081);1082if (!githubRepo) {1083return;1084}1085const prNumberMatcher = /^#?(\d*)$/;1086const prNumber = await vscode.window.showInputBox({1087ignoreFocusOut: true, prompt: vscode.l10n.t('Enter the pull request number'),1088validateInput: (input: string) => {1089const matches = input.match(prNumberMatcher);1090if (!matches || (matches.length !== 2) || Number.isNaN(Number(matches[1]))) {1091return vscode.l10n.t('Value must be a number');1092}1093return undefined;1094}1095});1096if ((prNumber === undefined) || prNumber === '#') {1097return;1098}1099const prModel = await githubRepo.manager.fetchById(githubRepo.repo, Number(prNumber.match(prNumberMatcher)![1]));1100if (prModel) {1101return ReviewManager.getReviewManagerForFolderManager(reviewManagers, githubRepo.manager)?.switch(prModel);1102}1103}));11041105function chooseRepoToOpen() {1106const githubRepositories: GitHubRepository[] = [];1107reposManager.folderManagers.forEach(manager => {1108githubRepositories.push(...(manager.gitHubRepositories));1109});1110return chooseItem<GitHubRepository>(1111githubRepositories,1112itemValue => `${itemValue.remote.owner}/${itemValue.remote.repositoryName}`,1113{ placeHolder: vscode.l10n.t('Which GitHub repository do you want to open?') }1114);1115}1116context.subscriptions.push(1117vscode.commands.registerCommand('pr.openPullsWebsite', async () => {1118const githubRepo = await chooseRepoToOpen();1119if (githubRepo) {1120vscode.env.openExternal(getPullsUrl(githubRepo));1121}1122}));1123context.subscriptions.push(1124vscode.commands.registerCommand('issues.openIssuesWebsite', async () => {1125const githubRepo = await chooseRepoToOpen();1126if (githubRepo) {1127vscode.env.openExternal(getIssuesUrl(githubRepo));1128}1129}));11301131context.subscriptions.push(1132vscode.commands.registerCommand('pr.applySuggestion', async (comment: GHPRComment) => {11331134const handler = resolveCommentHandler(comment.parent);11351136if (handler instanceof ReviewCommentController) {1137handler.applySuggestion(comment);1138}1139}));11401141function goToNextPrevDiff(diffs: vscode.LineChange[], next: boolean) {1142const tab = vscode.window.tabGroups.activeTabGroup.activeTab;1143const input = tab?.input;1144if (!(input instanceof vscode.TabInputTextDiff)) {1145return vscode.window.showErrorMessage(vscode.l10n.t('Current editor isn\'t a diff editor.'));1146}11471148const editor = vscode.window.visibleTextEditors.find(editor => editor.document.uri.toString() === input.modified.toString());1149if (!editor) {1150return vscode.window.showErrorMessage(vscode.l10n.t('Unexpectedly unable to find the current modified editor.'));1151}11521153const editorUri = editor.document.uri;1154if (input.original.scheme !== Schemes.Review) {1155return vscode.window.showErrorMessage(vscode.l10n.t('Current file isn\'t a pull request diff.'));1156}11571158// Find the next diff in the current file to scroll to1159const visibleRange = editor.visibleRanges[0];1160const iterateThroughDiffs = next ? diffs : diffs.reverse();1161for (const diff of iterateThroughDiffs) {1162const practicalModifiedEndLineNumber = (diff.modifiedEndLineNumber > diff.modifiedStartLineNumber) ? diff.modifiedEndLineNumber : diff.modifiedStartLineNumber as number + 1;1163const diffRange = new vscode.Range(diff.modifiedStartLineNumber ? diff.modifiedStartLineNumber - 1 : diff.modifiedStartLineNumber, 0, practicalModifiedEndLineNumber, 0);1164if (next && (visibleRange.end.line < practicalModifiedEndLineNumber) && (visibleRange.end.line !== (editor.document.lineCount - 1))) {1165editor.revealRange(diffRange);1166return;1167} else if (!next && (visibleRange.start.line > diff.modifiedStartLineNumber) && (visibleRange.start.line !== 0)) {1168editor.revealRange(diffRange);1169return;1170}1171}11721173// There is no new range to reveal, time to go to the next file.1174const folderManager = reposManager.getManagerForFile(editorUri);1175if (!folderManager) {1176return vscode.window.showErrorMessage(vscode.l10n.t('Unable to find a repository for pull request.'));1177}11781179const reviewManager = ReviewManager.getReviewManagerForFolderManager(reviewManagers, folderManager);1180if (!reviewManager) {1181return vscode.window.showErrorMessage(vscode.l10n.t('Cannot find active pull request.'));1182}11831184if (!reviewManager.reviewModel.hasLocalFileChanges || (reviewManager.reviewModel.localFileChanges.length === 0)) {1185return vscode.window.showWarningMessage(vscode.l10n.t('Pull request data is not yet complete, please try again in a moment.'));1186}11871188for (let i = 0; i < reviewManager.reviewModel.localFileChanges.length; i++) {1189const index = next ? i : reviewManager.reviewModel.localFileChanges.length - 1;1190const localFileChange = reviewManager.reviewModel.localFileChanges[index];1191if (localFileChange.changeModel.filePath.toString() === editorUri.toString()) {1192const nextIndex = next ? index + 1 : index - 1;1193if (reviewManager.reviewModel.localFileChanges.length > nextIndex) {1194return reviewManager.reviewModel.localFileChanges[nextIndex].openDiff(folderManager);1195}1196}1197}1198// No further files in PR.1199const goInCircle = next ? vscode.l10n.t('Go to first diff') : vscode.l10n.t('Go to last diff');1200return vscode.window.showInformationMessage(vscode.l10n.t('There are no more diffs in this pull request.'), goInCircle).then(result => {1201if (result === goInCircle) {1202return reviewManager.reviewModel.localFileChanges[next ? 0 : reviewManager.reviewModel.localFileChanges.length - 1].openDiff(folderManager);1203}1204});1205}12061207context.subscriptions.push(1208vscode.commands.registerDiffInformationCommand('pr.goToNextDiffInPr', async (diffs: vscode.LineChange[]) => {1209goToNextPrevDiff(diffs, true);1210}));1211context.subscriptions.push(1212vscode.commands.registerDiffInformationCommand('pr.goToPreviousDiffInPr', async (diffs: vscode.LineChange[]) => {1213goToNextPrevDiff(diffs, false);1214}));1215}121612171218