Skip to content

Commit afa199e

Browse files
Copilotalexr00
andauthored
Render emojis in labels correctly (#7400)
* Initial plan * Initial exploration: identify emoji rendering issue in labels Co-authored-by: alexr00 <[email protected]> * Add emoji support to label rendering in markdown and webviews Co-authored-by: alexr00 <[email protected]> * Fix emoji rendering in labels by adding displayName property - Add optional displayName property to ILabel interface - Keep original name for API calls, use displayName for UI rendering - Update issueOverview and createPRViewProvider to populate displayName with emojified text - Update Label components to display emojified text while preserving original name for removeLabel operations This addresses the issue where removeLabel was failing because it was trying to use emojified names in API calls. Co-authored-by: alexr00 <[email protected]> * Make it work --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: alexr00 <[email protected]>
1 parent aa241ae commit afa199e

File tree

11 files changed

+108
-82
lines changed

11 files changed

+108
-82
lines changed

common/views.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { ClosedEvent, CommentEvent } from '../src/common/timelineEvent';
77
import { GithubItemStateEnum, IAccount, ILabel, IMilestone, IProject, ITeam, MergeMethod, MergeMethodsAvailability } from '../src/github/interface';
8-
import { PreReviewState } from '../src/github/views';
8+
import { DisplayLabel, PreReviewState } from '../src/github/views';
99

1010
export interface RemoteInfo {
1111
owner: string;
@@ -108,7 +108,7 @@ export interface CreateParamsNew {
108108
compareBranch?: string;
109109
isDraftDefault: boolean;
110110
isDraft?: boolean;
111-
labels?: ILabel[];
111+
labels?: DisplayLabel[];
112112
projects?: IProject[];
113113
assignees?: IAccount[];
114114
reviewers?: (IAccount | ITeam)[];
@@ -169,14 +169,14 @@ export interface TitleAndDescriptionResult {
169169
description: string | undefined;
170170
}
171171

172-
export interface CloseResult {
173-
state: GithubItemStateEnum;
174-
commentEvent?: CommentEvent;
175-
closeEvent: ClosedEvent;
176-
}
177-
178-
export interface OpenCommitChangesArgs {
179-
commitSha: string;
180-
}
181-
172+
export interface CloseResult {
173+
state: GithubItemStateEnum;
174+
commentEvent?: CommentEvent;
175+
closeEvent: ClosedEvent;
176+
}
177+
178+
export interface OpenCommitChangesArgs {
179+
commitSha: string;
180+
}
181+
182182
// #endregion

src/github/activityBarViewProvider.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import * as vscode from 'vscode';
77
import { openPullRequestOnGitHub } from '../commands';
88
import { IComment } from '../common/comment';
9+
import { emojify, ensureEmojis } from '../common/emoji';
910
import { disposeAll } from '../common/lifecycle';
1011
import { ReviewEvent } from '../common/timelineEvent';
1112
import { formatError } from '../common/utils';
@@ -219,7 +220,8 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W
219220
this._folderRepositoryManager.getPullRequestRepositoryDefaultBranch(pullRequestModel),
220221
this._folderRepositoryManager.getCurrentUser(pullRequestModel.githubRepository),
221222
pullRequestModel.canEdit(),
222-
pullRequestModel.validateDraftMode()
223+
pullRequestModel.validateDraftMode(),
224+
ensureEmojis(this._folderRepositoryManager.context)
223225
])
224226
.then(result => {
225227
const [pullRequest, repositoryAccess, timelineEvents, requestedReviewers, branchInfo, defaultBranch, currentUser, viewerCanEdit, hasReviewDraft] = result;
@@ -267,7 +269,7 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W
267269
createdAt: pullRequest.createdAt,
268270
body: pullRequest.body,
269271
bodyHTML: pullRequest.bodyHTML,
270-
labels: pullRequest.item.labels,
272+
labels: pullRequest.item.labels.map(label => ({ ...label, displayName: emojify(label.name) })),
271273
author: {
272274
login: pullRequest.author.login,
273275
name: pullRequest.author.name,

src/github/createPRViewProvider.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as vscode from 'vscode';
77
import { ChooseBaseRemoteAndBranchResult, ChooseCompareRemoteAndBranchResult, ChooseRemoteAndBranchArgs, CreateParamsNew, CreatePullRequestNew, RemoteInfo, TitleAndDescriptionArgs } from '../../common/views';
88
import type { Branch, Ref } from '../api/api';
99
import { GitHubServerType } from '../common/authentication';
10+
import { emojify, ensureEmojis } from '../common/emoji';
1011
import { commands, contexts } from '../common/executeCommands';
1112
import Logger from '../common/logger';
1213
import { Protocol } from '../common/protocol';
@@ -38,7 +39,7 @@ import { PullRequestModel } from './pullRequestModel';
3839
import { getDefaultMergeMethod } from './pullRequestOverview';
3940
import { getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick, reviewersQuickPick } from './quickPicks';
4041
import { getIssueNumberLabelFromParsed, ISSUE_EXPRESSION, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, variableSubstitution } from './utils';
41-
import { PreReviewState } from './views';
42+
import { DisplayLabel, PreReviewState } from './views';
4243

4344
const ISSUE_CLOSING_KEYWORDS = new RegExp('closes|closed|close|fixes|fixed|fix|resolves|resolved|resolve\s$', 'i'); // https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword
4445

@@ -170,7 +171,9 @@ export abstract class BaseCreatePullRequestViewProvider<T extends BasePullReques
170171
const [detectedBaseMetadata, remotes, defaultOrigin] = await Promise.all([
171172
this.detectBaseMetadata(defaultCompareBranch),
172173
this._folderRepositoryManager.getGitHubRemotes(),
173-
this._folderRepositoryManager.getOrigin(defaultCompareBranch)]);
174+
this._folderRepositoryManager.getOrigin(defaultCompareBranch),
175+
ensureEmojis(this._folderRepositoryManager.context)
176+
]);
174177

175178
const defaultBaseRemote: RemoteInfo = {
176179
owner: detectedBaseMetadata?.owner ?? this._pullRequestDefaults.owner,
@@ -225,7 +228,7 @@ export abstract class BaseCreatePullRequestViewProvider<T extends BasePullReques
225228
}
226229
const preReviewer = this._folderRepositoryManager.getAutoReviewer();
227230

228-
this.labels = labels;
231+
this.labels = labels.map(label => ({ ...label, displayName: emojify(label.name) }));
229232

230233
const params: CreateParamsNew = {
231234
canModifyBranches: true,
@@ -430,20 +433,20 @@ export abstract class BaseCreatePullRequestViewProvider<T extends BasePullReques
430433
});
431434
}
432435

