Change:
Originally posted on without success. <!-- Do Not Delete This! bugreporttemplate --<!-- Please read our Rules of Conduct: --<!-- Read our guide about submitting issues: --<!-- Search existing issues to avoid creating duplicates. --<!-- Test using the latest Insiders build to see if your issue has already been fixed: --<!-- Instead of creating your report here, use 'Report Issue' from the 'Help' menu in VS Code to pre-fill useful information. --<!-- Launch with to check. --Does this issue occur when all extensions are disabled?: Yes <!-- If you answered No above, use 'Help: Start Extension Bisect' from Command Palette to try to identify the cause. --<!-- Issues caused by an extension need to be reported directly to the extension publisher. The 'Help Report Issue' dialog can assist with this. --- VS Code Version: 1.69.2 OS Version: linux Steps to Reproduce: project has files like "Bar`1.cs" to represent generic types, as is done in the dotnet SDK build task contains "problemMatcher": { "base": "$msCompile" } When such a file has errors/warnings, I get this: is fine. But I cannot control-click on that filename to open it. The file's path is not underlined as normal - there is a clickable part before the bactick, and after. Someone from SO offered this guess: " probably backticks are not part of the regex that matches the filepath, or backticks are not in the w regex set"
I can reproduce this, it is correctly validating the link, but it then recognizes it as a folder: ! I guess nothing happens if Test doesn't exist? As for the fallback "word links", you can support this type of link if you remove backtick from word separators:
This falls apart when converting the link to a URI in !
And that happens because backticks aren't allowed in the local path regex:
Thanks

Discussion:
import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink'; import { TerminalLinkDetectorAdapter } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkDetectorAdapter'; import { TerminalLocalFileLinkOpener, TerminalLocalFolderInWorkspaceLinkOpener, TerminalLocalFolderOutsideWorkspaceLinkOpener, TerminalSearchLinkOpener, TerminalUrlLinkOpener } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners'; <del> import { lineAndColumnClause, TerminalLocalLinkDetector, unixLocalLinkClause, winDrivePrefix, winLocalLinkClause } from 'vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector'; </del> <ins> import { TerminalLocalLinkDetector, winDrivePrefix } from 'vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector'; </ins> import { TerminalUriLinkDetector } from 'vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector'; import { TerminalWordLinkDetector } from 'vs/workbench/contrib/terminal/browser/links/terminalWordLinkDetector'; import { ITerminalExternalLinkProvider, TerminalLinkQuickPickEvent } from 'vs/workbench/contrib/terminal/browser/terminal';

import type { ILink, ILinkProvider, IViewportRange, Terminal } from 'xterm'; import { convertBufferRangeToViewport } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers'; import { RunOnceScheduler } from 'vs/base/common/async'; <ins> import { removeLinkSuffix } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkParsing'; </ins> export type XtermLinkMatcherHandler = (event: MouseEvent | undefined, link: string) => Promise<void>;

