content: package awesome.console;
  
  import awesome.console.config.AwesomeConsoleConfig;
  import awesome.console.match.FileLinkMatch;
  import awesome.console.match.URLLinkMatch;
  import awesome.console.util.IntegerUtil;
  import com.intellij.execution.filters.Filter;
  import com.intellij.execution.filters.HyperlinkInfo;
  import com.intellij.execution.filters.HyperlinkInfoFactory;
  import com.intellij.ide.browsers.OpenUrlHyperlinkInfo;
  import com.intellij.openapi.diagnostic.Logger;
  import com.intellij.openapi.editor.LogicalPosition;
  import com.intellij.openapi.editor.VisualPosition;
  import com.intellij.openapi.project.Project;
  import com.intellij.openapi.roots.ProjectRootManager;
  import com.intellij.openapi.vfs.VirtualFile;
  import com.intellij.util.PathUtil;
  import org.jetbrains.annotations.NotNull;
  import org.jetbrains.annotations.Nullable;
  
  import java.io.File;
  import java.util.*;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  import java.util.stream.Collectors;
  
  public class AwesomeLinkFilter implements Filter {
  	private static final Logger logger = Logger.getInstance(AwesomeLinkFilter.class);
  
  	public static final Pattern FILE_PATTERN = Pattern.compile(
  			"(?<link>(?<path>\"?([.~])?(([a-zA-Z]:)?[\\\\/])?\\w[@\\w/\\-.\\\\]*\\.[\\w\\-.]+)\\$?" +
  			"(?:(?::|\"?, line |:\\[|\\()(?<row>\\d+)(?:[:,]( column )?(?<col>\\d+)([)\\]])?)?)?)",
  			Pattern.UNICODE_CHARACTER_CLASS);
  	public static final Pattern URL_PATTERN = Pattern.compile(
  			"(?<link>[(']?(?<protocol>(([a-zA-Z]+):)?([/\\\\~]))(?<path>[-.!~*\\\\'()\\w;/?:@&=+$,%#]+))",
  			Pattern.UNICODE_CHARACTER_CLASS);
  	private static final int maxSearchDepth = 1;
  
  	private final AwesomeConsoleConfig config;
  	private final Map<String, List<VirtualFile>> fileCache;
  	private final Map<String, List<VirtualFile>> fileBaseCache;
  	private final Project project;
  	private final List<String> srcRoots;
  	private final ThreadLocal<Matcher> fileMatcher = ThreadLocal.withInitial(() -> FILE_PATTERN.matcher(""));
  	private final ThreadLocal<Matcher> urlMatcher = ThreadLocal.withInitial(() -> URL_PATTERN.matcher(""));
  	private final ProjectRootManager projectRootManager;
  
  	public AwesomeLinkFilter(final Project project) {
  		this.project = project;
  		this.fileCache = new ConcurrentHashMap<>();
  		this.fileBaseCache = new ConcurrentHashMap<>();
  		projectRootManager = ProjectRootManager.getInstance(project);
  		srcRoots = getSourceRoots();
  		config = AwesomeConsoleConfig.getInstance();
  
  		createFileCache();
  	}
  
  	@Nullable
  	@Override
  	public Result applyFilter(final String line, final int endPoint) {
  		final List<ResultItem> results = new ArrayList<>();
  		final int startPoint = endPoint - line.length();
  		final List<String> chunks = splitLine(line);
  		int offset = 0;
  
  		for (final String chunk : chunks) {
  			if (config.SEARCH_URLS) {
  				results.addAll(getResultItemsUrl(chunk, startPoint + offset));
  			}
  			results.addAll(getResultItemsFile(chunk, startPoint + offset));
  			offset += chunk.length();
  		}
  
  		return new Result(results);
  	}
  
  	public List<String> splitLine(final String line) {
  		final List<String> chunks = new ArrayList<>();
  		final int length = line.length();
  		if (!config.LIMIT_LINE_LENGTH || config.LINE_MAX_LENGTH >= length) {
  			chunks.add(line);
  			return chunks;
  		}
  		if (!config.SPLIT_ON_LIMIT) {
  			chunks.add(line.substring(0, config.LINE_MAX_LENGTH));
  			return chunks;
  		}
  		int offset = 0;
  		do {
  			final String chunk = line.substring(offset, Math.min(length, offset + config.LINE_MAX_LENGTH));
  			chunks.add(chunk);
  			offset += config.LINE_MAX_LENGTH;
  		} while (offset < length - 1);
  		return chunks;
  	}
  
  	public List<ResultItem> getResultItemsUrl(final String line, final int startPoint) {
  		final List<ResultItem> results = new ArrayList<>();
  		final List<URLLinkMatch> matches = detectURLs(line);
  
  		for (final URLLinkMatch match : matches) {
  			final String file = getFileFromUrl(match.match);
  
  			if (null != file && !new File(file).exists()) {
  				continue;
  			}
  			results.add(
  					new Result(
  							startPoint + match.start,
  							startPoint + match.end,
  							new OpenUrlHyperlinkInfo(match.match))
  			);
  		}
  		return results;
  	}
  
  	public String getFileFromUrl(final String url) {
  		if (url.startsWith("/")) {
  			return url;
  		}
  		final String fileUrl = "file://";
  		if (url.startsWith(fileUrl)) {
  			return url.substring(fileUrl.length());
  		}
  		return null;
  	}
  
  	public List<ResultItem> getResultItemsFile(final String line, final int startPoint) {
  		final List<ResultItem> results = new ArrayList<>();
  		final HyperlinkInfoFactory hyperlinkInfoFactory = HyperlinkInfoFactory.getInstance();
  
  		final List<FileLinkMatch> matches = detectPaths(line);
  
  		for(final FileLinkMatch match: matches) {
  			final String path = PathUtil.getFileName(match.path);
  			List<VirtualFile> matchingFiles = fileCache.get(path);
  
  			if (null == matchingFiles) {
  				matchingFiles = getResultItemsFileFromBasename(path);
  				if (null == matchingFiles || matchingFiles.isEmpty()) {
  					continue;
  				}
  			}
  
  			if (matchingFiles.isEmpty()) {
  				continue;
  			}
  
  			final List<VirtualFile> bestMatchingFiles = findBestMatchingFiles(match, matchingFiles);
  			if (bestMatchingFiles != null && !bestMatchingFiles.isEmpty()) {
  				matchingFiles = bestMatchingFiles;
  			}
  
  			final int row = match.linkedRow <= 0 ? 0 : match.linkedRow - 1;
  			final HyperlinkInfo linkInfo = hyperlinkInfoFactory.createMultipleFilesHyperlinkInfo(
  					matchingFiles,
  					row,
  					project,
  					(project, psiFile, editor, originalEditor) -> editor.getCaretModel().moveToLogicalPosition(new LogicalPosition(row, match.linkedCol))
  			);
  
  			results.add(new Result(
  					startPoint + match.start,
  					startPoint + match.end,
  					linkInfo)
  			);
  		}
  
  		return results;
  	}
  
  	private List<VirtualFile> findBestMatchingFiles(final FileLinkMatch match, final List<VirtualFile> matchingFiles) {
  		return findBestMatchingFiles(generalizePath(match.path), matchingFiles);
  	}
  
  	private List<VirtualFile> findBestMatchingFiles(final String generalizedMatchPath,
  			final List<VirtualFile> matchingFiles) {
  		final List<VirtualFile> foundFiles = getFilesByPath(generalizedMatchPath, matchingFiles);
  		if (!foundFiles.isEmpty()) {
  			return foundFiles;
  		}
  		final String widerMetchingPath = dropOneLevelFromRoot(generalizedMatchPath);
  		if (widerMetchingPath != null) {
  			return findBestMatchingFiles(widerMetchingPath, matchingFiles);
  		}
  		return null;
  	}
  
  	private List<VirtualFile> getFilesByPath(final String generalizedMatchPath, final List<VirtualFile> matchingFiles) {
  		return matchingFiles.parallelStream()
  				.filter(file -> generalizePath(file.getPath()).endsWith(generalizedMatchPath))
  				.collect(Collectors.toList());
  	}
  
  	private String dropOneLevelFromRoot(final String path) {
  		if (path.contains("/")) {
  			return path.substring(path.indexOf('/')+1);
  		} else {
  			return null;
  		}
  	}
  
  	private String generalizePath(final String path) {
  		return path.replace('\\', '/');
  	}
  
  	public List<VirtualFile> getResultItemsFileFromBasename(final String match) {
  		return getResultItemsFileFromBasename(match, 0);
  	}
  
  	public List<VirtualFile> getResultItemsFileFromBasename(final String match, final int depth) {
  		final char packageSeparator = '.';
  		final int index = match.lastIndexOf(packageSeparator);
  		if (-1 >= index) {
  			return new ArrayList<>();
  		}
  		final String basename = match.substring(index + 1);
  		final String origin = match.substring(0, index);
  		final String path = origin.replace(packageSeparator, File.separatorChar);
  		if (0 >= basename.length()) {
  			return new ArrayList<>();
  		}
  		if (!fileBaseCache.containsKey(basename)) {
  			/* Try to search deeper down the rabbit hole */
  			if (depth <= maxSearchDepth) {
  				return getResultItemsFileFromBasename(origin, depth + 1);
  			}
  			return new ArrayList<>();
  		}
  
  		return fileBaseCache.get(basename).parallelStream()
  				.filter(file -> null != file.getParent())
  				.filter(file -> matchSource(file.getParent().getPath(), path))
  				.collect(Collectors.toList());
  	}
  
  	private void createFileCache() {
  		projectRootManager.getFileIndex().iterateContent(
  				new AwesomeProjectFilesIterator(fileCache, fileBaseCache));
  	}
  
  	private List<String> getSourceRoots() {
  		final VirtualFile[] contentSourceRoots = projectRootManager.getContentSourceRoots();
  		return Arrays.stream(contentSourceRoots).map(VirtualFile::getPath).collect(Collectors.toList());
  	}
  
  	private boolean matchSource(final String parent, final String path) {
  		for (final String srcRoot : srcRoots) {
  			if ((srcRoot + File.separatorChar + path).equals(parent)) {
  				return true;
  			}
  		}
  		return false;
  	}
  
  	@NotNull
  	public List<FileLinkMatch> detectPaths(@NotNull final String line) {
  		final Matcher fileMatcher = this.fileMatcher.get();
  		fileMatcher.reset(line);
  		final List<FileLinkMatch> results = new LinkedList<>();
  		while (fileMatcher.find()) {
  			final String match = fileMatcher.group("link");
  			final String path = fileMatcher.group("path");
  			if (null == path) {
  				logger.error("Regex group 'path' was NULL while trying to match path line: " + line + "\nfor match: " + match);
  				continue;
  			}
  			final int row = IntegerUtil.parseInt(fileMatcher.group("row")).orElse(0);
  			final int col = IntegerUtil.parseInt(fileMatcher.group("col")).orElse(0);
  			results.add(new FileLinkMatch(match, path,
  					fileMatcher.start(), fileMatcher.end(),
  					row, col));
  		}
  		return results;
  	}
  
  	@NotNull
  	public List<URLLinkMatch> detectURLs(@NotNull final String line) {
  		final Matcher urlMatcher = this.urlMatcher.get();
  		urlMatcher.reset(line);
  		final List<URLLinkMatch> results = new LinkedList<>();
  		while (urlMatcher.find()) {
  			String match = urlMatcher.group("link");
  			if (null == match) {
  				logger.error("Regex group 'link' was NULL while trying to match url line: " + line);
  				continue;
  			}
  
  			int startOffset = 0;
  			int endOffset = 0;
  
  			for (final String surrounding : new String[]{"()", "''"}) {
  				final String start = "" + surrounding.charAt(0);
  				final String end = "" + surrounding.charAt(1);
  				if (match.startsWith(start)) {
  					startOffset = 1;
  					match = match.substring(1);
  					if (match.endsWith(end)) {
  						endOffset = 1;
  						match = match.substring(0, match.length() - 1);
  					}
  				}
  			}
  			results.add(new URLLinkMatch(match, urlMatcher.start() + startOffset, urlMatcher.end() - endOffset));
  		}
  		return results;
  	}
  }
  