// Copyright 2012 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.collide.client.code.gotodefinition; import com.google.collide.client.util.PathUtil; import com.google.collide.client.workspace.FileTreeModel; import com.google.collide.client.workspace.FileTreeNode; import com.google.collide.codemirror2.Token; import com.google.collide.codemirror2.TokenType; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.document.LineInfo; import com.google.common.annotations.VisibleForTesting; import com.google.gwt.regexp.shared.MatchResult; import com.google.gwt.regexp.shared.RegExp; import javax.annotation.Nullable; /** * Strictly speaking, this class gives an answer to a question "is there a * reference at given file position?". It uses local parser with a delay to * find URL links / local file links or anchor references. * It is "dynamic" in a sense that it does not keep any state. */ public class DynamicReferenceProvider { // Word chars, digits or dash, at least one. private static final String DOMAIN_CHARS = "[\\w\\-\\d]+"; // ":" plus at least one digit, optional. private static final String PORT_CHARS = "(\\:\\d+)?"; @VisibleForTesting static final RegExp REGEXP_URL = RegExp.compile("\\b(https?|ftp)://(" + DOMAIN_CHARS + "\\.)*" + DOMAIN_CHARS + PORT_CHARS + "[^\\.\\s\\\"']*(\\.[^\\.\\s\\\"']+)*", "gi"); private final String contextPath; private final DeferringLineParser parser; private final FileTreeModel fileTreeModel; private final AnchorTagParser anchorTagParser; public DynamicReferenceProvider(String contextPath, DeferringLineParser parser, FileTreeModel fileTreeModel, @Nullable AnchorTagParser anchorTagParser) { this.contextPath = contextPath; this.parser = parser; this.fileTreeModel = fileTreeModel; this.anchorTagParser = anchorTagParser; } /** * Attemps to find a reference at given position. This method cannot find any * references if line is not yet parsed. This is always true if the method * was not called before. {@code blocking} flag tells whether we should wait * until the line is parsed and find a reference. * * @param lineInfo line to look reference at * @param column column to look reference at * @param blocking whether to block until given line is parsed * @return found reference at given position or {@code null} if line is not * yet parsed (happens only when {@code blocking} is {@code false} OR * if there's not reference at given position */ NavigableReference getReferenceAt(LineInfo lineInfo, int column, boolean blocking) { JsonArray<Token> parsedLineTokens = parser.getParseResult(lineInfo, blocking); // TODO: We should get parser state here. if (parsedLineTokens == null) { return null; } return getReferenceAt(lineInfo, column, parsedLineTokens); } @VisibleForTesting NavigableReference getReferenceAt(LineInfo lineInfo, int column, JsonArray<Token> tokens) { /* We care about: * - "href" attribute values in "a" tag, looking for anchors defined elsewhere, * - all comment and string literals, looking for urls, * - "src" or "href" attribute values, looking for urls and local file paths. */ boolean inAttribute = false; boolean inAnchorTag = false; boolean inHrefAttribute = false; int tokenEndColumn = 0; for (int i = 0, l = tokens.size() - 1; i < l; i++) { Token token = tokens.get(i); TokenType type = token.getType(); String value = token.getValue(); int tokenStartColumn = tokenEndColumn; tokenEndColumn += value.length(); // Exclusive. if (type == TokenType.TAG) { if (">".equals(value) || "/>".equals(value)) { inAttribute = false; inHrefAttribute = false; } inAnchorTag = "<a".equalsIgnoreCase(value); continue; } else if (type == TokenType.ATTRIBUTE) { if (inAnchorTag && "href".equals(value)) { inHrefAttribute = true; inAttribute = true; } else if ("src".equals(value) || "href".equals(value)) { inAttribute = true; } continue; } else if (tokenEndColumn <= column) { // Too early. continue; } else if (tokenStartColumn > column) { // We went too far, we have nothing. return null; } else if (type != TokenType.STRING && type != TokenType.COMMENT) { continue; } // So now the token covers given position and we're in a string/comment or we're in attribute // "src" or "href". Awesome! int lineNumber = lineInfo.number(); int valueStartColumn = tokenStartColumn; int valueEndColumn = tokenEndColumn; // Exclusive. String valueWithoutQuotes = value; if (inAttribute && value.startsWith("\"") && value.endsWith("\"")) { valueWithoutQuotes = value.substring(1, value.length() - 1); valueStartColumn++; valueEndColumn--; } if (valueStartColumn > column || column >= valueEndColumn) { continue; } // Now check if the value is a workspace file path. if (inAttribute) { FileTreeNode fileNode = findFileNode(valueWithoutQuotes); if (fileNode != null) { int filePathEndColumn = valueEndColumn - 1; // Incl. return NavigableReference.createToFile(lineNumber, valueStartColumn, filePathEndColumn, fileNode.getNodePath().getPathString()); } } // Now check if the value is an URL. REGEXP_URL.setLastIndex(0); for (MatchResult matchResult = REGEXP_URL.exec(valueWithoutQuotes); matchResult != null; matchResult = REGEXP_URL.exec(valueWithoutQuotes)) { int matchColumn = valueStartColumn + matchResult.getIndex(); int matchEndColumn = matchColumn + matchResult.getGroup(0).length() - 1; // Inclusive. if (matchEndColumn < column) { // Too early. continue; } if (matchColumn > column) { // Too far. return null; } return NavigableReference.createToUrl(lineNumber, matchColumn, matchResult.getGroup(0)); } // Now check if the value is the name of the anchor tag. if (inHrefAttribute && valueWithoutQuotes.startsWith("#")) { AnchorTagParser.AnchorTag anchorTag = findAnchorTag(valueWithoutQuotes.substring(1)); if (anchorTag != null) { return NavigableReference.createToFile( lineNumber, valueStartColumn, valueEndColumn - 1, contextPath, anchorTag.getLineNumber(), anchorTag.getColumn()); } } } return null; } @VisibleForTesting FileTreeNode findFileNode(String displayPath) { PathUtil lookupPath = new PathUtil(displayPath); if (!displayPath.startsWith("/")) { PathUtil contextDir = PathUtil.createExcludingLastN(new PathUtil(contextPath), 1); lookupPath = PathUtil.concatenate(contextDir, lookupPath); } return fileTreeModel.getWorkspaceRoot().findChildNode(lookupPath); } private AnchorTagParser.AnchorTag findAnchorTag(String name) { if (anchorTagParser == null) { return null; } JsonArray<AnchorTagParser.AnchorTag> anchorTags = anchorTagParser.getAnchorTags(); for (int i = 0; i < anchorTags.size(); i++) { if (anchorTags.get(i).getName().equalsIgnoreCase(name)) { return anchorTags.get(i); } } return null; } }