return newLinkProvider; } <del> protected get _localLinkRegex(): RegExp { if (!this._processManager) { throw new Error('Process manager is required'); } const baseLocalLinkClause = this._processManager.os === OperatingSystem.Windows ? winLocalLinkClause : unixLocalLinkClause; // Append line and column number regex return new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`); } </del> protected _isLinkActivationModifierDown(event: MouseEvent): boolean { const editorConf = this._configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor'); if (editorConf.multiCursorModifier === 'ctrlCmd') {

return null; } <del> const linkUrl = this.extractLinkUrl(preprocessedLink); </del> <ins> const linkUrl = removeLinkSuffix(preprocessedLink); </ins> if (!linkUrl) { this._resolvedLinkCache.set(link, null); return null;

return null; } } <del> /** * Returns url from link as link may contain line and column information. * * @param link url link which may contain line and column number. */ extractLinkUrl(link: string): string | null { const matches: string[] | null = this._localLinkRegex.exec(link); if (!matches) { return null; } return matches[1]; } </del> } export interface ILineColumnInfo {

<ins> /*--------------------------------------------------------------------------------------------- *  Copyright (c) Microsoft Corporation. All rights reserved. *  Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Lazy } from 'vs/base/common/lazy'; /** * A regex that extracts the link suffix which contains line and column information. */ const linkSuffixRegex = new Lazy<RegExp>(() => { let ri = 0; let ci = 0; function l(): string { return `(?<row${ri++}>d+)`; } function c(): string { return `(?<col${ci++}>d+)`; } // The comments in the regex below use real strings/numbers for better readability, here's // the legend: // - Path = foo // - Row  = 339 // - Col  = 12 // // These all support single quote ' in the place of " and [] in the place of () const lineAndColumnRegexClauses = [ // foo:339 // foo:339:12 // foo 339 // foo 339:12                   [#140780] // "foo",339 // "foo",339:12 `(?::| |['"],)${l()}(:${c()})?$`, // "foo", line 339               [#40468] // "foo", line 339, col 12 // "foo", line 339, column 12 // "foo":line 339 // "foo":line 339, col 12 // "foo":line 339, column 12 // "foo": line 339 // "foo": line 339, col 12 // "foo": line 339, column 12 // "foo" on line 339 // "foo" on line 339, col 12 // "foo" on line 339, column 12 `['"](?:, |: ?| on )line ${l()}(, col(?:umn)? ${c()})?$`, // foo(339) // foo(339,12) // foo(339, 12) // foo (339) // foo (339,12) // foo (339, 12) ` ?[[(]${l()}(?:, ?${c()})?[])]`, ]; const suffixClause = lineAndColumnRegexClauses // Join all clauses together .join('|') // Convert spaces to allow the non-breaking space char (ascii 160) .replace(/ /g, `[${'u00A0'} ]`); return new RegExp(`(${suffixClause})`); }); /** * Removes the optional link suffix which contains line and column information. * @param link The link to parse. */ export function removeLinkSuffix(link: string): string { const suffix = getLinkSuffix(link)?.suffix; if (!suffix) { return link; } return link.substring(0, suffix.index); } /** * Returns the optional link suffix which contains line and column information. * @param link The link to parse. */ export function getLinkSuffix(link: string): { row: number | undefined; col: number | undefined; suffix: { index: number; text: string } } | null { const matches = linkSuffixRegex.getValue().exec(link); const groups = matches?.groups; if (!groups || matches.length < 1) { return null; } const rowString = groups.row0 || groups.row1 || groups.row2; const colString = groups.col0 || groups.col1 || groups.col2; return { row: rowString !== undefined ? parseInt(rowString) : undefined, col: colString !== undefined ? parseInt(colString) : undefined, suffix: { index: matches.index, text: matches[0] } }; } </ins>

/** A regex that matches paths in the form ?c:foo c:foo, ~foo, .foo, ..foo, foobar */ export const winLocalLinkClause = '((' + winPathPrefix + '|(' + winExcludedPathCharactersClause + ')+)?(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)+)'; <ins> // TODO: This should eventually move to the more structured terminalLinkParsing </ins> /** As xterm reads from DOM, space in that case is nonbreaking char ASCII code - 160, replacing space with nonBreakningSpace or space ASCII code - 32. */ export const lineAndColumnClause = [

<ins> /*--------------------------------------------------------------------------------------------- *  Copyright (c) Microsoft Corporation. All rights reserved. *  Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual } from 'assert'; import { getLinkSuffix, removeLinkSuffix } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkParsing'; interface ITestLink { link: string; suffix: string | undefined; hasRow: boolean; hasCol: boolean; } const testRow = 339; const testCol = 12; const testLinks: ITestLink[] = [ { link: 'foo', suffix: undefined, hasRow: false, hasCol: false }, { link: 'foo:339', suffix: ':339', hasRow: true, hasCol: false }, { link: 'foo:339:12', suffix: ':339:12', hasRow: true, hasCol: true }, { link: 'foo 339', suffix: ' 339', hasRow: true, hasCol: false }, { link: 'foo 339:12', suffix: ' 339:12', hasRow: true, hasCol: true }, { link: '"foo",339', suffix: '",339', hasRow: true, hasCol: false }, { link: '"foo",339:12', suffix: '",339:12', hasRow: true, hasCol: true }, { link: '"foo", line 339', suffix: '", line 339', hasRow: true, hasCol: false }, { link: '"foo", line 339, col 12', suffix: '", line 339, col 12', hasRow: true, hasCol: true }, { link: '"foo", line 339, column 12', suffix: '", line 339, column 12', hasRow: true, hasCol: true }, { link: '"foo":line 339', suffix: '":line 339', hasRow: true, hasCol: false }, { link: '"foo":line 339, col 12', suffix: '":line 339, col 12', hasRow: true, hasCol: true }, { link: '"foo":line 339, column 12', suffix: '":line 339, column 12', hasRow: true, hasCol: true }, { link: '"foo": line 339', suffix: '": line 339', hasRow: true, hasCol: false }, { link: '"foo": line 339, col 12', suffix: '": line 339, col 12', hasRow: true, hasCol: true }, { link: '"foo": line 339, column 12', suffix: '": line 339, column 12', hasRow: true, hasCol: true }, { link: '"foo" on line 339', suffix: '" on line 339', hasRow: true, hasCol: false }, { link: '"foo" on line 339, col 12', suffix: '" on line 339, col 12', hasRow: true, hasCol: true }, { link: '"foo" on line 339, column 12', suffix: '" on line 339, column 12', hasRow: true, hasCol: true }, { link: 'foo(339)', suffix: '(339)', hasRow: true, hasCol: false }, { link: 'foo(339,12)', suffix: '(339,12)', hasRow: true, hasCol: true }, { link: 'foo(339, 12)', suffix: '(339, 12)', hasRow: true, hasCol: true }, { link: 'foo (339)', suffix: ' (339)', hasRow: true, hasCol: false }, { link: 'foo (339,12)', suffix: ' (339,12)', hasRow: true, hasCol: true }, { link: 'foo (339, 12)', suffix: ' (339, 12)', hasRow: true, hasCol: true }, ]; suite('TerminalLinkParsing', () => { suite('removeLinkSuffix', () => { for (const testLink of testLinks) { test('`' + testLink.link + '`', () => { deepStrictEqual( removeLinkSuffix(testLink.link), testLink.suffix === undefined ? testLink.link : testLink.link.replace(testLink.suffix, '') ); }); } }); suite('getLinkSuffix', () => { for (const testLink of testLinks) { test('`' + testLink.link + '`', () => { deepStrictEqual( getLinkSuffix(testLink.link), testLink.suffix === undefined ? null : { row: testLink.hasRow ? testRow : undefined, col: testLink.hasCol ? testCol : undefined, suffix: { index: testLink.link.length - testLink.suffix.length, text: testLink.suffix } } as ReturnType<typeof getLinkSuffix> ); }); } }); }); </ins>