diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml
index e2a01b4..7d656ea 100644
--- a/resources/META-INF/plugin.xml
+++ b/resources/META-INF/plugin.xml
@@ -98,6 +98,9 @@
+
+
+
diff --git a/src/cz/jiripudil/intellij/nette/tester/console/ActualExpectedPathDetector.java b/src/cz/jiripudil/intellij/nette/tester/console/ActualExpectedPathDetector.java
new file mode 100644
index 0000000..15f9579
--- /dev/null
+++ b/src/cz/jiripudil/intellij/nette/tester/console/ActualExpectedPathDetector.java
@@ -0,0 +1,46 @@
+package cz.jiripudil.intellij.nette.tester.console;
+
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.text.StringUtil;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+public class ActualExpectedPathDetector {
+ /**
+ * @see diff line
+ * @see argument escaping
+ */
+ private final static Pattern DIFF_LINE_REGEX;
+
+ static {
+ String unixArg = "'(?:[^']|'\\\\'')+'";
+ String winArg = "\"[^\"]+\"";
+ String unquotedArg = "[a-z0-9._=/:-]+";
+ String arg = "(" + unixArg + "|" + winArg + "|" + unquotedArg + ")";
+ DIFF_LINE_REGEX = Pattern.compile("^diff " + arg + " " + arg + "$");
+ }
+
+ @Nullable
+ public static Pair detectPaths(final String diffLine) {
+ Matcher matcher = DIFF_LINE_REGEX.matcher(diffLine);
+ if (matcher.find()) {
+ return Pair.create(unquoteArg(matcher.group(1)), unquoteArg(matcher.group(2)));
+ }
+ return null;
+ }
+
+ private static String unquoteArg(String arg) {
+ if (arg.startsWith("'")) {
+ return StringUtil.unquoteString(arg).replace("'\\''", "'");
+ }
+
+ if (arg.startsWith("\"")) {
+ return StringUtil.unquoteString(arg);
+ }
+
+ return arg;
+ }
+}
diff --git a/src/cz/jiripudil/intellij/nette/tester/console/FilterProvider.java b/src/cz/jiripudil/intellij/nette/tester/console/FilterProvider.java
new file mode 100644
index 0000000..bd0a601
--- /dev/null
+++ b/src/cz/jiripudil/intellij/nette/tester/console/FilterProvider.java
@@ -0,0 +1,18 @@
+package cz.jiripudil.intellij.nette.tester.console;
+
+import com.intellij.execution.filters.ConsoleFilterProvider;
+import com.intellij.execution.filters.Filter;
+import com.intellij.openapi.project.Project;
+import cz.jiripudil.intellij.nette.tester.console.filters.MakeDiffLinkTextClickableFilter;
+import org.jetbrains.annotations.NotNull;
+
+
+public class FilterProvider implements ConsoleFilterProvider {
+ @NotNull
+ @Override
+ public Filter[] getDefaultFilters(@NotNull Project project) {
+ return new Filter[]{
+ new MakeDiffLinkTextClickableFilter(),
+ };
+ }
+}
diff --git a/src/cz/jiripudil/intellij/nette/tester/console/InputFilterProvider.java b/src/cz/jiripudil/intellij/nette/tester/console/InputFilterProvider.java
new file mode 100644
index 0000000..a5a9bee
--- /dev/null
+++ b/src/cz/jiripudil/intellij/nette/tester/console/InputFilterProvider.java
@@ -0,0 +1,18 @@
+package cz.jiripudil.intellij.nette.tester.console;
+
+import com.intellij.execution.filters.ConsoleInputFilterProvider;
+import com.intellij.execution.filters.InputFilter;
+import com.intellij.openapi.project.Project;
+import cz.jiripudil.intellij.nette.tester.console.filters.InsertDiffLinkTextInputFilter;
+import org.jetbrains.annotations.NotNull;
+
+
+public class InputFilterProvider implements ConsoleInputFilterProvider {
+ @NotNull
+ @Override
+ public InputFilter[] getDefaultFilters(@NotNull Project project) {
+ return new InputFilter[]{
+ new InsertDiffLinkTextInputFilter(),
+ };
+ }
+}
diff --git a/src/cz/jiripudil/intellij/nette/tester/console/filters/InsertDiffLinkTextInputFilter.java b/src/cz/jiripudil/intellij/nette/tester/console/filters/InsertDiffLinkTextInputFilter.java
new file mode 100644
index 0000000..7309736
--- /dev/null
+++ b/src/cz/jiripudil/intellij/nette/tester/console/filters/InsertDiffLinkTextInputFilter.java
@@ -0,0 +1,28 @@
+package cz.jiripudil.intellij.nette.tester.console.filters;
+
+import com.intellij.execution.ExecutionBundle;
+import com.intellij.execution.filters.InputFilter;
+import com.intellij.execution.ui.ConsoleViewContentType;
+import com.intellij.openapi.util.Pair;
+import cz.jiripudil.intellij.nette.tester.console.ActualExpectedPathDetector;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Adds text {@link InsertDiffLinkTextInputFilter#DIFF_LINK_TEXT} after a line with
+ * diff shell command (see: {@link ActualExpectedPathDetector#DIFF_LINE_REGEX}).
+ */
+public class InsertDiffLinkTextInputFilter implements InputFilter {
+ final static String DIFF_LINK_TEXT = ExecutionBundle.message("junit.click.to.see.diff.link");
+
+ @Nullable
+ @Override
+ public List> applyFilter(String text, ConsoleViewContentType contentType) {
+ if (ActualExpectedPathDetector.detectPaths(text) != null) {
+ return Collections.singletonList(Pair.create(text + DIFF_LINK_TEXT + "\n", contentType));
+ }
+ return null;
+ }
+}
diff --git a/src/cz/jiripudil/intellij/nette/tester/console/filters/MakeDiffLinkTextClickableFilter.java b/src/cz/jiripudil/intellij/nette/tester/console/filters/MakeDiffLinkTextClickableFilter.java
new file mode 100644
index 0000000..eea9132
--- /dev/null
+++ b/src/cz/jiripudil/intellij/nette/tester/console/filters/MakeDiffLinkTextClickableFilter.java
@@ -0,0 +1,71 @@
+package cz.jiripudil.intellij.nette.tester.console.filters;
+
+import com.intellij.execution.filters.Filter;
+import com.intellij.execution.filters.HyperlinkInfo;
+import com.intellij.execution.testframework.stacktrace.DiffHyperlink;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.io.FileUtil;
+import cz.jiripudil.intellij.nette.tester.console.ActualExpectedPathDetector;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Catches text {@link InsertDiffLinkTextInputFilter#DIFF_LINK_TEXT} and makes
+ * it clickable if previous line contains diff shell command (see: {@link ActualExpectedPathDetector#DIFF_LINE_REGEX}).
+ */
+public class MakeDiffLinkTextClickableFilter implements Filter {
+ private String expectedPath;
+ private String actualPath;
+
+ @Nullable
+ @Override
+ public Result applyFilter(String line, int endPoint) {
+ if (line.equals(InsertDiffLinkTextInputFilter.DIFF_LINK_TEXT + "\n")) {
+ if (expectedPath != null && actualPath != null) {
+ Result result = new Result(endPoint - line.length(), endPoint, new LazyDiffHyperlinkInfo(expectedPath, actualPath));
+ expectedPath = actualPath = null;
+ return result;
+ }
+ return null;
+ }
+
+ Pair paths = ActualExpectedPathDetector.detectPaths(line);
+ if (paths != null) {
+ expectedPath = paths.first;
+ actualPath = paths.second;
+ }
+
+ return null;
+ }
+
+ private static class LazyDiffHyperlinkInfo implements HyperlinkInfo {
+ private String expectedPath;
+ private String actualPath;
+ private DiffHyperlink.DiffHyperlinkInfo link;
+
+ LazyDiffHyperlinkInfo(@NotNull String expectedPath, @NotNull String actualPath) {
+ this.expectedPath = expectedPath;
+ this.actualPath = actualPath;
+ }
+
+ @Override
+ public void navigate(Project project) {
+ if (link == null) {
+ String expected, actual;
+ try {
+ expected = FileUtil.loadFile(new File(expectedPath));
+ actual = FileUtil.loadFile(new File(actualPath));
+ } catch (IOException e) {
+ return;
+ }
+ link = new DiffHyperlink(expected, actual, expectedPath, actualPath, false).new DiffHyperlinkInfo();
+ }
+
+ link.navigate(project);
+ }
+ }
+}
diff --git a/test/diff-link-integration-test.php b/test/diff-link-integration-test.php
new file mode 100644
index 0000000..0d03157
--- /dev/null
+++ b/test/diff-link-integration-test.php
@@ -0,0 +1,33 @@
+