/** * Copyright (c) 2005-2013 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package com.python.pydev.refactoring.wizards.rename; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; import org.eclipse.ltk.core.refactoring.RefactoringStatus; import org.eclipse.ltk.core.refactoring.TextChange; import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; import org.eclipse.text.edits.TextEditGroup; import org.python.pydev.core.FileUtilsFileBuffer; import org.python.pydev.core.FullRepIterable; import org.python.pydev.core.IPythonNature; import org.python.pydev.editor.codecompletion.revisited.modules.ASTEntryWithSourceModule; import org.python.pydev.editor.refactoring.RefactoringRequest; import org.python.pydev.editorinput.PySourceLocatorBase; import org.python.pydev.parser.visitors.scope.ASTEntry; import org.python.pydev.shared_core.SharedCorePlugin; import org.python.pydev.shared_core.callbacks.ICallback; import org.python.pydev.shared_core.io.FileUtils; import org.python.pydev.shared_core.string.FastStringBuffer; import org.python.pydev.shared_core.string.StringUtils; import org.python.pydev.shared_core.structure.Tuple; import com.python.pydev.analysis.scopeanalysis.AstEntryScopeAnalysisConstants; import com.python.pydev.refactoring.changes.PyRenameResourceChange; import com.python.pydev.refactoring.wizards.IRefactorRenameProcess; /** * Note that this class should only be used once and then should be thrown away. * * It should be used to get AstEntries and transform them into TextEdits (filled into a Change object * as required by the refactoring structure). * * @author Fabio */ public abstract class TextEditCreation { /** * New name for the variable renamed */ protected String inputName; /** * Initial name of renamed variable */ protected String initialName; /** * Name of the module where the rename was requested */ protected String moduleName; /** * Document where the rename was requested */ private IDocument currentDoc; /** * List of processes that will be a part of the refactoring */ private List<IRefactorRenameProcess> processes; /** * Status of the refactoring. Should be updated to contain errors. */ private RefactoringStatus status; /** * Dictionary with a tuple (name of renamed module / file of the renamed module) --> * occurrences to be renamed */ private Map<Tuple<String, File>, HashSet<ASTEntry>> fileOccurrences; /** * Occurrences to be renamed in the current module. */ private HashSet<ASTEntry> docOccurrences; private IFile currentFile; /** * Only for tests */ public static ICallback<IFile, File> createWorkspaceFile; public TextEditCreation(String initialName, String inputName, String moduleName, IDocument currentDoc, List<IRefactorRenameProcess> processes, RefactoringStatus status, IFile currentFile) { Assert.isNotNull(inputName); this.initialName = initialName; this.inputName = inputName; this.moduleName = moduleName; this.currentDoc = currentDoc; this.processes = processes; this.status = status; this.currentFile = currentFile; } /** * In this method, changes from the occurrences found in the current document and * other files are transformed to the objects required by the Eclipse Language Toolkit */ public void fillRefactoringChangeObject(RefactoringRequest request, CheckConditionsContext context) { for (IRefactorRenameProcess p : processes) { if (status.hasFatalError() || request.getMonitor().isCanceled()) { break; } HashSet<ASTEntry> occurrences = p.getOccurrences(); if (docOccurrences == null) { docOccurrences = occurrences; } else { docOccurrences.addAll(occurrences); } Map<Tuple<String, File>, HashSet<ASTEntry>> occurrencesInOtherFiles = p.getOccurrencesInOtherFiles(); if (fileOccurrences == null) { fileOccurrences = occurrencesInOtherFiles; } else { //iterate in a copy for (Map.Entry<Tuple<String, File>, HashSet<ASTEntry>> entry : new HashMap<Tuple<String, File>, HashSet<ASTEntry>>( occurrencesInOtherFiles).entrySet()) { HashSet<ASTEntry> set = occurrencesInOtherFiles.get(entry.getKey()); if (set == null) { occurrencesInOtherFiles.put(entry.getKey(), entry.getValue()); } else { set.addAll(entry.getValue()); } } } } createCurrModuleChange(request); createOtherFileChanges(request); } /** * Create the changes for references in other modules. * @param request * * @param fChange the 'root' change. * @param status the status of the change * @param editsAlreadyCreated */ private void createOtherFileChanges(RefactoringRequest request) { for (Map.Entry<Tuple<String, File>, HashSet<ASTEntry>> entry : fileOccurrences.entrySet()) { //key = module name, IFile for the module (__init__ file may be found if it is a package) Tuple<String, File> tup = entry.getKey(); //now, let's make the mapping from the filesystem to the Eclipse workspace IFile workspaceFile = null; IPath path = null; IDocument doc = null; if (!SharedCorePlugin.inTestMode()) { IProject project = null; IPythonNature nature = request.nature; if (nature != null) { project = nature.getProject(); } workspaceFile = new PySourceLocatorBase().getWorkspaceFile(tup.o2, project); if (workspaceFile == null) { status.addWarning(StringUtils.format( "Error. Unable to resolve the file:\n" + "%s\n" + "to a file in the Eclipse workspace.", tup.o2)); continue; } path = workspaceFile.getFullPath(); } else { //otherwise, we're in tests: just keep going... path = Path.fromOSString(tup.o2.getAbsolutePath()); doc = new Document(FileUtils.getFileContents(tup.o2)); workspaceFile = createWorkspaceFile.call(tup.o2); } //check the text changes HashSet<ASTEntry> astEntries = filterAstEntries(entry.getValue(), AST_ENTRIES_FILTER_TEXT); if (astEntries.size() > 0) { if (doc == null) { doc = FileUtilsFileBuffer.getDocFromResource(workspaceFile); } List<Tuple<List<TextEdit>, String>> renameEdits = getAllRenameEdits(doc, astEntries, path, request.nature); if (status.hasFatalError()) { return; } if (renameEdits.size() > 0) { Tuple<TextChange, MultiTextEdit> textFileChange = getTextFileChange(workspaceFile, doc); TextChange docChange = textFileChange.o1; MultiTextEdit rootEdit = textFileChange.o2; fillEditsInDocChange(docChange, rootEdit, renameEdits); } } //now, check for file changes astEntries = filterAstEntries(entry.getValue(), AST_ENTRIES_FILTER_FILE); if (astEntries.size() > 0) { IResource resourceToRename = workspaceFile; String newName = inputName; //if we have an __init__ file but the initial token is not an __init__ file, it means //that we have to rename the folder that contains the __init__ file if (tup.o1.endsWith(".__init__") && !initialName.equals("__init__")) { resourceToRename = resourceToRename.getParent(); newName = inputName; if (!resourceToRename.getName().equals(FullRepIterable.getLastPart(initialName))) { status.addFatalError(org.python.pydev.shared_core.string.StringUtils .format("Error. The package that was found (%s) for renaming does not match the initial token found (%s)", resourceToRename.getName(), initialName)); return; } } createResourceChange(resourceToRename, newName, request); } } } protected abstract PyRenameResourceChange createResourceChange(IResource resourceToRename, String newName, RefactoringRequest request); /** * TextChange docChange, MultiTextEdit rootEdit * @param currentDoc */ protected abstract Tuple<TextChange, MultiTextEdit> getTextFileChange(IFile workspaceFile, IDocument currentDoc); private final static int AST_ENTRIES_FILTER_TEXT = 1; private final static int AST_ENTRIES_FILTER_FILE = 2; private HashSet<ASTEntry> filterAstEntries(HashSet<ASTEntry> value, int astEntryFilter) { HashSet<ASTEntry> ret = new HashSet<ASTEntry>(); for (ASTEntry entry : value) { if (entry instanceof ASTEntryWithSourceModule) { if ((astEntryFilter & AST_ENTRIES_FILTER_FILE) != 0) { ret.add(entry); } } else { if ((astEntryFilter & AST_ENTRIES_FILTER_TEXT) != 0) { ret.add(entry); } } } return ret; } /** * Create the change for the current module * * @param status the status for the change. * @param fChange tho 'root' change. * @param editsAlreadyCreated */ private void createCurrModuleChange(RefactoringRequest request) { if (docOccurrences.size() == 0 && !(request.isModuleRenameRefactoringRequest())) { status.addFatalError("No occurrences found."); return; } Tuple<TextChange, MultiTextEdit> textFileChange = getTextFileChange(this.currentFile, this.currentDoc); TextChange docChange = textFileChange.o1; MultiTextEdit rootEdit = textFileChange.o2; List<Tuple<List<TextEdit>, String>> renameEdits = getAllRenameEdits(currentDoc, docOccurrences, this.currentFile != null ? this.currentFile.getFullPath() : null, request.nature); if (status.hasFatalError()) { return; } if (renameEdits.size() > 0) { fillEditsInDocChange(docChange, rootEdit, renameEdits); } } /** * Puts the edits found in a doc change, tak * @param fChange * @param editsAlreadyCreatedLst * @param docChange * @param rootEdit * @param renameEdits */ private void fillEditsInDocChange(TextChange docChange, MultiTextEdit rootEdit, List<Tuple<List<TextEdit>, String>> renameEdits) { Assert.isTrue(renameEdits.size() > 0); try { for (Tuple<List<TextEdit>, String> t : renameEdits) { TextEdit[] arr = t.o1.toArray(new TextEdit[t.o1.size()]); rootEdit.addChildren(arr); docChange.addTextEditGroup(new TextEditGroup(t.o2, arr)); } } catch (RuntimeException e) { //StringBuffer buf = new StringBuffer("Found occurrences:"); //for (Tuple<TextEdit, String> t : renameEdits) { // buf.append("Offset: "); // buf.append(t.o1.getOffset()); // buf.append("Len: "); // buf.append(t.o1.getLength()); // buf.append("Str: "); // buf.append(t.o2); // buf.append("\n"); //} // //don't bother reporting this to the user (usually happens if we have it the file changes during the analysis). //PydevPlugin.log(buf.toString(), e); throw e; } } /** * Create a text edit on the given offset. * * It uses the information in the request to obtain the length of the replace and * the new name to be set in the replace * * @param offset the offset marking the place where the replace should happen. * @return a TextEdit corresponding to a rename. */ protected TextEdit createRenameEdit(int offset) { return new ReplaceEdit(offset, initialName.length(), inputName); } /** * Gets the occurrences in a document and converts it to a TextEdit as required * by the Eclipse language toolkit. * * @param occurrences the occurrences found * @param doc the doc where the occurrences were found * @param occurrences * @param workspaceFile may be null! * @param nature * @return a list of tuples with the TextEdit and the description for that edit. */ protected List<Tuple<List<TextEdit>, String>> getAllRenameEdits(IDocument doc, HashSet<ASTEntry> occurrences, IPath workspaceFile, IPythonNature nature) { Set<Integer> s = new HashSet<Integer>(); List<Tuple<List<TextEdit>, String>> ret = new ArrayList<>(); //occurrences = sortOccurrences(occurrences); FastStringBuffer entryBuf = new FastStringBuffer(); for (ASTEntry entry : occurrences) { entryBuf.clear(); Integer loc = (Integer) entry.getAdditionalInfo(AstEntryScopeAnalysisConstants.AST_ENTRY_FOUND_LOCATION, 0); if (loc == AstEntryScopeAnalysisConstants.AST_ENTRY_FOUND_IN_COMMENT) { entryBuf.append("Change (comment): "); } else if (loc == AstEntryScopeAnalysisConstants.AST_ENTRY_FOUND_IN_STRING) { entryBuf.append("Change (string): "); } else { entryBuf.append("Change: "); } entryBuf.appendObject(initialName); entryBuf.append(" >> "); entryBuf.appendObject(inputName); entryBuf.append(" (line:"); entryBuf.append(entry.node.beginLine); entryBuf.append(")"); int offset = AbstractRenameRefactorProcess.getOffset(doc, entry); if (!s.contains(offset)) { s.add(offset); if (entry instanceof IRefactorCustomEntry) { IRefactorCustomEntry iRefactorCustomEntry = (IRefactorCustomEntry) entry; List<TextEdit> edits = iRefactorCustomEntry.createRenameEdit(doc, initialName, inputName, status, workspaceFile, nature); ret.add(new Tuple<List<TextEdit>, String>(edits, entryBuf.toString())); entry.setAdditionalInfo(AstEntryScopeAnalysisConstants.AST_ENTRY_REPLACE_EDIT, edits); if (status.hasFatalError()) { return ret; } } else { checkExpectedInput(doc, entry.node.beginLine, offset, initialName, status, workspaceFile); if (status.hasFatalError()) { return ret; } List<TextEdit> edits = Arrays.asList(createRenameEdit(offset)); entry.setAdditionalInfo(AstEntryScopeAnalysisConstants.AST_ENTRY_REPLACE_EDIT, edits); ret.add(new Tuple<List<TextEdit>, String>(edits, entryBuf.toString())); } } } return ret; } public static void checkExpectedInput(IDocument doc, int line, int offset, String initialName, RefactoringStatus status, IPath workspaceFile) { try { String string = doc.get(offset, initialName.length()); if (!(string.equals(initialName))) { status.addFatalError(StringUtils .format("Error: file %s changed during analysis.\nExpected doc to contain: '%s' and it contained: '%s' at offset: %s (line: %s).", workspaceFile != null ? workspaceFile : "has", initialName, string, offset, line)); return; } } catch (BadLocationException e) { status.addFatalError(StringUtils .format("Error: file %s changed during analysis.\nExpected doc to contain: '%s' at offset: %s (line: %s).", workspaceFile != null ? workspaceFile : "has", initialName, offset, line)); } } }