55'use strict' ;
66
77import * as vscode from 'vscode' ;
8- import { CloseResult } from '../../common/views' ;
8+ import { CloseResult , OpenLocalFileArgs } from '../../common/views' ;
99import { openPullRequestOnGitHub } from '../commands' ;
1010import { FolderRepositoryManager } from './folderRepositoryManager' ;
1111import { GithubItemStateEnum , IAccount , IMilestone , IProject , IProjectItem , RepoAccessAndMergeMethods } from './interface' ;
1212import { IssueModel } from './issueModel' ;
1313import { getAssigneesQuickPickItems , getLabelOptions , getMilestoneFromQuickPick , getProjectFromQuickPick } from './quickPicks' ;
14- import { isInCodespaces , vscodeDevPrLink } from './utils' ;
14+ import { isInCodespaces , processPermalinks , vscodeDevPrLink } from './utils' ;
1515import { ChangeAssigneesReply , DisplayLabel , Issue , ProjectItemsReply , SubmitReviewReply , UnresolvedIdentity } from './views' ;
1616import { COPILOT_ACCOUNTS , IComment } from '../common/comment' ;
1717import { emojify , ensureEmojis } from '../common/emoji' ;
@@ -249,7 +249,7 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
249249 return isInCodespaces ( ) ;
250250 }
251251
252- protected getInitializeContext ( currentUser : IAccount , issue : IssueModel , timelineEvents : TimelineEvent [ ] , repositoryAccess : RepoAccessAndMergeMethods , viewerCanEdit : boolean , assignableUsers : IAccount [ ] ) : Issue {
252+ protected async getInitializeContext ( currentUser : IAccount , issue : IssueModel , timelineEvents : TimelineEvent [ ] , repositoryAccess : RepoAccessAndMergeMethods , viewerCanEdit : boolean , assignableUsers : IAccount [ ] ) : Promise < Issue > {
253253 const hasWritePermission = repositoryAccess . hasWritePermission ;
254254 const canEdit = hasWritePermission || viewerCanEdit ;
255255 const labels = issue . item . labels . map ( label => ( {
@@ -266,12 +266,12 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
266266 url : issue . html_url ,
267267 createdAt : issue . createdAt ,
268268 body : issue . body ,
269- bodyHTML : issue . bodyHTML ,
269+ bodyHTML : await this . processLinksInBodyHtml ( issue . bodyHTML ) ,
270270 labels : labels ,
271271 author : issue . author ,
272272 state : issue . state ,
273273 stateReason : issue . stateReason ,
274- events : timelineEvents ,
274+ events : await this . processTimelineEvents ( timelineEvents ) ,
275275 continueOnGitHub : this . continueOnGitHub ( ) ,
276276 canEdit,
277277 hasWritePermission,
@@ -321,10 +321,13 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
321321 this . _item = issue as TItem ;
322322 this . setPanelTitle ( this . buildPanelTitle ( issueModel . number , issueModel . title ) ) ;
323323
324+ // Process permalinks in bodyHTML before sending to webview
325+ const context = await this . getInitializeContext ( currentUser , issue , timelineEvents , repositoryAccess , viewerCanEdit , assignableUsers [ this . _item . remote . remoteName ] ?? [ ] ) ;
326+
324327 Logger . debug ( 'pr.initialize' , IssueOverviewPanel . ID ) ;
325328 this . _postMessage ( {
326329 command : 'pr.initialize' ,
327- pullrequest : this . getInitializeContext ( currentUser , issue , timelineEvents , repositoryAccess , viewerCanEdit , assignableUsers [ this . _item . remote . remoteName ] ?? [ ] ) ,
330+ pullrequest : context ,
328331 } ) ;
329332
330333 } catch ( e ) {
@@ -445,6 +448,8 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
445448 return this . copyVscodeDevLink ( ) ;
446449 case 'pr.openOnGitHub' :
447450 return openPullRequestOnGitHub ( this . _item , this . _telemetry ) ;
451+ case 'pr.open-local-file' :
452+ return this . openLocalFile ( message ) ;
448453 case 'pr.debug' :
449454 return this . webviewDebug ( message ) ;
450455 default :
@@ -568,16 +573,54 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
568573 Logger . debug ( message . args , IssueOverviewPanel . ID ) ;
569574 }
570575
571- private editDescription ( message : IRequestMessage < { text : string } > ) {
572- this . _item
573- . edit ( { body : message . args . text } )
574- . then ( result => {
575- this . _replyMessage ( message , { body : result . body , bodyHTML : result . bodyHTML } ) ;
576- } )
577- . catch ( e => {
578- this . _throwError ( message , e ) ;
579- vscode . window . showErrorMessage ( `Editing description failed: ${ formatError ( e ) } ` ) ;
580- } ) ;
576+ /**
577+ * Process code reference links in bodyHTML. Can be overridden by subclasses (e.g., PullRequestOverviewPanel)
578+ * to provide custom processing logic for different item types.
579+ * Returns undefined if bodyHTML is undefined.
580+ */
581+ protected async processLinksInBodyHtml ( bodyHTML : string | undefined ) : Promise < string | undefined > {
582+ if ( ! bodyHTML ) {
583+ return bodyHTML ;
584+ }
585+ return processPermalinks (
586+ bodyHTML ,
587+ this . _item . githubRepository ,
588+ this . _item . githubRepository . rootUri
589+ ) ;
590+ }
591+
592+ /**
593+ * Process code reference links in timeline events (comments, reviews, commits).
594+ * Updates bodyHTML fields for all events that contain them.
595+ */
596+ protected async processTimelineEvents ( events : TimelineEvent [ ] ) : Promise < TimelineEvent [ ] > {
597+ return Promise . all ( events . map ( async ( event ) => {
598+ // Create a shallow copy to avoid mutating the original
599+ const processedEvent = { ...event } ;
600+
601+ if ( processedEvent . event === EventType . Commented || processedEvent . event === EventType . Reviewed || processedEvent . event === EventType . Committed ) {
602+ processedEvent . bodyHTML = await this . processLinksInBodyHtml ( processedEvent . bodyHTML ) ;
603+ // ReviewEvent also has comments array
604+ if ( processedEvent . event === EventType . Reviewed && processedEvent . comments ) {
605+ processedEvent . comments = await Promise . all ( processedEvent . comments . map ( async ( comment : IComment ) => ( {
606+ ...comment ,
607+ bodyHTML : await this . processLinksInBodyHtml ( comment . bodyHTML )
608+ } ) ) ) ;
609+ }
610+ }
611+ return processedEvent ;
612+ } ) ) ;
613+ }
614+
615+ private async editDescription ( message : IRequestMessage < { text : string } > ) {
616+ try {
617+ const result = await this . _item . edit ( { body : message . args . text } ) ;
618+ const bodyHTML = await this . processLinksInBodyHtml ( result . bodyHTML ) ;
619+ this . _replyMessage ( message , { body : result . body , bodyHTML } ) ;
620+ } catch ( e ) {
621+ this . _throwError ( message , e ) ;
622+ vscode . window . showErrorMessage ( `Editing description failed: ${ formatError ( e ) } ` ) ;
623+ }
581624 }
582625 private editTitle ( message : IRequestMessage < { text : string } > ) {
583626 return this . _item
@@ -591,8 +634,9 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
591634 } ) ;
592635 }
593636
594- protected _getTimeline ( ) : Promise < TimelineEvent [ ] > {
595- return this . _item . getIssueTimelineEvents ( ) ;
637+ protected async _getTimeline ( ) : Promise < TimelineEvent [ ] > {
638+ const events = await this . _item . getIssueTimelineEvents ( ) ;
639+ return this . processTimelineEvents ( events ) ;
596640 }
597641
598642 private async changeAssignees ( message : IRequestMessage < void > ) : Promise < void > {
@@ -726,18 +770,15 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
726770 return this . _item . editIssueComment ( comment , text ) ;
727771 }
728772
729- private editComment ( message : IRequestMessage < { comment : IComment ; text : string } > ) {
730- this . editCommentPromise ( message . args . comment , message . args . text )
731- . then ( result => {
732- this . _replyMessage ( message , {
733- body : result . body ,
734- bodyHTML : result . bodyHTML ,
735- } ) ;
736- } )
737- . catch ( e => {
738- this . _throwError ( message , e ) ;
739- vscode . window . showErrorMessage ( formatError ( e ) ) ;
740- } ) ;
773+ private async editComment ( message : IRequestMessage < { comment : IComment ; text : string } > ) {
774+ try {
775+ const result = await this . editCommentPromise ( message . args . comment , message . args . text ) ;
776+ const bodyHTML = await this . processLinksInBodyHtml ( result . bodyHTML ) ;
777+ this . _replyMessage ( message , { body : result . body , bodyHTML } ) ;
778+ } catch ( e ) {
779+ this . _throwError ( message , e ) ;
780+ vscode . window . showErrorMessage ( formatError ( e ) ) ;
781+ }
741782 }
742783
743784 protected deleteCommentPromise ( comment : IComment ) : Promise < void > {
@@ -761,6 +802,29 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
761802 } ) ;
762803 }
763804
805+ protected async openLocalFile ( message : IRequestMessage < OpenLocalFileArgs > ) : Promise < void > {
806+ try {
807+ const { file, startLine, endLine } = message . args ;
808+ // Resolve relative path to absolute using repository root
809+ const fileUri = vscode . Uri . joinPath (
810+ this . _item . githubRepository . rootUri ,
811+ file
812+ ) ;
813+ const selection = new vscode . Range (
814+ new vscode . Position ( startLine - 1 , 0 ) ,
815+ new vscode . Position ( endLine - 1 , Number . MAX_SAFE_INTEGER )
816+ ) ;
817+ await vscode . window . showTextDocument ( fileUri , {
818+ selection,
819+ viewColumn : vscode . ViewColumn . One
820+ } ) ;
821+ } catch ( e ) {
822+ Logger . error ( `Open local file failed: ${ formatError ( e ) } ` , IssueOverviewPanel . ID ) ;
823+ // Fallback to opening external URL
824+ await vscode . env . openExternal ( vscode . Uri . parse ( message . args . href ) ) ;
825+ }
826+ }
827+
764828 protected async close ( message : IRequestMessage < string > ) {
765829 let comment : IComment | undefined ;
766830 if ( message . args ) {
0 commit comments