Change:
Example:
Do you mind if I take on this issue? I assume the request is to make and Ctrl-clickable such that the links open the correct versions of the files.
sounds good. The link registration is done here and it's based on regexs: We basically want the and parts to be matched, but ignored as part of the link.
I'm having a bit of trouble with getting the two git blob IDs and , which are necessary to get the correct versions of the files. I tried to write a multi-line regex for that captures the blob IDs in groups, along with the paths we want to link, but currently only runs the regex on a per line basis (by iterating through the buffer), not on the whole content of the terminal. So the regex didn't find any matches. I also tried to get the blob IDs by parsing the contents of the terminal manually, but doesn't seem to offer a public interface to retrieve the contents. I do see but I don't want to rely on an internal implementation to do something like this. How do you suggest I proceed? Should I submit a PR to to allow multi-line regex in ?
I'm having a bit of trouble with getting the two git blob IDs and , which are necessary to get the correct versions of the files. Oh I wouldn't go that far, just pointing both links to the current version of the file would be good enough start with.
Ah I see, that's a lot simpler to do. Will open a PR soon.

Discussion:
private _widgetManager: TerminalWidgetManager; private _processCwd: string; private _localLinkPattern: RegExp; <ins> private _gitDiffPreImagePattern: RegExp; private _gitDiffPostImagePattern: RegExp; private readonly _tooltipCallback: (event: MouseEvent, uri: string) => boolean | void; </ins> constructor( private _xterm: any,

const baseLocalLinkClause = _platform === platform.Platform.Windows ? winLocalLinkClause : unixLocalLinkClause; // Append line and column number regex this._localLinkPattern = new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`); <ins> // Matches '--- a/src/file1', capturing 'src/file1' in group 1 this._gitDiffPreImagePattern = /^--- a/(S*)/; // Matches '+++ b/src/file1', capturing 'src/file1' in group 1 this._gitDiffPostImagePattern = /^+++ b/(S*)/; this._tooltipCallback = (e: MouseEvent) => { if (this._terminalService && this._terminalService.configHelper.config.rendererType === 'dom') { const target = (e.target as HTMLElement); this._widgetManager.showMessage(target.offsetLeft, target.offsetTop, this._getLinkHoverString()); } else { this._widgetManager.showMessage(e.offsetX, e.offsetY, this._getLinkHoverString()); } }; </ins> this.registerWebLinkHandler(); this.registerLocalLinkHandler(); <ins> this.registerGitDiffLinkHandlers(); </ins> } public setWidgetManager(widgetManager: TerminalWidgetManager): void {

public registerCustomLinkHandler(regex: RegExp, handler: (uri: string) => void, matchIndex?: number, validationCallback?: XtermLinkMatcherValidationCallback): number { const options: ILinkMatcherOptions = { matchIndex, <del> tooltipCallback: (e: MouseEvent) => { if (this._terminalService && this._terminalService.configHelper.config.rendererType === 'dom') { const target = (e.target as HTMLElement); this._widgetManager.showMessage(target.offsetLeft, target.offsetTop, this._getLinkHoverString()); } else { this._widgetManager.showMessage(e.offsetX, e.offsetY, this._getLinkHoverString()); } }, </del> <ins> tooltipCallback: this._tooltipCallback, </ins> leaveCallback: () => this._widgetManager.closeMessage(), willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e), priority: CUSTOM_LINK_PRIORITY

}); this._xterm.webLinksInit(wrappedHandler, { validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateWebLink(uri, callback), <del> tooltipCallback: (e: MouseEvent) => { if (this._terminalService && this._terminalService.configHelper.config.rendererType === 'dom') { const target = (e.target as HTMLElement); this._widgetManager.showMessage(target.offsetLeft, target.offsetTop, this._getLinkHoverString()); } else { this._widgetManager.showMessage(e.offsetX, e.offsetY, this._getLinkHoverString()); } }, </del> <ins> tooltipCallback: this._tooltipCallback, </ins> leaveCallback: () => this._widgetManager.closeMessage(), willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e) });

}); this._xterm.registerLinkMatcher(this._localLinkRegex, wrappedHandler, { validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateLocalLink(uri, callback), <del> tooltipCallback: (e: MouseEvent) => { if (this._terminalService && this._terminalService.configHelper.config.rendererType === 'dom') { const target = (e.target as HTMLElement); this._widgetManager.showMessage(target.offsetLeft, target.offsetTop, this._getLinkHoverString()); } else { this._widgetManager.showMessage(e.offsetX, e.offsetY, this._getLinkHoverString()); } }, </del> <ins> tooltipCallback: this._tooltipCallback, </ins> leaveCallback: () => this._widgetManager.closeMessage(), willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e), priority: LOCAL_LINK_PRIORITY }); } <ins> public registerGitDiffLinkHandlers(): void { const wrappedHandler = this._wrapLinkHandler(url => { this._handleLocalLink(url); }); const options = { matchIndex: 1, validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateLocalLink(uri, callback), tooltipCallback: this._tooltipCallback, leaveCallback: () => this._widgetManager.closeMessage(), willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e), priority: LOCAL_LINK_PRIORITY }; this._xterm.registerLinkMatcher(this._gitDiffPreImagePattern, wrappedHandler, options); this._xterm.registerLinkMatcher(this._gitDiffPostImagePattern, wrappedHandler, options); } </ins> public dispose(): void { this._xterm = null; this._hoverDisposables = dispose(this._hoverDisposables);

return this._localLinkPattern; } <ins> protected get _gitDiffPreImageRegex(): RegExp { return this._gitDiffPreImagePattern; } protected get _gitDiffPostImageRegex(): RegExp { return this._gitDiffPostImagePattern; } </ins> private _handleLocalLink(link: string): PromiseLike<any> { return this._resolvePath(link).then(resolvedLink => { if (!resolvedLink) {

public get localLinkRegex(): RegExp { return this._localLinkRegex; } <ins> public get gitDiffLinkPreImageRegex(): RegExp { return this._gitDiffPreImageRegex; } public get gitDiffLinkPostImageRegex(): RegExp { return this._gitDiffPostImageRegex; } </ins> public preprocessPath(link: string): string | null { return this._preprocessPath(link); }

assert.equal(linkHandler.preprocessPath('/absolute/path/file3'), '/absolute/path/file3'); }); }); <ins> test('gitDiffLinkRegex', () => { // The platform is irrelevant because the links generated by Git are the same format regardless of platform const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, null!, null!, null!, null!); function assertAreGoodMatches(matches: RegExpMatchArray | null) { if (matches) { assert.equal(matches.length, 2); assert.equal(matches[1], 'src/file1'); } else { assert.fail(); } } // Happy cases assertAreGoodMatches('--- a/src/file1'.match(linkHandler.gitDiffLinkPreImageRegex)); assertAreGoodMatches('--- a/src/file1             '.match(linkHandler.gitDiffLinkPreImageRegex)); assertAreGoodMatches('+++ b/src/file1'.match(linkHandler.gitDiffLinkPostImageRegex)); assertAreGoodMatches('+++ b/src/file1             '.match(linkHandler.gitDiffLinkPostImageRegex)); // Make sure /dev/null isn't a match assert.equal(linkHandler.gitDiffLinkPreImageRegex.test('--- /dev/null'), false); assert.equal(linkHandler.gitDiffLinkPreImageRegex.test('--- /dev/null           '), false); assert.equal(linkHandler.gitDiffLinkPostImageRegex.test('+++ /dev/null'), false); assert.equal(linkHandler.gitDiffLinkPostImageRegex.test('+++ /dev/null          '), false); }); </ins> });