433-
private labels: ILabel[] = [];
436+
private labels: DisplayLabel[] = [];
434437
public async addLabels(): Promise<void> {
435-
let newLabels: ILabel[] = [];
438+
let newLabels: DisplayLabel[] = [];
436439

437-
const labelsToAdd = await vscode.window.showQuickPick(
440+
const labelsToAdd = await vscode.window.showQuickPick<vscode.QuickPickItem & { name: string }>(
438441
getLabelOptions(this._folderRepositoryManager, this.labels, this.model.baseOwner, this.model.repositoryName).then(options => {
439442
newLabels = options.newLabels;
440443
return options.labelPicks;
441-
}) as Promise<vscode.QuickPickItem[]>,
444+
}),
442445
{ canPickMany: true, matchOnDescription: true, placeHolder: vscode.l10n.t('Apply labels') },
443446
);
444447

445448
if (labelsToAdd) {
446-
const addedLabels: ILabel[] = labelsToAdd.map(label => newLabels.find(l => l.name === label.label)!);
449+
const addedLabels: DisplayLabel[] = labelsToAdd.map(label => newLabels.find(l => l.name === label.name)!);
447450
this.labels = addedLabels;
448451
this._postMessage({
449452
command: 'set-labels',

src/github/issueOverview.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,19 @@ import * as vscode from 'vscode';
88
import { CloseResult } from '../../common/views';
99
import { openPullRequestOnGitHub } from '../commands';
1010
import { COPILOT_ACCOUNTS, IComment } from '../common/comment';
11+
import { emojify, ensureEmojis } from '../common/emoji';
1112
import Logger from '../common/logger';
1213
import { PR_SETTINGS_NAMESPACE, WEBVIEW_REFRESH_INTERVAL } from '../common/settingKeys';
1314
import { ITelemetry } from '../common/telemetry';
1415
import { CommentEvent, EventType, ReviewStateValue, TimelineEvent } from '../common/timelineEvent';
1516
import { asPromise, formatError } from '../common/utils';
1617
import { getNonce, IRequestMessage, WebviewBase } from '../common/webview';
1718
import { FolderRepositoryManager } from './folderRepositoryManager';
18-
import { GithubItemStateEnum, IAccount, ILabel, IMilestone, IProject, IProjectItem, RepoAccessAndMergeMethods } from './interface';
19+
import { GithubItemStateEnum, IAccount, IMilestone, IProject, IProjectItem, RepoAccessAndMergeMethods } from './interface';
1920
import { IssueModel } from './issueModel';
2021
import { getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick } from './quickPicks';
2122
import { isInCodespaces, vscodeDevPrLink } from './utils';
22-
import { ChangeAssigneesReply, Issue, ProjectItemsReply, SubmitReviewReply } from './views';
23+
import { ChangeAssigneesReply, DisplayLabel, Issue, ProjectItemsReply, SubmitReviewReply } from './views';
2324

2425
export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends WebviewBase {
2526
public static ID: string = 'IssueOverviewPanel';
@@ -42,6 +43,7 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
4243
issue: IssueModel,
4344
toTheSide: Boolean = false,
4445
) {
46+
await ensureEmojis(folderRepositoryManager.context);
4547
const activeColumn = toTheSide
4648
? vscode.ViewColumn.Beside
4749
: vscode.window.activeTextEditor
@@ -194,6 +196,11 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
194196
protected getInitializeContext(currentUser: IAccount, issue: IssueModel, timelineEvents: TimelineEvent[], repositoryAccess: RepoAccessAndMergeMethods, viewerCanEdit: boolean, assignableUsers: IAccount[]): Issue {
195197
const hasWritePermission = repositoryAccess!.hasWritePermission;
196198
const canEdit = hasWritePermission || viewerCanEdit;
199+
const labels = issue.item.labels.map(label => ({
200+
...label,
201+
displayName: emojify(label.name)
202+
}));
203+
197204
const context: Issue = {
198205
owner: issue.remote.owner,
199206
repo: issue.remote.repositoryName,
@@ -204,7 +211,7 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
204211
createdAt: issue.createdAt,
205212
body: issue.body,
206213
bodyHTML: issue.bodyHTML,
207-
labels: issue.item.labels,
214+
labels: labels,
208215
author: issue.author,
209216
state: issue.state,
210217
events: timelineEvents,
@@ -402,9 +409,9 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
402409
}
403410

404411
private async addLabels(message: IRequestMessage<void>): Promise<void> {
405-
const quickPick = vscode.window.createQuickPick<vscode.QuickPickItem>();
412+
const quickPick = vscode.window.createQuickPick<(vscode.QuickPickItem & { name: string })>();
406413
try {
407-
let newLabels: ILabel[] = [];
414+
let newLabels: DisplayLabel[] = [];
408415

409416
quickPick.busy = true;
410417
quickPick.canSelectMany = true;
@@ -420,13 +427,13 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
420427
return quickPick.selectedItems;
421428
});
422429
const hidePromise = asPromise<void>(quickPick.onDidHide);
423-
const labelsToAdd = await Promise.race<readonly vscode.QuickPickItem[] | void>([acceptPromise, hidePromise]);
430+
const labelsToAdd = await Promise.race<readonly (vscode.QuickPickItem & { name: string })[] | void>([acceptPromise, hidePromise]);
424431
quickPick.busy = true;
425432
quickPick.enabled = false;
426433

427434
if (labelsToAdd) {
428-
await this._item.setLabels(labelsToAdd.map(r => r.label));
429-
const addedLabels: ILabel[] = labelsToAdd.map(label => newLabels.find(l => l.name === label.label)!);
435+
await this._item.setLabels(labelsToAdd.map(r => r.name));
436+
const addedLabels: DisplayLabel[] = labelsToAdd.map(label => newLabels.find(l => l.name === label.name)!);
430437

431438
await this._replyMessage(message, {
432439
added: addedLabels,

src/github/markdownUtils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import * as marked from 'marked';
77
import 'url-search-params-polyfill';
88
import * as vscode from 'vscode';
9+
import { ensureEmojis } from '../common/emoji';
910
import Logger from '../common/logger';
1011
import { CODE_PERMALINK, findCodeLinkLocally } from '../issues/issueLinkLookup';
1112
import { PullRequestDefaults } from './folderRepositoryManager';
@@ -188,6 +189,7 @@ export async function issueMarkdown(
188189
markdown.appendMarkdown(body + ' \n');
189190

190191
if (issue.item.labels.length > 0) {
192+
await ensureEmojis(context);
191193
markdown.appendMarkdown('&nbsp; \n');
192194
issue.item.labels.forEach(label => {
193195
markdown.appendMarkdown(

src/github/quickPicks.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
import { Buffer } from 'buffer';
88
import * as vscode from 'vscode';
99
import { COPILOT_ACCOUNTS } from '../common/comment';
10+
import { emojify, ensureEmojis } from '../common/emoji';
1011
import Logger from '../common/logger';
1112
import { DataUri } from '../common/uri';
1213
import { formatError } from '../common/utils';
1314
import { FolderRepositoryManager } from './folderRepositoryManager';
1415
import { GitHubRepository, TeamReviewerRefreshKind } from './githubRepository';
1516
import { AccountType, IAccount, ILabel, IMilestone, IProject, isSuggestedReviewer, isTeam, ISuggestedReviewer, ITeam, reviewerId, ReviewState } from './interface';
1617
import { IssueModel } from './issueModel';
18+
import { DisplayLabel } from './views';
1719

1820
async function getItems<T extends IAccount | ITeam | ISuggestedReviewer>(context: vscode.ExtensionContext, skipList: Set<string>, users: T[], picked: boolean, tooManyAssignable: boolean = false): Promise<(vscode.QuickPickItem & { user?: T })[]> {
1921
const alreadyAssignedItems: (vscode.QuickPickItem & { user?: T })[] = [];
@@ -392,12 +394,14 @@ export async function getLabelOptions(
392394
labels: ILabel[],
393395
baseOwner: string,
394396
repositoryName: string
395-
): Promise<{ newLabels: ILabel[], labelPicks: vscode.QuickPickItem[] }> {
396-
const newLabels = await folderRepoManager.getLabels(undefined, { owner: baseOwner, repo: repositoryName });
397+
): Promise<{ newLabels: DisplayLabel[], labelPicks: (vscode.QuickPickItem & { name: string })[] }> {
398+
await ensureEmojis(folderRepoManager.context);
399+
const newLabels = (await folderRepoManager.getLabels(undefined, { owner: baseOwner, repo: repositoryName })).map(label => ({ ...label, displayName: emojify(label.name) }));
397400

398401
const labelPicks = newLabels.map(label => {
399402
return {
400-
label: label.name,
403+
label: label.displayName,
404+
name: label.name,
401405
description: label.description ?? undefined,
402406
picked: labels.some(existingLabel => existingLabel.name === label.name),
403407
iconPath: DataUri.asImageDataURI(Buffer.from(`<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">

src/github/utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { GitApiImpl } from '../api/api1';
1212
import { AuthProvider, GitHubServerType } from '../common/authentication';
1313
import { COPILOT_ACCOUNTS, IComment, IReviewThread, SubjectType } from '../common/comment';
1414
import { DiffHunk, parseDiffHunk } from '../common/diffHunk';
15+
import { emojify } from '../common/emoji';
1516
import { GitHubRef } from '../common/githubRef';
1617
import Logger from '../common/logger';
1718
import { Remote } from '../common/remote';
@@ -1746,7 +1747,8 @@ export function vscodeDevPrLink(pullRequest: IssueModel) {
17461747
export function makeLabel(label: ILabel): string {
17471748
const isDarkTheme = vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark;
17481749
const labelColor = gitHubLabelColor(label.color, isDarkTheme, true);
1749-
return `<span style="color:${labelColor.textColor};background-color:${labelColor.backgroundColor};border-radius:10px;">&nbsp;&nbsp;${label.name.trim()}&nbsp;&nbsp;</span>`;
1750+
const labelName = emojify(label.name.trim());
1751+
return `<span style="color:${labelColor.textColor};background-color:${labelColor.backgroundColor};border-radius:10px;">&nbsp;&nbsp;${labelName}&nbsp;&nbsp;</span>`;
17501752
}
17511753

17521754

src/github/views.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export enum ReviewType {
2626
RequestChanges = 'requestChanges',
2727
}
2828

29+
export interface DisplayLabel extends ILabel {
30+
displayName: string;
31+
}
32+
2933
export interface Issue {
3034
owner: string;
3135
repo: string;
@@ -39,7 +43,7 @@ export interface Issue {
3943
author: IAccount;
4044
state: GithubItemStateEnum; // TODO: don't allow merged
4145
events: TimelineEvent[];
42-
labels: ILabel[];
46+
labels: DisplayLabel[];
4347
assignees: IAccount[];
4448
projectItems: IProjectItem[] | undefined;
4549
milestone: IMilestone | undefined;

src/lm/tools/displayIssuesTool.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
'use strict';
66

77
import * as vscode from 'vscode';
8+
import { ensureEmojis } from '../../common/emoji';
89
import Logger from '../../common/logger';
910
import { reviewerLabel } from '../../github/interface';
1011
import { makeLabel } from '../../github/utils';
@@ -29,7 +30,7 @@ Here are the possible columns:
2930
export class DisplayIssuesTool extends ToolBase<DisplayIssuesParameters> {
3031
public static readonly toolId = 'github-pull-request_renderIssues';
3132
private static ID = 'DisplayIssuesTool';
32-
constructor(chatParticipantState: ChatParticipantState) {
33+
constructor(private readonly context: vscode.ExtensionContext, chatParticipantState: ChatParticipantState) {
3334
super(chatParticipantState);
3435
}
3536

@@ -144,6 +145,7 @@ export class DisplayIssuesTool extends ToolBase<DisplayIssuesParameters> {
144145
}
145146

146147
async invoke(options: vscode.LanguageModelToolInvocationOptions<DisplayIssuesParameters>, token: vscode.CancellationToken): Promise<vscode.LanguageModelToolResult | undefined> {
148+
await ensureEmojis(this.context);
147149
const issueItemsInfo: vscode.LanguageModelTextPart | undefined = this.chatParticipantState.firstUserMessage;
148150
const issueItems: IssueSearchResultItem[] | undefined = options.input.arrayOfIssues;
149151
if (!issueItems || issueItems.length === 0) {

src/lm/tools/tools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,5 @@ function registerCopilotAgentTools(context: vscode.ExtensionContext, copilotRemo
5050
function registerSearchTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState) {
5151
context.subscriptions.push(vscode.lm.registerTool(ConvertToSearchSyntaxTool.toolId, new ConvertToSearchSyntaxTool(credentialStore, repositoriesManager, chatParticipantState)));
5252
context.subscriptions.push(vscode.lm.registerTool(SearchTool.toolId, new SearchTool(credentialStore, repositoriesManager, chatParticipantState)));
53-
context.subscriptions.push(vscode.lm.registerTool(DisplayIssuesTool.toolId, new DisplayIssuesTool(chatParticipantState)));
53+
context.subscriptions.push(vscode.lm.registerTool(DisplayIssuesTool.toolId, new DisplayIssuesTool(context, chatParticipantState)));
5454
}

0 commit comments

Comments
 (0)