ruby.refactoring/src/org/netbeans/modules/refactoring/ruby/plugins/RenameRefactoringPlugin.java
author enebo@netbeans.org
Tue, 22 Apr 2014 15:36:21 -0500
changeset 4559 7a0a8afa3e90
parent 4542 be97a5e85907
permissions -rw-r--r--
Bump jruby-parser and hopefully see green (commented out tests pass individually -- some state surviving to kill them later -- workaround for now)
     1 /*
     2  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     3  *
     4  * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
     5  *
     6  * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
     7  * Other names may be trademarks of their respective owners.
     8  *
     9  * The contents of this file are subject to the terms of either the GNU
    10  * General Public License Version 2 only ("GPL") or the Common
    11  * Development and Distribution License("CDDL") (collectively, the
    12  * "License"). You may not use this file except in compliance with the
    13  * License. You can obtain a copy of the License at
    14  * http://www.netbeans.org/cddl-gplv2.html
    15  * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
    16  * specific language governing permissions and limitations under the
    17  * License.  When distributing the software, include this License Header
    18  * Notice in each file and include the License file at
    19  * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
    20  * particular file as subject to the "Classpath" exception as provided
    21  * by Oracle in the GPL Version 2 section of the License file that
    22  * accompanied this code. If applicable, add the following below the
    23  * License Header, with the fields enclosed by brackets [] replaced by
    24  * your own identifying information:
    25  * "Portions Copyrighted [year] [name of copyright owner]"
    26  *
    27  * Contributor(s):
    28  *
    29  * The Original Software is NetBeans. The Initial Developer of the Original
    30  * Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
    31  * Microsystems, Inc. All Rights Reserved.
    32  *
    33  * If you wish your version of this file to be governed by only the CDDL
    34  * or only the GPL Version 2, indicate your decision by adding
    35  * "[Contributor] elects to include this software in this distribution
    36  * under the [CDDL or GPL Version 2] license." If you do not indicate a
    37  * single choice of license, a recipient has the option to distribute
    38  * your version of this file under either the CDDL, the GPL Version 2 or
    39  * to extend the choice of license to its licensees as provided above.
    40  * However, if you add GPL Version 2 code and therefore, elected the GPL
    41  * Version 2 license, then the option applies only if the new code is
    42  * made subject to such option by the copyright holder.
    43  */
    44 package org.netbeans.modules.refactoring.ruby.plugins;
    45 
    46 import java.io.IOException;
    47 import java.text.MessageFormat;
    48 import java.util.*;
    49 import java.util.logging.Level;
    50 import java.util.logging.Logger;
    51 import javax.swing.text.BadLocationException;
    52 import javax.swing.text.Document;
    53 import javax.swing.text.Position.Bias;
    54 import org.jrubyparser.ast.ArgumentNode;
    55 import org.jrubyparser.ast.ClassNode;
    56 import org.jrubyparser.ast.ClassVarAsgnNode;
    57 import org.jrubyparser.ast.ClassVarDeclNode;
    58 import org.jrubyparser.ast.ClassVarNode;
    59 import org.jrubyparser.ast.Colon2Node;
    60 import org.jrubyparser.ast.DAsgnNode;
    61 import org.jrubyparser.ast.DVarNode;
    62 import org.jrubyparser.ast.GlobalAsgnNode;
    63 import org.jrubyparser.ast.GlobalVarNode;
    64 import org.jrubyparser.ast.InstAsgnNode;
    65 import org.jrubyparser.ast.InstVarNode;
    66 import org.jrubyparser.ast.LocalAsgnNode;
    67 import org.jrubyparser.ast.LocalVarNode;
    68 import org.jrubyparser.ast.MethodDefNode;
    69 import org.jrubyparser.ast.ModuleNode;
    70 import org.jrubyparser.ast.Node;
    71 import org.jrubyparser.ast.SClassNode;
    72 import org.jrubyparser.ast.SymbolNode;
    73 import org.jrubyparser.ast.INameNode;
    74 import org.netbeans.api.lexer.Token;
    75 import org.netbeans.api.lexer.TokenHierarchy;
    76 import org.netbeans.api.lexer.TokenId;
    77 import org.netbeans.api.lexer.TokenSequence;
    78 import org.netbeans.api.lexer.TokenUtilities;
    79 import org.netbeans.api.project.FileOwnerQuery;
    80 import org.netbeans.api.project.Project;
    81 import org.netbeans.editor.BaseDocument;
    82 import org.netbeans.editor.Utilities;
    83 import org.netbeans.modules.csl.api.ElementKind;
    84 import org.netbeans.modules.csl.api.Error;
    85 import org.netbeans.modules.csl.api.OffsetRange;
    86 import org.netbeans.modules.csl.api.Severity;
    87 import org.netbeans.modules.csl.spi.ParserResult;
    88 import org.netbeans.modules.csl.spi.support.ModificationResult;
    89 import org.netbeans.modules.csl.spi.support.ModificationResult.Difference;
    90 import org.netbeans.modules.parsing.api.ParserManager;
    91 import org.netbeans.modules.parsing.api.ResultIterator;
    92 import org.netbeans.modules.parsing.api.Source;
    93 import org.netbeans.modules.parsing.api.UserTask;
    94 import org.netbeans.modules.parsing.spi.ParseException;
    95 import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport.Kind;
    96 import org.netbeans.modules.refactoring.api.*;
    97 import org.netbeans.modules.refactoring.ruby.DiffElement;
    98 import org.netbeans.modules.refactoring.ruby.RetoucheUtils;
    99 import org.netbeans.modules.refactoring.ruby.RubyElementCtx;
   100 import org.netbeans.modules.refactoring.spi.RefactoringElementsBag;
   101 import org.netbeans.modules.ruby.AstPath;
   102 import org.netbeans.modules.ruby.AstUtilities;
   103 import org.netbeans.modules.ruby.RubyIndex;
   104 import org.netbeans.modules.ruby.RubyParseResult;
   105 import org.netbeans.modules.ruby.RubyStructureAnalyzer.AnalysisResult;
   106 import org.netbeans.modules.ruby.RubyUtils;
   107 import org.netbeans.modules.ruby.elements.AstElement;
   108 import org.netbeans.modules.ruby.elements.Element;
   109 import org.netbeans.modules.ruby.elements.IndexedClass;
   110 import org.netbeans.modules.ruby.elements.IndexedElement;
   111 import org.netbeans.modules.ruby.elements.IndexedMethod;
   112 import org.netbeans.modules.ruby.lexer.LexUtilities;
   113 import org.netbeans.modules.ruby.rubyproject.RubyBaseProject;
   114 import org.openide.filesystems.FileObject;
   115 import org.openide.filesystems.FileUtil;
   116 import org.openide.loaders.OperationEvent.Rename;
   117 import org.openide.text.CloneableEditorSupport;
   118 import org.openide.text.PositionRef;
   119 import org.openide.util.Exceptions;
   120 import org.openide.util.NbBundle;
   121 
   122 /**
   123  * The actual Renaming refactoring work for Ruby. The skeleton (name checks etc.) based
   124  * on the Java refactoring module by Jan Becicka, Martin Matula, Pavel Flaska and Daniel Prusa.
   125  * 
   126  * @author Jan Becicka
   127  * @author Martin Matula
   128  * @author Pavel Flaska
   129  * @author Daniel Prusa
   130  * @author Tor Norbye
   131  * 
   132  * @todo Perform index lookups to determine the set of files to be checked!
   133  * @todo Check that the new name doesn't conflict with an existing name
   134  * @todo Check unknown files!
   135  * @todo More prechecks
   136  * @todo When invoking refactoring on a file object, I also rename the file. I should (a) list the
   137  *   name it's going to change the file to, and (b) definitely "filenamize" it - e.g. for class FooBar the
   138  *   filename should be foo_bar.
   139  * @todo If you rename a Model, I should add a corresponding rename_table entry in the migrations...
   140  *
   141  * @todo Complete this. Most of the prechecks are not implemented - and the refactorings themselves need a lot of work.
   142  */
   143 public class RenameRefactoringPlugin extends RubyRefactoringPlugin {
   144     
   145     private RubyElementCtx treePathHandle;
   146     private final Collection<IndexedMethod> overriddenByMethods = new ArrayList<IndexedMethod>();
   147     private final Collection<IndexedMethod> overridesMethods = new ArrayList<IndexedMethod>();; // methods that are overridden by the method to be renamed
   148 //    private boolean doCheckName = true;
   149     
   150     private RenameRefactoring refactoring;
   151     private RubyBaseProject project;
   152     
   153     /** Creates a new instance of RenameRefactoring */
   154     public RenameRefactoringPlugin(RenameRefactoring rename) {
   155         this.refactoring = rename;
   156         RubyElementCtx tph = rename.getRefactoringSource().lookup(RubyElementCtx.class);
   157         if (tph != null) {
   158             treePathHandle = tph;
   159         } else {
   160             Source source = Source.create(rename.getRefactoringSource().lookup(FileObject.class));
   161             try {
   162                 ParserManager.parse(Collections.singleton(source), new UserTask() {
   163 
   164                     public
   165                     @Override
   166                     void run(ResultIterator co) throws Exception {
   167                         if (co.getSnapshot().getMimeType().equals(RubyUtils.RUBY_MIME_TYPE)) {
   168                             RubyParseResult parserResult = AstUtilities.getParseResult(co.getParserResult());
   169                             org.jrubyparser.ast.Node root = parserResult.getRootNode();
   170                             if (root != null) {
   171                                 AnalysisResult ar = parserResult.getStructure();
   172                                 List<? extends AstElement> els = ar.getElements();
   173                                 if (els.size() > 0) {
   174                                     // TODO - try to find the outermost or most "relevant" module/class in the file?
   175                                     // In Java, we look for a class with the name corresponding to the file.
   176                                     // It's not as simple in Ruby.
   177                                     AstElement element = els.get(0);
   178                                     org.jrubyparser.ast.Node node = element.getNode();
   179                                     treePathHandle = new RubyElementCtx(root, node,
   180                                             element, RubyUtils.getFileObject(parserResult), parserResult);
   181                                     refactoring.getContext().add(co);
   182                                 }
   183                             }
   184                         }
   185                     }
   186                 });
   187             } catch (ParseException e) {
   188                 Logger.getLogger(RenameRefactoringPlugin.class.getName()).log(Level.WARNING, null, e);
   189             }
   190         }
   191         if (treePathHandle != null) {
   192             Project p = FileOwnerQuery.getOwner(treePathHandle.getFileObject());
   193             if (p instanceof RubyBaseProject) {
   194                 project = (RubyBaseProject) p;
   195             }
   196         }
   197     }
   198 
   199     public Problem fastCheckParameters() {
   200         Problem fastCheckProblem = null;
   201         ElementKind kind = treePathHandle.getKind();
   202         String newName = refactoring.getNewName();
   203         String oldName = treePathHandle.getSimpleName();
   204         if (oldName == null) {
   205             return new Problem(true, "Cannot determine target name. Please file a bug with detailed information on how to reproduce (preferably including the current source file and the cursor position)");
   206         }
   207         
   208         if (oldName.equals(newName)) {
   209             boolean nameNotChanged = true;
   210             //if (kind == ElementKind.CLASS || kind == ElementKind.MODULE) {
   211             //    if (!((TypeElement) element).getNestingKind().isNested()) {
   212             //        nameNotChanged = info.getFileObject().getName().equals(element);
   213             //    }
   214             //}
   215             if (nameNotChanged) {
   216                 fastCheckProblem = createProblem(fastCheckProblem, true, getString("ERR_NameNotChanged"));
   217                 return fastCheckProblem;
   218             }
   219             
   220         }
   221         
   222         // TODO - get a better ruby name picker - and check for invalid Ruby symbol names etc.
   223         // TODO - call RubyUtils.isValidLocalVariableName if we're renaming a local symbol!
   224         if (kind == ElementKind.CLASS && !RubyUtils.isValidConstantFQN(newName)) {
   225             String s = getString("ERR_InvalidClassName"); //NOI18N
   226             String msg = new MessageFormat(s).format(
   227                     new Object[] {newName}
   228             );
   229             fastCheckProblem = createProblem(fastCheckProblem, true, msg);
   230             return fastCheckProblem;
   231         } else if (kind == ElementKind.METHOD && !RubyUtils.isValidRubyMethodName(newName)) {
   232             String s = getString("ERR_InvalidMethodName"); //NOI18N
   233             String msg = new MessageFormat(s).format(
   234                     new Object[] {newName}
   235             );
   236             fastCheckProblem = createProblem(fastCheckProblem, true, msg);
   237             return fastCheckProblem;
   238         } else if (!RubyUtils.isValidRubyIdentifier(newName)) {
   239             String s = getString("ERR_InvalidIdentifier"); //NOI18N
   240             String msg = new MessageFormat(s).format(
   241                     new Object[] {newName}
   242             );
   243             fastCheckProblem = createProblem(fastCheckProblem, true, msg);
   244             return fastCheckProblem;
   245         }
   246         String msg = getWarningMsg(kind, newName);
   247         if (msg != null) {
   248             fastCheckProblem = createProblem(fastCheckProblem, false, msg);
   249         }
   250         
   251         return fastCheckProblem;
   252     }
   253 
   254     private Set<String> asNames(Collection<? extends IndexedElement> elems) {
   255         Set<String> names = new HashSet<String>(elems.size());
   256         for (IndexedElement each : elems) {
   257             names.add(each.getName());
   258         }
   259         return names;
   260     }
   261 
   262     public Problem checkParameters() {
   263         
   264         Problem checkProblem = null;
   265         int steps = 0;
   266         if (AstUtilities.isCall(treePathHandle.getNode()) || treePathHandle.getKind() == ElementKind.METHOD) {
   267             RubyIndex index = RubyIndex.get(treePathHandle.getInfo());
   268             String className = treePathHandle.getDefClass();
   269             String methodName = AstUtilities.getName(treePathHandle.getNode());
   270             Set<IndexedMethod> methodsInSameTree = index.getAllOverridingMethodsInHierachy(methodName, className);
   271             overridesMethods.addAll(methodsInSameTree);
   272 
   273             // inherited contains also the method itself
   274             if (overridesMethods.size() > 1) {
   275                 Set<String> superClassNames = asNames(index.getSuperClasses(className));
   276                 // does the method override a super method that is defined in a class in the project sources
   277                 boolean overridesFromSources = false;
   278                 // does the method overrided a super method that is also overridden in a class in a 
   279                 // different branch of the class hierarhcy
   280                 boolean classesInOtherBranch = false;
   281 
   282                 for (IndexedMethod method : overridesMethods) {
   283                     // warn about matches under non-source roots (we don't rename them)
   284                     if (!isUnderSourceRoot(method.getFileObject())) {
   285                         checkProblem =
   286                                 createProblem(checkProblem,
   287                                 false, NbBundle.getMessage(RenameRefactoringPlugin.class, "ERR_Overrides_Method",
   288                                 method.getIn() + "#" + method.getName(), method.getFileObject().getPath()));
   289                     } else if (!method.getFileObject().equals(treePathHandle.getFileObject())){
   290                         overridesFromSources = true;
   291                     }
   292                     if (!classesInOtherBranch 
   293                             && !className.equals(method.getIn())
   294                             && !superClassNames.contains(method.getIn())) {
   295                         classesInOtherBranch = true;
   296                     }
   297                 }
   298                 if (overridesFromSources) {
   299                     checkProblem = createProblem(checkProblem, false, NbBundle.getMessage(RenameRefactoringPlugin.class, "ERR_Overrides"));
   300                 }
   301                 if (classesInOtherBranch) {
   302                     checkProblem = createProblem(checkProblem, false, NbBundle.getMessage(RenameRefactoringPlugin.class, "ERR_Overrides_tree"));
   303                 }
   304             }
   305         }
   306 
   307         steps += overriddenByMethods.size();
   308         steps += overridesMethods.size();
   309 
   310         fireProgressListenerStart(RenameRefactoring.PARAMETERS_CHECK, 8 + 3*steps);
   311         
   312         fireProgressListenerStep();
   313         fireProgressListenerStep();
   314         fireProgressListenerStop();
   315         return checkProblem;
   316     }
   317 
   318     private boolean isUnderSourceRoot(FileObject fo) {
   319         if (project == null) {
   320             return false;
   321         }
   322         for (FileObject root : project.getSourceRootFiles()) {
   323             if (FileUtil.isParentOf(root, fo)) {
   324                 return true;
   325             }
   326         }
   327         for (FileObject root : project.getTestSourceRootFiles()) {
   328             if (FileUtil.isParentOf(root, fo)) {
   329                 return true;
   330             }
   331         }
   332         return false;
   333     }
   334     
   335     @Override
   336     public Problem preCheck() {
   337         if (treePathHandle == null || treePathHandle.getFileObject() == null || !treePathHandle.getFileObject().isValid()) {
   338             return new Problem(true, NbBundle.getMessage(RenameRefactoringPlugin.class, "DSC_ElNotAvail")); // NOI18N
   339         }
   340         return null;
   341     }
   342 
   343     private Set<FileObject> getRelevantFiles() {
   344         if (treePathHandle.getKind() == ElementKind.VARIABLE || treePathHandle.getKind() == ElementKind.PARAMETER) {
   345             // For local variables, only look in the current file!
   346             return Collections.singleton(treePathHandle.getFileObject());
   347         } else {
   348             return RetoucheUtils.getRubyFilesInProject(treePathHandle.getFileObject());
   349         }
   350 //        }
   351     }
   352 
   353     private Set<RubyElementCtx> allMethods;
   354 
   355     private static final Comparator<Difference> COMPARATOR = new Comparator<Difference>() {
   356         public int compare(Difference d1, Difference d2) {
   357             return d1.getStartPosition().getOffset() - d2.getStartPosition().getOffset();
   358 
   359         };
   360     };
   361     
   362     public Problem prepare(RefactoringElementsBag elements) {
   363         if (treePathHandle == null) {
   364             return null;
   365         }
   366         Problem problem  = null;
   367         Set<FileObject> files = getRelevantFiles();
   368         fireProgressListenerStart(ProgressEvent.START, files.size());
   369         if (!files.isEmpty()) {
   370             TransformTask transform = new TransformTask() {
   371                 @Override
   372                 protected Collection<ModificationResult> process(ParserResult parserResult) {
   373                     RenameTransformer rt = new RenameTransformer(refactoring.getNewName(), allMethods);
   374                     rt.setWorkingCopy(parserResult);
   375                     rt.scan();
   376                     ModificationResult mr = new ModificationResult();
   377 
   378                     mr.addDifferences(parserResult.getSnapshot().getSource().getFileObject(), cullDifferences(rt.diffs));
   379 
   380                     return Collections.singleton(mr);
   381                 }
   382             };
   383 
   384             final Collection<ModificationResult> results = processFiles(files, transform);
   385 
   386             // We don't want retouche to look at all results since we are finding the same nodes
   387             // repeated in our tree (e.g. @a += 1 has two nodes for @a for the assign and the references).
   388             for (ModificationResult result: results) {
   389                 for (FileObject jfo : result.getModifiedFileObjects()) {
   390 
   391                     for (Difference diff: result.getDifferences(jfo)) {
   392                         String old = diff.getOldText();
   393                         if (old!=null) {  //TODO: workaround. generator issue?
   394                             elements.add(refactoring,DiffElement.create(diff, jfo, result));
   395                         }
   396                     }
   397                 }
   398             }
   399 
   400             elements.registerTransaction(new RetoucheCommit(results));
   401         }
   402         // see #126733. need to set a correct new name for the file rename plugin
   403         // that gets invoked after this plugin when the refactoring is invoked on a file.
   404         if (refactoring.getRefactoringSource().lookup(FileObject.class) != null) {
   405             String newName = RubyUtils.camelToUnderlinedName(refactoring.getNewName());
   406             refactoring.setNewName(newName);
   407         }
   408 
   409         fireProgressListenerStop();
   410                 
   411         return problem;
   412     }
   413 
   414     // At NB 8.0 it gets really unhappy if we submit duplicate Differences which overlap.
   415     // This method sorts and then removes any which happen to have the same start offset.
   416     // Start offset might not be perfect but in the cases on improper overlaps it should get
   417     // repaired by the thing supplying the differences.
   418     private static List<Difference> cullDifferences(List<Difference> oldDiffs) {
   419         List<Difference> diffs = new ArrayList<Difference>();
   420         Difference lastDiff = null;
   421 
   422         if (oldDiffs.size() > 0) Collections.sort(oldDiffs, COMPARATOR);
   423 
   424         for (Difference diff: oldDiffs) {
   425             if (lastDiff == null ||
   426                     (diff.getStartPosition().getOffset() != lastDiff.getStartPosition().getOffset() &&
   427                     diff.getEndPosition().getOffset() != lastDiff.getEndPosition().getOffset())) {
   428                 diffs.add(diff);
   429             }
   430 
   431             lastDiff = diff;
   432         }
   433 
   434         return diffs;
   435     }
   436 
   437     private static final String getString(String key) {
   438         return NbBundle.getMessage(RenameRefactoringPlugin.class, key);
   439     }
   440 
   441     private String getWarningMsg(ElementKind kind, String newName) {
   442         String msg = null;
   443         if (ElementKind.CLASS == kind) {
   444             for (String each : newName.split("::")) {
   445                 //NOI18N
   446                 msg = RubyUtils.getIdentifierWarning(each, 0);
   447                 if (msg != null) {
   448                     break;
   449                 }
   450             }
   451         } else {
   452             msg = RubyUtils.getIdentifierWarning(newName, 0);
   453         }
   454         return msg;
   455     }
   456     
   457     /**
   458      *
   459      * @author Jan Becicka
   460      */
   461     public class RenameTransformer extends SearchVisitor {
   462 
   463         private final Set<RubyElementCtx> allMethods;
   464         private final String newName;
   465         private final String oldName;
   466         private CloneableEditorSupport ces;
   467         private List<Difference> diffs;
   468 
   469         @Override
   470         public void setWorkingCopy(ParserResult workingCopy) {
   471             // Cached per working copy
   472             this.ces = null;
   473             this.diffs = null;
   474             super.setWorkingCopy(workingCopy);
   475         }
   476         
   477         public RenameTransformer(String newName, Set<RubyElementCtx> am) {
   478             this.newName = newName;
   479             this.oldName = treePathHandle.getSimpleName();
   480             this.allMethods = am;
   481         }
   482         
   483         @Override
   484         public void scan() {
   485             // TODO - do I need to force state to resolved?
   486             //compiler.toPhase(org.netbeans.napi.gsfret.source.Phase.RESOLVED);
   487 
   488             diffs = new ArrayList<Difference>();
   489             RubyElementCtx searchCtx = treePathHandle;
   490             Error error = null;
   491             Node root = AstUtilities.getRoot(workingCopy);
   492             FileObject workingCopyFileObject = RubyUtils.getFileObject(workingCopy);
   493             if (root != null) {
   494                 
   495                 Element element = AstElement.create(workingCopy, root);
   496                 Node node = searchCtx.getNode();
   497                 RubyElementCtx fileCtx = new RubyElementCtx(root, node, element, workingCopyFileObject, workingCopy);
   498                 Node method = null;
   499                 if (node instanceof ArgumentNode) {
   500                     AstPath path = searchCtx.getPath();
   501                     assert path.leaf() == node;
   502                     Node parent = path.leafParent();
   503 
   504                     if (!(parent instanceof MethodDefNode)) {
   505                         method = AstUtilities.findLocalScope(node, path);
   506                     }
   507                 } else if (node instanceof LocalVarNode || node instanceof LocalAsgnNode || node instanceof DAsgnNode || 
   508                         node instanceof DVarNode) {
   509                     // A local variable read or a parameter read, or an assignment to one of these
   510                     AstPath path = searchCtx.getPath();
   511                     method = AstUtilities.findLocalScope(node, path);
   512                 }
   513 
   514                 if (method != null) {
   515                     findLocal(searchCtx, fileCtx, method, oldName);
   516                 } else {
   517                     // Full AST search
   518                     AstPath path = new AstPath();
   519                     path.descend(root);
   520                     find(path, searchCtx, fileCtx, root, oldName, Character.isUpperCase(oldName.charAt(0)));
   521                     path.ascend();
   522                 }
   523             } else {
   524                 // See if the document contains references to this symbol and if so, put a warning in
   525                 String workingCopyText = workingCopy.getSnapshot().getText().toString();
   526 
   527                 if (workingCopyText.indexOf(oldName) != -1) {
   528                     // TODO - icon??
   529                     if (ces == null) {
   530                         ces = RetoucheUtils.findCloneableEditorSupport(workingCopy);
   531                     }
   532                     int start = 0;
   533                     int end = 0;
   534                     String desc = NbBundle.getMessage(RenameRefactoringPlugin.class, "ParseErrorFile", oldName);
   535                     List<? extends Error> errors = workingCopy.getDiagnostics();
   536                     if (errors.size() > 0) {
   537                         for (Error e : errors) {
   538                             if (e.getSeverity() == Severity.ERROR) {
   539                                 error = e;
   540                                 break;
   541                             }
   542                         }
   543                         if (error == null) {
   544                             error = errors.get(0);
   545                         }
   546                         
   547                         String errorMsg = error.getDisplayName();
   548                         
   549                         if (errorMsg.length() > 80) {
   550                             errorMsg = errorMsg.substring(0, 77) + "..."; // NOI18N
   551                         }
   552 
   553                         desc = desc + "; " + errorMsg;
   554                         start = error.getStartPosition();
   555                         start = LexUtilities.getLexerOffset(workingCopy, start);
   556                         if (start == -1) {
   557                             start = 0;
   558                         }
   559                         end = start;
   560                     }
   561                     PositionRef startPos = ces.createPositionRef(start, Bias.Forward);
   562                     PositionRef endPos = ces.createPositionRef(end, Bias.Forward);
   563                     Difference diff = new Difference(Difference.Kind.CHANGE, startPos, endPos, "", "", desc); // NOI18N
   564                     diffs.add(diff);
   565                 }
   566             }
   567 
   568             if (error == null && refactoring.isSearchInComments()) {
   569                 Document doc = RetoucheUtils.getDocument(workingCopy, RubyUtils.getFileObject(workingCopy));
   570                 if (doc != null) {
   571                     //force open
   572                     TokenHierarchy<Document> th = TokenHierarchy.get(doc);
   573                     TokenSequence<?> ts = th.tokenSequence();
   574 
   575                     ts.move(0);
   576 
   577                     searchTokenSequence(ts);
   578                 }
   579             }
   580 
   581             ces = null;
   582         }
   583         
   584         private void searchTokenSequence(TokenSequence<?> ts) {
   585             if (ts.moveNext()) {
   586                 do {
   587                     Token<?> token = ts.token();
   588                     TokenId id = token.id();
   589 
   590                     String primaryCategory = id.primaryCategory();
   591                     if ("comment".equals(primaryCategory) || "block-comment".equals(primaryCategory)) { // NOI18N
   592                         // search this comment
   593                         CharSequence tokenText = token.text();
   594                         if (tokenText == null || oldName == null) {
   595                             continue;
   596                         }
   597                         int index = TokenUtilities.indexOf(tokenText, oldName);
   598                         if (index != -1) {
   599                             String text = tokenText.toString();
   600                             // TODO make sure it's its own word. Technically I could
   601                             // look at identifier chars like "_" here but since they are
   602                             // used for other purposes in comments, consider letters
   603                             // and numbers as enough
   604                             if ((index == 0 || !Character.isLetterOrDigit(text.charAt(index-1))) &&
   605                                     (index+oldName.length() >= text.length() || 
   606                                     !Character.isLetterOrDigit(text.charAt(index+oldName.length())))) {
   607                                 int start = ts.offset() + index;
   608                                 int end = start + oldName.length();
   609                                 if (ces == null) {
   610                                     ces = RetoucheUtils.findCloneableEditorSupport(workingCopy);
   611                                 }
   612                                 PositionRef startPos = ces.createPositionRef(start, Bias.Forward);
   613                                 PositionRef endPos = ces.createPositionRef(end, Bias.Forward);
   614                                 String desc = getString("ChangeComment");
   615                                 Difference diff = new Difference(Difference.Kind.CHANGE, startPos, endPos, oldName, newName, desc);
   616                                 diffs.add(diff);
   617                             }
   618                         }
   619                     } else {
   620                         TokenSequence<?> embedded = ts.embedded();
   621                         if (embedded != null) {
   622                             searchTokenSequence(embedded);
   623                         }                                    
   624                     }
   625                 } while (ts.moveNext());
   626             }
   627         }
   628 
   629         private void rename(Node node, String oldCode, String newCode, String desc) {
   630             OffsetRange range = AstUtilities.getNameRange(node);
   631             assert range != OffsetRange.NONE;
   632             int pos = range.getStart();
   633 
   634             if (desc == null) {
   635                 // TODO - insert "method call", "method definition", "class definition", "symbol", "attribute" etc. and from and too?
   636                 if (node instanceof MethodDefNode) {
   637                     desc = getString("UpdateMethodDef");
   638                 } else if (AstUtilities.isCall(node)) {
   639                     desc = getString("UpdateCall");
   640                 } else if (node instanceof SymbolNode) {
   641                     desc = getString("UpdateSymbol");
   642                 } else if (node instanceof ClassNode || node instanceof SClassNode) {
   643                     desc = getString("UpdateClassDef");
   644                 } else if (node instanceof ModuleNode) {
   645                     desc = getString("UpdateModule");
   646                 } else if (node instanceof LocalVarNode || node instanceof LocalAsgnNode || node instanceof DVarNode || node instanceof DAsgnNode) {
   647                     desc = getString("UpdateLocalvar");
   648                 } else if (node instanceof GlobalVarNode || node instanceof GlobalAsgnNode) {
   649                     desc = getString("UpdateGlobal");
   650                 } else if (node instanceof InstVarNode || node instanceof InstAsgnNode) {
   651                     desc = getString("UpdateInstance");
   652                 } else if (node instanceof ClassVarNode || node instanceof ClassVarDeclNode || node instanceof ClassVarAsgnNode) {
   653                     desc = getString("UpdateClassvar");
   654                 } else {
   655                     desc = NbBundle.getMessage(RenameRefactoringPlugin.class, "UpdateRef", oldCode);
   656                 }
   657             }
   658 
   659             if (ces == null) {
   660                 ces = RetoucheUtils.findCloneableEditorSupport(workingCopy);
   661             }
   662             
   663             // Convert from AST to lexer offsets if necessary
   664             pos = LexUtilities.getLexerOffset(workingCopy, pos);
   665             if (pos == -1) {
   666                 // Translation failed
   667                 return;
   668             }
   669             
   670             int start = pos;
   671             int end = pos+oldCode.length();
   672             // TODO if a SymbolNode, +=1 since the symbolnode includes the ":"
   673             BaseDocument doc = null;
   674             try {
   675                 doc = (BaseDocument)ces.openDocument();
   676                 doc.readLock();
   677 
   678                 if (start > doc.getLength()) {
   679                     start = end = doc.getLength();
   680                 }
   681 
   682                 if (end > doc.getLength()) {
   683                     end = doc.getLength();
   684                 }
   685 
   686                 // Look in the document and search around a bit to detect the exact method reference
   687                 // (and adjust position accordingly). Thus, if I have off by one errors in the AST (which
   688                 // occasionally happens) the user's source won't get munged
   689                 if (!oldCode.equals(doc.getText(start, end-start))) {
   690                     // Look back and forwards by 1 at first
   691                     int lineStart = Utilities.getRowFirstNonWhite(doc, start);
   692                     int lineEnd = Utilities.getRowLastNonWhite(doc, start)+1; // +1: after last char
   693                     if (lineStart == -1 || lineEnd == -1) { // We're really on the wrong line!
   694                         System.out.println("Empty line entry in " + FileUtil.getFileDisplayName(RubyUtils.getFileObject(workingCopy)) +
   695                                 "; no match for " + oldCode + " in line " + start + " referenced by node " + 
   696                                 node + " of type " + node.getClass().getName());
   697                         return;
   698                     }
   699 
   700                     if (lineStart < 0 || lineEnd-lineStart < 0) {
   701                         return; // Can't process this one
   702                     }
   703 
   704                     String line = doc.getText(lineStart, lineEnd-lineStart);
   705                     if (line.indexOf(oldCode) == -1) {
   706                         System.out.println("Skipping entry in " + FileUtil.getFileDisplayName(RubyUtils.getFileObject(workingCopy)) +
   707                                 "; no match for " + oldCode + " in line " + line + " referenced by node " + 
   708                                 node + " of type " + node.getClass().getName());
   709                     } else {
   710                         int lineOffset = start-lineStart;
   711                         int newOffset = -1;
   712                         // Search up and down by one
   713                         for (int distance = 1; distance < line.length(); distance++) {
   714                             // Ahead first
   715                             if (lineOffset+distance+oldCode.length() <= line.length() &&
   716                                     oldCode.equals(line.substring(lineOffset+distance, lineOffset+distance+oldCode.length()))) {
   717                                 newOffset = lineOffset+distance;
   718                                 break;
   719                             }
   720                             if (lineOffset-distance >= 0 && lineOffset-distance+oldCode.length() <= line.length() &&
   721                                     oldCode.equals(line.substring(lineOffset-distance, lineOffset-distance+oldCode.length()))) {
   722                                 newOffset = lineOffset-distance;
   723                                 break;
   724                             }
   725                         }
   726 
   727                         if (newOffset != -1) {
   728                             start = newOffset+lineStart;
   729                             end = start+oldCode.length();
   730                         }
   731                     }
   732                 }
   733             } catch (IOException ie) {
   734                 Exceptions.printStackTrace(ie);
   735             } catch (BadLocationException ble) {
   736                 Exceptions.printStackTrace(ble);
   737             } finally {
   738                 if (doc != null) {
   739                     doc.readUnlock();
   740                 }
   741             }
   742             
   743             if (newCode == null) {
   744                 // Usually it's the new name so allow client code to refer to it as just null
   745                 newCode = refactoring.getNewName(); // XXX isn't this == our field "newName"?
   746             }
   747 
   748             PositionRef startPos = ces.createPositionRef(start, Bias.Forward);
   749             PositionRef endPos = ces.createPositionRef(end, Bias.Forward);
   750             Difference diff = new Difference(Difference.Kind.CHANGE, startPos, endPos, oldCode, newCode, desc);
   751             diffs.add(diff);
   752         }
   753     
   754         /** Search for local variables in local scope */
   755         private void findLocal(RubyElementCtx searchCtx, RubyElementCtx fileCtx, Node node, String name) {
   756             switch (node.getNodeType()) {
   757             case ARGUMENTNODE:
   758                 // TODO - check parent and make sure it's not a method of the same name?
   759                 // e.g. if I have "def foo(foo)" and I'm searching for "foo" (the parameter),
   760                 // I don't want to pick up the ArgumentNode under def foo that corresponds to the
   761                 // "foo" method name!
   762                 if (((ArgumentNode)node).getName().equals(name)) {
   763                     rename(node, name, null, getString("RenameParam"));
   764                 }
   765                 break;
   766 // I don't have alias nodes within a method, do I?                
   767 //            } else if (node instanceof AliasNode) { 
   768 //                AliasNode an = (AliasNode)node;
   769 //                if (an.getNewName().equals(name) || an.getOldName().equals(name)) {
   770 //                    elements.add(refactoring, WhereUsedElement.create(matchCtx));
   771 //                }
   772 //                break;
   773             case LOCALVARNODE:
   774             case LOCALASGNNODE:
   775                 if (((INameNode)node).getName().equals(name)) {
   776                     rename(node, name, null, getString("UpdateLocalvar"));
   777                 }
   778                 break;
   779             case DVARNODE:
   780             case DASGNNODE:
   781                  if (((INameNode)node).getName().equals(name)) {
   782                     // Found a method call match
   783                     // TODO - make a node on the same line
   784                     // TODO - check arity - see OccurrencesFinder
   785                     rename(node, name, null, getString("UpdateDynvar"));
   786                  }                 
   787                  break;
   788             case SYMBOLNODE:
   789                 // XXX Can I have symbols to local variables? Try it!!!
   790                 if (((SymbolNode)node).getName().equals(name)) {
   791                     rename(node, name, null, getString("UpdateSymbol"));
   792                 }
   793                 break;
   794             }
   795 
   796             for (Node child : node.childNodes()) {
   797                 findLocal(searchCtx, fileCtx, child, name);
   798             }
   799         }
   800         
   801         /**
   802          * @todo P1: This is matching method names on classes that have nothing to do with the class we're searching for
   803          *   - I've gotta filter fields, methods etc. that are not in the current class
   804          *  (but I also have to search for methods that are OVERRIDING the class... so I've gotta work a little harder!)
   805          * @todo Arity matching on the methods to preclude methods that aren't overriding or aliasing!
   806          */
   807         @SuppressWarnings("fallthrough")
   808         private void find(AstPath path, RubyElementCtx searchCtx, RubyElementCtx fileCtx, Node node, String name, boolean upperCase) {
   809             /* TODO look for both old and new and attempt to fix
   810              if (node instanceof AliasNode) {
   811                 AliasNode an = (AliasNode)node;
   812                 if (an.getNewName().equals(name) || an.getOldName().equals(name)) {
   813                     RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node);
   814                     elements.add(refactoring, WhereUsedElement.create(matchCtx));
   815                 }
   816             } else*/ if (!upperCase) {
   817                 // Local variables - I can be smarter about context searches here!
   818                 
   819                 // Methods, attributes, etc.
   820                 // TODO - be more discriminating on the filetype
   821                 switch (node.getNodeType()) {
   822                 case DEFNNODE:
   823                 case DEFSNODE: {
   824                     if (((MethodDefNode)node).getName().equals(name)) {
   825                                                 
   826                         boolean skip = false;
   827 
   828                         // Check that we're in a class or module we're interested in
   829                         String fqn = AstUtilities.getFqnName(path);
   830                         if (fqn == null || fqn.length() == 0) {
   831                             fqn = RubyIndex.OBJECT;
   832                         }
   833                         
   834                         if (!fqn.equals(searchCtx.getDefClass())) {
   835                             boolean inherited = false;
   836                             for (IndexedMethod method : overridesMethods) {
   837                                 if (method.getIn().equals(fqn)) {
   838                                     inherited = true;
   839                                     break;
   840                                 }
   841                             }
   842                             // XXX THE ABOVE IS NOT RIGHT - I shouldn't
   843                             // use equals on the class names, I should use the
   844                             // index and see if one derives fromor includes the other
   845                             skip = !inherited;
   846                         }
   847 
   848                         // Check arity
   849                         if (!skip && AstUtilities.isCall(searchCtx.getNode())) {
   850                             // The reference is a call and this is a definition; see if
   851                             // this looks like a match
   852                             // TODO - enforce that this method is also in the desired
   853                             // target class!!!
   854                             if (!AstUtilities.isCallFor(searchCtx.getNode(), searchCtx.getArity(), node)) {
   855                                 skip = true;
   856                             }
   857                         } else {
   858                             // The search handle is a method def, as is this, with the same name.
   859                             // Now I need to go and see if this is an override (e.g. compatible
   860                             // arglist...)
   861                             // XXX TODO
   862                         }
   863                         
   864                         if (!skip) {
   865                             // Found a method match
   866                             // TODO - check arity - see OccurrencesFinder
   867                             node = ((MethodDefNode)node).getNameNode();
   868                             rename(node, name, null, getString("UpdateMethodDef"));
   869                         }
   870                     }
   871                     break;
   872                 }
   873                 case FCALLNODE:
   874                     if (AstUtilities.isAttr(node)) {
   875                         SymbolNode[] symbols = AstUtilities.getAttrSymbols(node);
   876                         for (SymbolNode symbol : symbols) {
   877                             if (symbol.getName().equals(name)) {
   878                                 // TODO - can't replace the whole node here - I need to replace only the text!
   879                                 rename(node, name, null, null);
   880                             }
   881                         }
   882                     }
   883                     // Fall through for other call checking
   884                 case VCALLNODE:
   885                 case CALLNODE:
   886                      if (((INameNode)node).getName().equals(name)) {
   887                          // TODO - if it's a call without a lhs (e.g. Call.LOCAL),
   888                          // make sure that we're referring to the same method call
   889                         // Found a method call match
   890                         // TODO - make a node on the same line
   891                         // TODO - check arity - see OccurrencesFinder
   892                         rename(node, name, null, null);
   893                      }
   894                      break;
   895                 case SYMBOLNODE:
   896                     if (((SymbolNode)node).getName().equals(name)) {
   897                         // TODO do something about the colon?
   898                         rename(node, name, null, null);
   899                     }
   900                     break;
   901                 case GLOBALVARNODE:
   902                 case GLOBALASGNNODE:
   903                 case INSTVARNODE:
   904                 case INSTASGNNODE:
   905                 case CLASSVARNODE:
   906                 case CLASSVARASGNNODE:
   907                 case CLASSVARDECLNODE:
   908                     if (((INameNode)node).getName().equals(name)) {
   909                         rename(node, ((INameNode)node).getLexicalName(), null, null);
   910                     }
   911                     break;
   912                 }
   913             } else {
   914                 // Classes, modules, constants, etc.
   915                 switch (node.getNodeType()) {
   916                 case COLON2NODE: {
   917                     Colon2Node c2n = (Colon2Node)node;
   918                     if (c2n.getName().equals(name)) {
   919                         rename(node, name, null, null);
   920                     }
   921                     
   922                     break;
   923                 }
   924                 case CONSTNODE:
   925                 case CONSTDECLNODE:
   926                     if (((INameNode)node).getName().equals(name)) {
   927                         rename(node, name, null, null);
   928                     }
   929                     break;
   930                 }
   931             }
   932             
   933             for (Node child : node.childNodes()) {
   934                 path.descend(child);
   935                 find(path, searchCtx, fileCtx, child, name, upperCase);
   936                 path.ascend();
   937             }
   938         }
   939     
   940     }
   941     
   942 }