From 20e45655eb24674aa4fb920d57447e7efef652f5 Mon Sep 17 00:00:00 2001 From: Sergey Simonchik Date: Tue, 14 Oct 2025 00:18:40 +0200 Subject: [PATCH] [console, terminal] IJPL-201536 support links to local files as `file:/path/to/file` GitOrigin-RevId: 73ec8073c6e99b45f6bdf89c722e05c1af0fca3e --- .../intellij/execution/filters/UrlFilter.java | 27 +++++++++++++++---- .../execution/filters/UrlFilterTest.java | 9 +++++++ .../src/com/intellij/util/io/URLUtil.java | 4 +-- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/platform/execution-impl/src/com/intellij/execution/filters/UrlFilter.java b/platform/execution-impl/src/com/intellij/execution/filters/UrlFilter.java index 51662c7f8d04..78f174f0ddda 100644 --- a/platform/execution-impl/src/com/intellij/execution/filters/UrlFilter.java +++ b/platform/execution-impl/src/com/intellij/execution/filters/UrlFilter.java @@ -28,6 +28,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; public class UrlFilter implements Filter, DumbAware { + + private static final String FILE_MINIMAL_PROTOCOL_PREFIX = "file:/"; + private final Project myProject; public UrlFilter() { @@ -55,7 +58,7 @@ public class UrlFilter implements Filter, DumbAware { int textStartOffset = entireLength - line.length(); List resultList = new ArrayList<>(); - if (line.contains(LocalFileSystem.PROTOCOL_PREFIX)) { + if (line.contains(FILE_MINIMAL_PROTOCOL_PREFIX)) { resultList.addAll(findMatchingItems(line, URLUtil.FILE_URL_PATTERN_OPTIMIZED, textStartOffset)); } @@ -89,17 +92,21 @@ public class UrlFilter implements Filter, DumbAware { } private @Nullable HyperlinkInfo buildFileHyperlinkInfo(@NotNull String url) { - if (myProject != null && !url.endsWith(".html") && url.startsWith(LocalFileSystem.PROTOCOL_PREFIX)) { + if (myProject != null && !url.endsWith(".html")) { + String prefix = findFileProtocolPrefix(url); + if (prefix == null) { + return null; + } int documentLine = -1, documentColumn = -1; int filePathEndIndex = url.length(); final int lastColonInd = url.lastIndexOf(':'); - if (lastColonInd > LocalFileSystem.PROTOCOL_PREFIX.length() && lastColonInd < url.length() - 1) { + if (lastColonInd > prefix.length() && lastColonInd < url.length() - 1) { int lastValue = StringUtil.parseInt(url.substring(lastColonInd + 1), Integer.MIN_VALUE); if (lastValue != Integer.MIN_VALUE) { documentLine = lastValue - 1; filePathEndIndex = lastColonInd; int preLastColonInd = url.lastIndexOf(':', lastColonInd - 1); - if (preLastColonInd > LocalFileSystem.PROTOCOL_PREFIX.length()) { + if (preLastColonInd > prefix.length()) { int preLastValue = StringUtil.parseInt(url.substring(preLastColonInd + 1, lastColonInd), Integer.MIN_VALUE); if (preLastValue != Integer.MIN_VALUE) { documentLine = preLastValue - 1; @@ -109,12 +116,22 @@ public class UrlFilter implements Filter, DumbAware { } } } - String filePath = toWindowsPath(decode(url.substring(LocalFileSystem.PROTOCOL_PREFIX.length(), filePathEndIndex))); + String filePath = toWindowsPath(decode(url.substring(prefix.length(), filePathEndIndex))); return new FileUrlHyperlinkInfo(myProject, filePath, documentLine, documentColumn, url, true); } return null; } + private static @Nullable String findFileProtocolPrefix(@NotNull String url) { + if (url.startsWith("file:///")) { + return LocalFileSystem.PROTOCOL_PREFIX; + } + if (url.startsWith(FILE_MINIMAL_PROTOCOL_PREFIX) && !url.startsWith(LocalFileSystem.PROTOCOL_PREFIX)) { + return "file:"; + } + return null; + } + private static @NotNull String toWindowsPath(@NotNull String path) { if (path.length() >= 4 && path.charAt(0) == '/' && OSAgnosticPathUtil.isDriveLetter(path.charAt(1)) && path.charAt(2) == ':' && PathUtilRt.isSeparator(path.charAt(3))) { diff --git a/platform/platform-tests/testSrc/com/intellij/execution/filters/UrlFilterTest.java b/platform/platform-tests/testSrc/com/intellij/execution/filters/UrlFilterTest.java index 899cbb7d4205..370b7aa7ed1a 100644 --- a/platform/platform-tests/testSrc/com/intellij/execution/filters/UrlFilterTest.java +++ b/platform/platform-tests/testSrc/com/intellij/execution/filters/UrlFilterTest.java @@ -29,6 +29,15 @@ public class UrlFilterTest extends BasePlatformTestCase { 4, 52, "//wsl$/Ubuntu-20.04/projects/report.txt", 4, -1); } + public void testMinimalFileProtocol() { + assertFileHyperlink("Click file:/path/to/file.diff:2:4 to see the difference", 6, 33, "/path/to/file.diff", 2, 4); + assertFileHyperlink("file:/path/to/file.txt", 0, 22, "/path/to/file.txt", -1, -1); + } + + public void testNoFileHyperlink() { + assertBrowserHyperlink("file://path/to/file.txt", 0, 23); + } + public void testSingleBrowserHyperlink() { assertBrowserHyperlink("http://test.com", 0, 15); assertBrowserHyperlink(" at http://test.com", 4, 19); diff --git a/platform/util/src/com/intellij/util/io/URLUtil.java b/platform/util/src/com/intellij/util/io/URLUtil.java index 195878e0156e..ded4035527e9 100644 --- a/platform/util/src/com/intellij/util/io/URLUtil.java +++ b/platform/util/src/com/intellij/util/io/URLUtil.java @@ -39,7 +39,7 @@ public final class URLUtil { public static final Pattern DATA_URI_PATTERN_OPTIMIZED = Pattern.compile("data:[^,;]+/[^,;]+(?:;charset[=:][^,;]+)?(;base64)?,(.+)"); public static final Pattern URL_PATTERN_OPTIMIZED = Pattern.compile("\\b(?:mailto:|(?:news|(?:ht|f)tps?)://|(?([^<]*)"); @@ -49,7 +49,7 @@ public final class URLUtil { * If {@code false}, then the line contains no URL, otherwise the heavier {@link #URL_PATTERN} check should be used. */ public static boolean canContainUrl(@NotNull String line) { - return line.contains("mailto:") || line.contains(SCHEME_SEPARATOR) || line.contains("www."); + return line.contains("mailto:") || line.contains(SCHEME_SEPARATOR) || line.contains("www.") || line.contains("file:/"); } /**