/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2010 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]" * * Contributor(s): * * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun * Microsystems, Inc. All Rights Reserved. * * 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. */ package org.netbeans.modules.refactoring.ruby.ui; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Dictionary; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JOptionPane; import javax.swing.text.Document; import javax.swing.text.JTextComponent; import org.netbeans.api.fileinfo.NonRecursiveFolder; import org.netbeans.editor.BaseDocument; import org.netbeans.modules.csl.spi.ParserResult; import org.netbeans.modules.parsing.api.Embedding; 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.refactoring.ruby.RetoucheUtils; import org.netbeans.modules.refactoring.ruby.RubyElementCtx; import org.netbeans.modules.refactoring.spi.ui.ActionsImplementationProvider; import org.netbeans.modules.refactoring.spi.ui.RefactoringUI; import org.netbeans.modules.refactoring.spi.ui.UI; import org.netbeans.modules.ruby.AstUtilities; import org.netbeans.modules.ruby.RubyParseResult; import org.netbeans.modules.ruby.RubyStructureAnalyzer.AnalysisResult; import org.netbeans.modules.ruby.RubyUtils; import org.netbeans.modules.ruby.elements.AstElement; import org.netbeans.modules.ruby.elements.Element; import org.netbeans.modules.ruby.lexer.LexUtilities; import org.openide.cookies.EditorCookie; import org.openide.filesystems.FileObject; import org.openide.loaders.DataObject; import org.openide.nodes.Node; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.windows.TopComponent; /** * * @author Jan Becicka */ @org.openide.util.lookup.ServiceProvider(service=org.netbeans.modules.refactoring.spi.ui.ActionsImplementationProvider.class, position=100) public class RefactoringActionsProvider extends ActionsImplementationProvider { private static final Logger LOG = Logger.getLogger(RefactoringActionsProvider.class.getName()); private static boolean isFindUsages; /** Creates a new instance of RefactoringActionsProvider */ public RefactoringActionsProvider() { } @Override public void doRename(final Lookup lookup) { Runnable task; EditorCookie ec = lookup.lookup(EditorCookie.class); final Dictionary dictionary = lookup.lookup(Dictionary.class); if (isFromEditor(ec)) { task = new TextComponentTask(ec) { @Override protected RefactoringUI createRefactoringUI(RubyElementCtx selectedElement,int startOffset,int endOffset, final ParserResult parserResult) { // If you're trying to rename a constructor, rename the enclosing class instead return new RenameRefactoringUI(selectedElement, parserResult); } }; } else { task = new NodeToFileObjectTask(lookup.lookupAll(Node.class)) { @Override protected RefactoringUI createRefactoringUI(FileObject[] selectedElements, Collection<RubyElementCtx> handles) { String newName = getName(dictionary); if (newName != null) { if (pkg[0] != null) { return new RenameRefactoringUI(pkg[0], newName); } else { return new RenameRefactoringUI(selectedElements[0], newName, handles == null || handles.isEmpty() ? null : handles.iterator().next(), cinfo == null ? null : cinfo.get()); } } else if (pkg[0] != null) { return new RenameRefactoringUI(pkg[0]); } else { return new RenameRefactoringUI(selectedElements[0], handles == null || handles.isEmpty() ? null : handles.iterator().next(), cinfo == null ? null : cinfo.get()); } } }; } task.run(); } /** * returns true if exactly one refactorable file is selected */ @Override public boolean canRename(Lookup lookup) { Collection<? extends Node> nodes = lookup.lookupAll(Node.class); if (nodes.size() != 1) { return false; } Node n = nodes.iterator().next(); DataObject dob = n.getCookie(DataObject.class); if (dob==null) { return false; } FileObject fo = dob.getPrimaryFile(); if (isOutsideRuby(lookup, fo)) { return false; } if (RetoucheUtils.isRefactorable(fo)) { //NOI18N return true; } return false; } /** * returns true if exactly one refactorable file is selected */ @Override public boolean canCopy(Lookup lookup) { return false; } private boolean isOutsideRuby(Lookup lookup, FileObject fo) { if (RubyUtils.isRhtmlOrYamlFile(fo)) { // We're attempting to refactor in an RHTML file... If it's in // the editor, make sure we're trying to refactoring in a Ruby section; // if not, we shouldn't grab it. (JavaScript refactoring won't get // invoked if Ruby returns true for canRename even when the caret is // in the caret section EditorCookie ec = lookup.lookup(EditorCookie.class); if (isFromEditor(ec)) { // TODO - use editor registry JTextComponent textC = ec.getOpenedPanes()[0]; Document d = textC.getDocument(); if (!(d instanceof BaseDocument)) { return true; } int caret = textC.getCaretPosition(); if (LexUtilities.getToken((BaseDocument)d, caret) == null) { // Not in Ruby code! return true; } } } return false; } @Override public boolean canFindUsages(Lookup lookup) { Collection<? extends Node> nodes = lookup.lookupAll(Node.class); if (nodes.size() != 1) { return false; } Node n = nodes.iterator().next(); DataObject dob = n.getCookie(DataObject.class); if (dob == null) { return false; } FileObject fo = dob.getPrimaryFile(); if (isOutsideRuby(lookup, fo)) { return false; } if ((dob!=null) && RubyUtils.canContainRuby(fo)) { //NOI18N return true; } return false; } @Override public void doFindUsages(Lookup lookup) { Runnable task; EditorCookie ec = lookup.lookup(EditorCookie.class); if (isFromEditor(ec)) { task = new TextComponentTask(ec) { @Override protected RefactoringUI createRefactoringUI(RubyElementCtx selectedElement,int startOffset,int endOffset, ParserResult parserResult) { return new WhereUsedQueryUI(selectedElement, parserResult); } }; } else { task = new NodeToElementTask(lookup.lookupAll(Node.class)) { protected RefactoringUI createRefactoringUI(RubyElementCtx selectedElement, ParserResult parserResult) { return new WhereUsedQueryUI(selectedElement, parserResult); } }; } try { isFindUsages = true; task.run(); } finally { isFindUsages = false; } } @Override public boolean canDelete(Lookup lookup) { return false; } static String getName(Dictionary dict) { if (dict==null) return null; return (String) dict.get("name"); //NOI18N } @Override public boolean canMove(Lookup lookup) { return false; } @Override public void doMove(final Lookup lookup) { } public static abstract class TextComponentTask extends UserTask implements Runnable { private final JTextComponent textC; private final int caret; private final int start; private final int end; private RefactoringUI ui; public TextComponentTask(EditorCookie ec) { this.textC = ec.getOpenedPanes()[0]; this.caret = textC.getCaretPosition(); this.start = textC.getSelectionStart(); this.end = textC.getSelectionEnd(); assert caret != -1; assert start != -1; assert end != -1; } public void run(ResultIterator ri) throws Exception { if (ri.getSnapshot().getMimeType().equals(RubyUtils.RUBY_MIME_TYPE)) { ParserResult parserResult = AstUtilities.getParseResult(ri.getParserResult()); org.jrubyparser.ast.Node root = AstUtilities.getRoot(parserResult); if (root != null) { RubyElementCtx ctx = new RubyElementCtx(parserResult, caret); if (ctx.getSimpleName() != null) { ui = createRefactoringUI(ctx, start, end, parserResult); } } else { // TODO How do I add some kind of error message? System.out.println("FAILURE - can't refactor uncompileable sources"); } } else { for (Embedding e : ri.getEmbeddings()) { run(ri.getResultIterator(e)); } } } public final void run() { try { Source source = Source.create(textC.getDocument()); ParserManager.parse(Collections.singleton(source), this); } catch (ParseException e) { LOG.log(Level.WARNING, null, e); return ; } TopComponent activetc = TopComponent.getRegistry().getActivated(); if (ui!=null) { // XXX - Parsing API // if (fo != null) { // ClasspathInfo classpathInfoFor = RetoucheUtils.getClasspathInfoFor(fo); // if (classpathInfoFor == null) { // JOptionPane.showMessageDialog(null, NbBundle.getMessage(RefactoringActionsProvider.class, "ERR_CannotFindClasspath")); // return; // } // } UI.openRefactoringUI(ui, activetc); } else { String key = "ERR_CannotRenameLoc"; // NOI18N if (isFindUsages) { key = "ERR_CannotFindUsages"; // NOI18N } JOptionPane.showMessageDialog(null,NbBundle.getMessage(RefactoringActionsProvider.class, key)); } } protected abstract RefactoringUI createRefactoringUI(RubyElementCtx selectedElement, int startOffset, int endOffset, ParserResult info); } public static abstract class NodeToElementTask extends UserTask implements Runnable { private final Node node; private RefactoringUI ui; public NodeToElementTask(Collection<? extends Node> nodes) { assert nodes.size() == 1; this.node = nodes.iterator().next(); } public void run(ResultIterator ri) throws ParseException { if (ri.getSnapshot().getMimeType().equals(RubyUtils.RUBY_MIME_TYPE)) { ParserResult parserResult = AstUtilities.getParseResult(ri.getParserResult()); org.jrubyparser.ast.Node root = AstUtilities.getRoot(parserResult); if (root != null) { Element element = AstElement.create(parserResult, root); RubyElementCtx fileCtx = new RubyElementCtx(root, root, element, RubyUtils.getFileObject(parserResult), parserResult); ui = createRefactoringUI(fileCtx, parserResult); } } else { for (Embedding e : ri.getEmbeddings()) { run(ri.getResultIterator(e)); } } } public final void run() { try { DataObject o = node.getCookie(DataObject.class); Source source = Source.create(o.getPrimaryFile()); ParserManager.parse(Collections.singleton(source), this); } catch (ParseException e) { LOG.log(Level.WARNING, null, e); return ; } if (ui != null) { UI.openRefactoringUI(ui); } else { String key = "ERR_CannotRenameLoc"; // NOI18N if (isFindUsages) { key = "ERR_CannotFindUsages"; // NOI18N } JOptionPane.showMessageDialog(null, NbBundle.getMessage(RefactoringActionsProvider.class, key)); } } protected abstract RefactoringUI createRefactoringUI(RubyElementCtx selectedElement, ParserResult info); } public static abstract class NodeToFileObjectTask extends UserTask implements Runnable { private final Collection<? extends Node> nodes; // private RefactoringUI ui; public final NonRecursiveFolder pkg[]; public WeakReference<ParserResult> cinfo; Collection<RubyElementCtx> handles = new ArrayList<RubyElementCtx>(); public NodeToFileObjectTask(Collection<? extends Node> nodes) { assert nodes != null; this.nodes = nodes; this.pkg = new NonRecursiveFolder[nodes.size()]; } public void cancel() { } public void run(ResultIterator ri) throws ParseException { if (ri.getSnapshot().getMimeType().equals(RubyUtils.RUBY_MIME_TYPE)) { RubyParseResult parserResult = AstUtilities.getParseResult(ri.getParserResult()); org.jrubyparser.ast.Node root = AstUtilities.getRoot(parserResult); if (root != null) { if (parserResult != null) { AnalysisResult ar = parserResult.getStructure(); List<? extends AstElement> els = ar.getElements(); if (els.size() > 0) { // TODO - try to find the outermost or most "relevant" module/class in the file? // In Java, we look for a class with the name corresponding to the file. // It's not as simple in Ruby. AstElement element = els.get(0); org.jrubyparser.ast.Node node = element.getNode(); RubyElementCtx representedObject = new RubyElementCtx( root, node, element, RubyUtils.getFileObject(parserResult), parserResult); representedObject.setNames(element.getFqn(), element.getName()); handles.add(representedObject); } } } cinfo = new WeakReference<ParserResult>(parserResult); } } public void run() { FileObject[] fobs = new FileObject[nodes.size()]; int i = 0; for (Node node : nodes) { DataObject dob = node.getCookie(DataObject.class); if (dob != null) { fobs[i] = dob.getPrimaryFile(); Source source = Source.create(fobs[i]); try { ParserManager.parse(Collections.singleton(source), this); } catch (ParseException ex) { LOG.log(Level.WARNING, null, ex); } pkg[i++] = node.getLookup().lookup(NonRecursiveFolder.class); } } UI.openRefactoringUI(createRefactoringUI(fobs, handles)); } protected abstract RefactoringUI createRefactoringUI(FileObject[] selectedElement, Collection<RubyElementCtx> handles); } static boolean isFromEditor(EditorCookie ec) { if (ec != null && ec.getOpenedPanes() != null) { // This doesn't seem to work well - a lot of the time, I'm right clicking // on the editor and it still has another activated view (this is on the mac) // and as a result does file-oriented refactoring rather than the specific // editor node... // TopComponent activetc = TopComponent.getRegistry().getActivated(); // if (activetc instanceof CloneableEditorSupport.Pane) { // return true; // } } return false; } }