/** * Copyright (c) 2005-2011 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.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.IResource; import org.eclipse.jface.text.IDocument; import org.eclipse.ltk.core.refactoring.CompositeChange; import org.eclipse.ltk.core.refactoring.RefactoringStatus; import org.eclipse.ltk.core.refactoring.TextChange; import org.eclipse.ltk.core.refactoring.TextFileChange; 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.docutils.StringUtils; 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.refactoring.core.base.PyDocumentChange; import org.python.pydev.refactoring.core.base.PyTextFileChange; import com.aptana.shared_core.string.FastStringBuffer; import com.aptana.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 class TextEditCreation { /** * New name for the variable renamed */ private String inputName; /** * Initial name of renamed variable */ private String initialName; /** * Name of the module where the rename was requested */ private 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; /** * Change object with all the changes that will be done in the rename */ private CompositeChange fChange; /** * 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; public TextEditCreation(String initialName, String inputName, String moduleName, IDocument currentDoc, List<IRefactorRenameProcess> processes, RefactoringStatus status, CompositeChange fChange, IFile currentFile) { this.initialName = initialName; this.inputName = inputName; this.moduleName = moduleName; this.currentDoc = currentDoc; this.processes = processes; this.fChange = fChange; 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(); createOtherFileChanges(); } /** * Create the changes for references in other modules. * * @param fChange the 'root' change. * @param status the status of the change * @param editsAlreadyCreated */ private void createOtherFileChanges() { 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; try { workspaceFile = new PySourceLocatorBase().getWorkspaceFile(tup.o2); if (workspaceFile == null) { status.addWarning(com.aptana.shared_core.string.StringUtils.format("Error. Unable to resolve the file:\n" + "%s\n" + "to a file in the Eclipse workspace.", tup.o2)); continue; } } catch (IllegalStateException e) { //this can happen on tests (but if not on tests, we want to re-throw it String message = e.getMessage(); if (message == null || !message.equals("Workspace is closed.")) { throw e; } //otherwise, let's just keep going in the test... continue; } //check the text changes HashSet<ASTEntry> astEntries = filterAstEntries(entry.getValue(), AST_ENTRIES_FILTER_TEXT); if (astEntries.size() > 0) { IDocument docFromResource = FileUtilsFileBuffer.getDocFromResource(workspaceFile); TextFileChange fileChange = new PyTextFileChange("RenameChange: " + inputName, workspaceFile); MultiTextEdit rootEdit = new MultiTextEdit(); fileChange.setEdit(rootEdit); fileChange.setKeepPreviewEdits(true); List<Tuple<TextEdit, String>> renameEdits = getAllRenameEdits(docFromResource, astEntries); fillEditsInDocChange(fileChange, 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 + ".py"; //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(initialName)) { status.addFatalError(com.aptana.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; } } fChange.add(new PyRenameResourceChange(resourceToRename, newName, com.aptana.shared_core.string.StringUtils.format( "Renaming %s to %s", resourceToRename.getName(), inputName))); } } } 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() { TextChange docChange; if (this.currentFile != null) { docChange = new PyTextFileChange("Current module: " + moduleName, this.currentFile); } else { //used for tests docChange = PyDocumentChange.create("Current module: " + moduleName, this.currentDoc); } if (docOccurrences.size() == 0) { status.addFatalError("No occurrences found."); return; } MultiTextEdit rootEdit = new MultiTextEdit(); docChange.setEdit(rootEdit); docChange.setKeepPreviewEdits(true); List<Tuple<TextEdit, String>> renameEdits = getAllRenameEdits(currentDoc, docOccurrences); 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<TextEdit, String>> renameEdits) { try { boolean addedEdit = false; for (Tuple<TextEdit, String> t : renameEdits) { addedEdit = true; rootEdit.addChild(t.o1); docChange.addTextEditGroup(new TextEditGroup(t.o2, t.o1)); } if (addedEdit) { fChange.add(docChange); } } 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 correponding 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 * @return a list of tuples with the TextEdit and the description for that edit. */ protected List<Tuple<TextEdit, String>> getAllRenameEdits(IDocument doc, HashSet<ASTEntry> occurrences) { Set<Integer> s = new HashSet<Integer>(); List<Tuple<TextEdit, String>> ret = new ArrayList<Tuple<TextEdit, String>>(); //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.append(initialName); entryBuf.append(" >> "); entryBuf.append(inputName); entryBuf.append(" (line:"); entryBuf.append(entry.node.beginLine); entryBuf.append(")"); int offset = AbstractRenameRefactorProcess.getOffset(doc, entry); if (!s.contains(offset)) { s.add(offset); ret.add(new Tuple<TextEdit, String>(createRenameEdit(offset), entryBuf.toString())); } } return ret; } }