/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2013 Oracle and/or its affiliates. All rights reserved. * * Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. * * Contributor(s): * * Portions Copyrighted 2013 Sun Microsystems, Inc. */ package org.nbphpcouncil.modules.php.yii.editor; import java.awt.Point; import java.awt.Rectangle; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.SwingUtilities; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.JTextComponent; import org.nbphpcouncil.modules.php.yii.YiiModule; import org.nbphpcouncil.modules.php.yii.YiiModuleFactory; import static org.nbphpcouncil.modules.php.yii.editor.YiiHyperlinkProviderExt.DEFAULT_OFFSET; import org.nbphpcouncil.modules.php.yii.editor.navi.GoToDefaultItem; import org.nbphpcouncil.modules.php.yii.editor.navi.GoToItem; import org.nbphpcouncil.modules.php.yii.editor.navi.GoToPopup; import org.nbphpcouncil.modules.php.yii.editor.navi.GoToTItem; import org.nbphpcouncil.modules.php.yii.editor.navi.PopupUtil; import org.nbphpcouncil.modules.php.yii.util.YiiDocUtils; import org.nbphpcouncil.modules.php.yii.util.YiiPathAliasSupport; import org.nbphpcouncil.modules.php.yii.util.YiiUtils; import org.nbphpcouncil.modules.php.yii.util.YiiViewPathSupport; import org.netbeans.api.editor.EditorRegistry; import org.netbeans.api.editor.mimelookup.MimeRegistration; import org.netbeans.api.lexer.Token; import org.netbeans.api.lexer.TokenSequence; import org.netbeans.lib.editor.hyperlink.spi.HyperlinkProviderExt; import org.netbeans.lib.editor.hyperlink.spi.HyperlinkType; import org.netbeans.modules.csl.api.UiUtils; import org.netbeans.modules.csl.spi.ParserResult; import org.netbeans.modules.editor.NbEditorUtilities; import org.netbeans.modules.parsing.api.ParserManager; import org.netbeans.modules.parsing.api.ResultIterator; import org.netbeans.modules.parsing.api.Source; import org.netbeans.modules.parsing.api.UserTask; import org.netbeans.modules.parsing.spi.ParseException; import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport; import org.netbeans.modules.php.api.phpmodule.PhpModule; import org.netbeans.modules.php.api.util.FileUtils; import org.netbeans.modules.php.api.util.StringUtils; import org.netbeans.modules.php.editor.api.ElementQuery; import org.netbeans.modules.php.editor.api.ElementQueryFactory; import org.netbeans.modules.php.editor.api.NameKind; import org.netbeans.modules.php.editor.api.QuerySupportFactory; import org.netbeans.modules.php.editor.api.elements.ClassElement; import org.netbeans.modules.php.editor.lexer.PHPTokenId; import org.netbeans.modules.php.editor.parser.api.Utils; import org.netbeans.modules.php.editor.parser.astnodes.ArrayElement; import org.netbeans.modules.php.editor.parser.astnodes.Expression; import org.netbeans.modules.php.editor.parser.astnodes.Scalar; import org.netbeans.modules.php.editor.parser.astnodes.visitors.DefaultVisitor; import org.openide.filesystems.FileObject; import org.openide.util.Exceptions; import org.openide.util.NbBundle; /** * * @author junichi11 */ @MimeRegistration(mimeType = FileUtils.PHP_MIME_TYPE, service = HyperlinkProviderExt.class) public class YiiGoToFileHyperlinkProvider extends YiiHyperlinkProviderExt { private static final Set<String> methods = new HashSet<>(); private static final String DEFAULT_MESSAGES_DIR_NAME = "messages"; // NOI18N private List<GoToItem> goToItems; private int paramCount; private static final Logger LOGGER = Logger.getLogger(YiiGoToFileHyperlinkProvider.class.getName()); static { methods.add("widget"); // NOI18N methods.add("beginWidget"); // NOI18N methods.add("endWidget"); // NOI18N methods.add("createWidget"); // NOI18N methods.add("import"); // NOI18N methods.add("beginCache"); // NOI18N methods.add("beginContent"); // NOI18N methods.add("t"); // NOI18N } @Override public void performClickAction(Document doc, int offset, HyperlinkType type) { // open file if (goToItems.size() == 1) { GoToItem item = goToItems.get(0); UiUtils.open(item.getFileObject(), DEFAULT_OFFSET); } // i18n JTextComponent editor = EditorRegistry.lastFocusedComponent(); // show popup if (editor != null && !goToItems.isEmpty()) { try { Rectangle rectangle = editor.modelToView(offset); final Point point = new Point(rectangle.x, rectangle.y + rectangle.height); SwingUtilities.convertPointToScreen(point, editor); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { PopupUtil.showPopup(new GoToPopup(" Select LocaleID ", goToItems), " Select LocaleID ", point.x, point.y, true, 0); } }); } catch (BadLocationException ex) { Exceptions.printStackTrace(ex); } } } @Override protected boolean verifyState(Document doc, int offset, HyperlinkType type) { // get TokenSequence TokenSequence<PHPTokenId> ts = YiiDocUtils.getTokenSequence(doc); if (ts == null) { return false; } // get current positon text target = getCurrentPositionString(ts, offset); if (target == null) { return false; } // set span targetStart = ts.offset() + 1; targetEnd = targetStart + target.length(); // create go to items PhpModule phpModule = PhpModule.Factory.inferPhpModule(); goToItems = createGoToItems(phpModule, doc, offset); return !goToItems.isEmpty(); } @Override @NbBundle.Messages({ "LBL.NotFoundFile=Not found file", "LBL.I18N.Message=If you click, go to list is displayed" }) public String getTooltipText(Document doc, int offset, HyperlinkType type) { if (goToItems.size() == 1) { return goToItems.get(0).getFileObject().getPath(); } if (!goToItems.isEmpty()) { return Bundle.LBL_I18N_Message(); } return Bundle.LBL_NotFoundFile(); } /** * Get method name of current caret position. * * @param ts TokenSequence * @return method name. */ private String getMethodName(TokenSequence<PHPTokenId> ts) { String name = null; paramCount = 1; while (ts.movePrevious()) { Token<PHPTokenId> token = ts.token(); String text = token.text().toString(); PHPTokenId id = token.id(); if (id == PHPTokenId.PHP_TOKEN && text.equals(",")) { // NOI18N // XXX this is not exact paramCount++; continue; } if (id == PHPTokenId.PHP_SEMICOLON || id == PHPTokenId.PHP_FUNCTION || id == PHPTokenId.PHP_OPENTAG) { break; } if (id == PHPTokenId.PHP_STRING) { name = text; break; } } return name; } /** * Get class file if target is class name. * * @param phpModule PhpModule * @return FileObject for class if file exists, otherwise null. */ private FileObject getClassFileObject(PhpModule phpModule, String target) { if (!StringUtils.isEmpty(target) && !target.contains(" ")) { // NOI18N ElementQuery.Index indexQuery = ElementQueryFactory.createIndexQuery(QuerySupportFactory.get(phpModule.getSourceDirectory())); Set<ClassElement> classElements = indexQuery.getClasses(NameKind.create(target, QuerySupport.Kind.EXACT)); for (ClassElement element : classElements) { if (element.getName().equals(target)) { return element.getFileObject(); } } } return null; } /** * Create GoToItems. * * @param phpModule * @param doc * @param offset * @return GoToItem list */ private List<GoToItem> createGoToItems(PhpModule phpModule, Document doc, int offset) { List<GoToItem> items = new ArrayList<>(); // check whether target is class name FileObject classFile = getClassFileObject(phpModule, target); if (classFile != null) { items.add(new GoToDefaultItem(classFile, DEFAULT_OFFSET)); return items; } // get method name TokenSequence<PHPTokenId> ts = YiiDocUtils.getTokenSequence(doc); ts.move(offset); ts.moveNext(); String methodName = getMethodName(ts); if (!methods.contains(methodName)) { return items; } // for i18n if (methodName.equals("t")) { // NOI18N return createGoToTItems(phpModule, ts, offset); } // for absolute view path FileObject file = null; if (YiiViewPathSupport.isAbsoluteViewPath(target)) { // for application's view path FileObject currentFile = NbEditorUtilities.getFileObject(doc); file = YiiViewPathSupport.getAbsoluteViewFile(target, currentFile); } // for path alias if (file == null) { file = YiiPathAliasSupport.getFileObject(phpModule, target); } if (file != null && file.isData()) { items.add(new GoToDefaultItem(file, DEFAULT_OFFSET)); } return items; } /** * Create GoToTItems. * * @param phpModule * @return true if item list is not empty, otherwise false. */ private List<GoToItem> createGoToTItems(PhpModule phpModule, TokenSequence<PHPTokenId> ts, int offset) { List<GoToItem> items = new ArrayList<>(); YiiModule yiiModule = YiiModuleFactory.create(phpModule); FileObject application = yiiModule.getApplication(); if (application == null || paramCount > 2) { return items; } // move to offset of message for second parameter String message = ""; // NOI18N if (paramCount == 2) { String tFirstParam = getTFirstParam(ts, offset); if (tFirstParam == null) { return items; } message = target; target = tFirstParam; } // get messages directory FileObject messagesDirectory = null; String messagesDirectoryName = DEFAULT_MESSAGES_DIR_NAME; // check whether text contains class name String className = ""; // NOI18N if (target.contains(".")) { // NOI18N int dotIndex = target.indexOf("."); // NOI18N className = target.substring(0, dotIndex); String categoryName = target.substring(dotIndex + 1); target = categoryName; FileObject classFile = getClassFileObject(phpModule, className); if (classFile != null) { FileObject parent = classFile.getParent(); if (parent != null && parent.isFolder()) { messagesDirectory = parent.getFileObject(messagesDirectoryName); } } } if (className.isEmpty() && messagesDirectory == null) { messagesDirectory = yiiModule.getMessages(); } if (messagesDirectory == null) { return items; } // sort FileObject[] children = messagesDirectory.getChildren(); YiiUtils.sort(children); // add item for (FileObject child : children) { // create GoToItem int messageOffset = DEFAULT_OFFSET; final Set<Integer> mOffset = new HashSet<>(); final String messageKey = message; FileObject messageFile = child.getFileObject(target + ".php"); // NOI18N if (messageFile != null) { // click second parameter if (!StringUtils.isEmpty(messageKey)) { // get offset for massage try { ParserManager.parse(Collections.singleton(Source.create(messageFile)), new UserTask() { @Override public void run(ResultIterator resultIterator) throws Exception { ParserResult parseResult = (ParserResult) resultIterator.getParserResult(); if (parseResult == null) { return; } final MessageVisitor messageVisitor = new MessageVisitor(messageKey); messageVisitor.scan(Utils.getRoot(parseResult)); mOffset.add(messageVisitor.getOffset()); } }); } catch (ParseException ex) { LOGGER.log(Level.WARNING, null, ex); } } // set offset if (!mOffset.isEmpty()) { for (Integer o : mOffset) { messageOffset = o.intValue(); break; } } items.add(new GoToTItem(messageFile, messageOffset, child.getName())); } } return items; } /** * Get first parameter for t method. It is valid for only second parameter. * * @param ts TokenSequence * @param offset current offset * @return first parameter for t method. */ private String getTFirstParam(TokenSequence<PHPTokenId> ts, int offset) { if (paramCount == 2) { ts.move(offset); ts.moveNext(); // search method while (ts.movePrevious()) { Token<PHPTokenId> token = ts.token(); if (token.id() == PHPTokenId.PHP_STRING) { break; } } // search first parameter while (ts.moveNext()) { Token<PHPTokenId> token = ts.token(); PHPTokenId id = token.id(); if (id == PHPTokenId.PHP_SEMICOLON) { break; } if (id == PHPTokenId.PHP_CONSTANT_ENCAPSED_STRING) { String text = token.text().toString(); return text.substring(1, text.length() - 1); } } } return null; } //~ inner class private class MessageVisitor extends DefaultVisitor { private int offset; private final String message; public MessageVisitor(String message) { this.message = message; } @Override public void visit(ArrayElement node) { super.visit(node); Expression key = node.getKey(); if (key instanceof Scalar) { Scalar s = (Scalar) key; if (s.getScalarType() == Scalar.Type.STRING) { String keyValue = s.getStringValue(); keyValue = keyValue.substring(1, keyValue.length() - 1); if (keyValue.equals(message)) { offset = node.getStartOffset(); } } } } public int getOffset() { return offset; } } }