Index: editor/tools/plugins/com.google.dart.tools.search/src/com/google/dart/tools/search/internal/ui/text/ReplaceRefactoring.java |
=================================================================== |
--- editor/tools/plugins/com.google.dart.tools.search/src/com/google/dart/tools/search/internal/ui/text/ReplaceRefactoring.java (revision 0) |
+++ editor/tools/plugins/com.google.dart.tools.search/src/com/google/dart/tools/search/internal/ui/text/ReplaceRefactoring.java (revision 0) |
@@ -0,0 +1,562 @@ |
+/* |
+ * Copyright (c) 2012, the Dart project authors. |
+ * |
+ * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except |
+ * in compliance with the License. You may obtain a copy of the License at |
+ * |
+ * http://www.eclipse.org/legal/epl-v10.html |
+ * |
+ * Unless required by applicable law or agreed to in writing, software distributed under the License |
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
+ * or implied. See the License for the specific language governing permissions and limitations under |
+ * the License. |
+ */ |
+package com.google.dart.tools.search.internal.ui.text; |
+ |
+import com.google.dart.tools.search.internal.core.text.PatternConstructor; |
+import com.google.dart.tools.search.internal.ui.Messages; |
+import com.google.dart.tools.search.internal.ui.SearchMessages; |
+import com.google.dart.tools.search.ui.text.Match; |
+import com.google.dart.tools.search2.internal.ui.InternalSearchUI; |
+import com.google.dart.tools.search2.internal.ui.text.PositionTracker; |
+ |
+import com.ibm.icu.text.Collator; |
+ |
+import org.eclipse.core.filebuffers.FileBuffers; |
+import org.eclipse.core.filebuffers.ITextFileBuffer; |
+import org.eclipse.core.filebuffers.ITextFileBufferManager; |
+import org.eclipse.core.filebuffers.LocationKind; |
+import org.eclipse.core.filesystem.URIUtil; |
+import org.eclipse.core.resources.IContainer; |
+import org.eclipse.core.resources.IFile; |
+import org.eclipse.core.resources.IResource; |
+import org.eclipse.core.resources.ResourcesPlugin; |
+import org.eclipse.core.runtime.Assert; |
+import org.eclipse.core.runtime.CoreException; |
+import org.eclipse.core.runtime.IProgressMonitor; |
+import org.eclipse.core.runtime.IStatus; |
+import org.eclipse.core.runtime.OperationCanceledException; |
+import org.eclipse.jface.text.BadLocationException; |
+import org.eclipse.jface.text.IDocument; |
+import org.eclipse.jface.text.Position; |
+import org.eclipse.jface.text.TextUtilities; |
+import org.eclipse.ltk.core.refactoring.Change; |
+import org.eclipse.ltk.core.refactoring.CompositeChange; |
+import org.eclipse.ltk.core.refactoring.Refactoring; |
+import org.eclipse.ltk.core.refactoring.RefactoringStatus; |
+import org.eclipse.ltk.core.refactoring.TextChange; |
+import org.eclipse.ltk.core.refactoring.TextEditChangeGroup; |
+import org.eclipse.ltk.core.refactoring.TextFileChange; |
+import org.eclipse.ltk.core.refactoring.participants.ResourceChangeChecker; |
+import org.eclipse.text.edits.MultiTextEdit; |
+import org.eclipse.text.edits.ReplaceEdit; |
+import org.eclipse.text.edits.TextEditGroup; |
+ |
+import java.net.URI; |
+import java.util.ArrayList; |
+import java.util.Arrays; |
+import java.util.Collection; |
+import java.util.Comparator; |
+import java.util.HashMap; |
+import java.util.HashSet; |
+import java.util.Iterator; |
+import java.util.Map; |
+import java.util.Set; |
+import java.util.regex.Matcher; |
+import java.util.regex.Pattern; |
+import java.util.regex.PatternSyntaxException; |
+ |
+public class ReplaceRefactoring extends Refactoring { |
+ |
+ public static class SearchResultUpdateChange extends Change { |
+ |
+ private MatchGroup[] fMatchGroups; |
+ private Match[] fMatches; |
+ |
+ private Map<URI, ArrayList<FileMatch>> fIgnoredMatches; |
+ private final FileSearchResult fResult; |
+ private final boolean fIsRemove; |
+ |
+ public SearchResultUpdateChange(FileSearchResult result, MatchGroup[] matchGroups, |
+ Map<URI, ArrayList<FileMatch>> ignoredMatches) { |
+ this(result, null, ignoredMatches, true); |
+ fMatchGroups = matchGroups; |
+ } |
+ |
+ private SearchResultUpdateChange(FileSearchResult result, Match[] matches, |
+ Map<URI, ArrayList<FileMatch>> ignoredMatches, boolean isRemove) { |
+ fResult = result; |
+ fMatches = matches; |
+ fIgnoredMatches = ignoredMatches; |
+ fIsRemove = isRemove; |
+ } |
+ |
+ @Override |
+ public Object getModifiedElement() { |
+ return null; |
+ } |
+ |
+ @Override |
+ public String getName() { |
+ return SearchMessages.ReplaceRefactoring_result_update_name; |
+ } |
+ |
+ @Override |
+ public void initializeValidationData(IProgressMonitor pm) { |
+ } |
+ |
+ @Override |
+ public RefactoringStatus isValid(IProgressMonitor pm) throws CoreException, |
+ OperationCanceledException { |
+ return new RefactoringStatus(); |
+ } |
+ |
+ @Override |
+ public Change perform(IProgressMonitor pm) throws CoreException { |
+ Match[] matches = getMatches(); |
+ if (fIsRemove) { |
+ fResult.removeMatches(matches); |
+ } else { |
+ fResult.addMatches(matches); |
+ } |
+ return new SearchResultUpdateChange(fResult, matches, fIgnoredMatches, !fIsRemove); |
+ } |
+ |
+ private Match[] getMatches() { |
+ if (fMatches == null) { |
+ ArrayList<FileMatch> matches = new ArrayList<FileMatch>(); |
+ for (int i = 0; i < fMatchGroups.length; i++) { |
+ MatchGroup curr = fMatchGroups[i]; |
+ if (curr.group.isEnabled()) { |
+ FileMatch match = curr.match; |
+ matches.add(match); |
+ |
+ if (fIgnoredMatches == null) { |
+ continue; |
+ } |
+ |
+ // Add matches that we removed before starting the refactoring |
+ IFile file = match.getFile(); |
+ URI uri = file.getLocationURI(); |
+ if (uri != null) { |
+ ArrayList<FileMatch> ignoredMatches = fIgnoredMatches.get(uri); |
+ if (ignoredMatches != null) { |
+ matches.addAll(ignoredMatches); |
+ } |
+ } |
+ } |
+ } |
+ fMatches = matches.toArray(new Match[matches.size()]); |
+ fMatchGroups = null; |
+ } |
+ return fMatches; |
+ } |
+ |
+ } |
+ |
+ private static class MatchGroup { |
+ public TextEditChangeGroup group; |
+ public FileMatch match; |
+ |
+ public MatchGroup(TextEditChangeGroup group, FileMatch match) { |
+ this.group = group; |
+ this.match = match; |
+ } |
+ } |
+ |
+ private static String getOriginalText(IDocument doc, int offset, int length) { |
+ try { |
+ return doc.get(offset, length); |
+ } catch (BadLocationException e) { |
+ return null; |
+ } |
+ } |
+ |
+ private final FileSearchResult fResult; |
+ |
+ private final Object[] fSelection; |
+ |
+ private final HashMap<IFile, HashSet<FileMatch>> fMatches; |
+ |
+ /** Map that keeps already collected locations. */ |
+ private final Map<URI, IFile> fAlreadyCollected; |
+ |
+ /** Map that keeps ignored matches (can be null). */ |
+ private Map<URI, ArrayList<FileMatch>> fIgnoredMatches; |
+ |
+ private String fReplaceString; |
+ |
+ private Change fChange; |
+ |
+ public ReplaceRefactoring(FileSearchResult result, Object[] selection) { |
+ Assert.isNotNull(result); |
+ |
+ fResult = result; |
+ fSelection = selection; |
+ |
+ fMatches = new HashMap<IFile, HashSet<FileMatch>>(); |
+ fAlreadyCollected = new HashMap<URI, IFile>(selection != null ? selection.length |
+ : result.getElements().length); |
+ |
+ fReplaceString = null; |
+ } |
+ |
+ @SuppressWarnings({"rawtypes", "unchecked"}) |
+ @Override |
+ public RefactoringStatus checkFinalConditions(IProgressMonitor pm) throws CoreException, |
+ OperationCanceledException { |
+ if (fReplaceString == null) { |
+ return RefactoringStatus.createFatalErrorStatus(SearchMessages.ReplaceRefactoring_error_no_replace_string); |
+ } |
+ |
+ Pattern pattern = null; |
+ FileSearchQuery query = getQuery(); |
+ if (query.isRegexSearch()) { |
+ pattern = createSearchPattern(query); |
+ } |
+ |
+ RefactoringStatus resultingStatus = new RefactoringStatus(); |
+ |
+ Collection<IFile> allFilesSet = fMatches.keySet(); |
+ IFile[] allFiles = allFilesSet.toArray(new IFile[allFilesSet.size()]); |
+ Arrays.sort(allFiles, new Comparator() { |
+ private Collator fCollator = Collator.getInstance(); |
+ |
+ @Override |
+ public int compare(Object o1, Object o2) { |
+ String p1 = ((IFile) o1).getFullPath().toString(); |
+ String p2 = ((IFile) o2).getFullPath().toString(); |
+ return fCollator.compare(p1, p2); |
+ } |
+ }); |
+ checkFilesToBeChanged(allFiles, resultingStatus); |
+ if (resultingStatus.hasFatalError()) { |
+ return resultingStatus; |
+ } |
+ |
+ CompositeChange compositeChange = new CompositeChange( |
+ SearchMessages.ReplaceRefactoring_composite_change_name); |
+ compositeChange.markAsSynthetic(); |
+ |
+ ArrayList<MatchGroup> matchGroups = new ArrayList<MatchGroup>(); |
+ boolean hasChanges = false; |
+ try { |
+ for (int i = 0; i < allFiles.length; i++) { |
+ IFile file = allFiles[i]; |
+ Collection bucket = fMatches.get(file); |
+ if (!bucket.isEmpty()) { |
+ try { |
+ TextChange change = createFileChange( |
+ file, |
+ pattern, |
+ bucket, |
+ resultingStatus, |
+ matchGroups); |
+ if (change != null) { |
+ compositeChange.add(change); |
+ hasChanges = true; |
+ } |
+ } catch (CoreException e) { |
+ String message = Messages.format( |
+ SearchMessages.ReplaceRefactoring_error_access_file, |
+ new Object[] {file.getName(), e.getLocalizedMessage()}); |
+ return RefactoringStatus.createFatalErrorStatus(message); |
+ } |
+ } |
+ } |
+ } catch (PatternSyntaxException e) { |
+ String message = Messages.format( |
+ SearchMessages.ReplaceRefactoring_error_replacement_expression, |
+ e.getLocalizedMessage()); |
+ return RefactoringStatus.createFatalErrorStatus(message); |
+ } |
+ if (!hasChanges && resultingStatus.isOK()) { |
+ return RefactoringStatus.createFatalErrorStatus(SearchMessages.ReplaceRefactoring_error_no_changes); |
+ } |
+ |
+ compositeChange.add(new SearchResultUpdateChange( |
+ fResult, |
+ matchGroups.toArray(new MatchGroup[matchGroups.size()]), |
+ fIgnoredMatches)); |
+ |
+ fChange = compositeChange; |
+ return resultingStatus; |
+ } |
+ |
+ @Override |
+ public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, |
+ OperationCanceledException { |
+ String searchString = getQuery().getSearchString(); |
+ if (searchString.length() == 0) { |
+ return RefactoringStatus.createFatalErrorStatus(SearchMessages.ReplaceRefactoring_error_illegal_search_string); |
+ } |
+ fMatches.clear(); |
+ |
+ if (fSelection != null) { |
+ for (int i = 0; i < fSelection.length; i++) { |
+ collectMatches(fSelection[i]); |
+ } |
+ } else { |
+ Object[] elements = fResult.getElements(); |
+ for (int i = 0; i < elements.length; i++) { |
+ collectMatches(elements[i]); |
+ } |
+ } |
+ if (!hasMatches()) { |
+ return RefactoringStatus.createFatalErrorStatus(SearchMessages.ReplaceRefactoring_error_no_matches); |
+ } |
+ return new RefactoringStatus(); |
+ } |
+ |
+ @Override |
+ public Change createChange(IProgressMonitor pm) throws CoreException, OperationCanceledException { |
+ return fChange; |
+ } |
+ |
+ @Override |
+ public String getName() { |
+ return SearchMessages.ReplaceRefactoring_refactoring_name; |
+ } |
+ |
+ public int getNumberOfFiles() { |
+ return fMatches.keySet().size(); |
+ } |
+ |
+ public int getNumberOfMatches() { |
+ int count = 0; |
+ for (Iterator<HashSet<FileMatch>> iterator = fMatches.values().iterator(); iterator.hasNext();) { |
+ Set<FileMatch> bucket = iterator.next(); |
+ count += bucket.size(); |
+ } |
+ return count; |
+ } |
+ |
+ public FileSearchQuery getQuery() { |
+ return (FileSearchQuery) fResult.getQuery(); |
+ } |
+ |
+ public boolean hasMatches() { |
+ return !fMatches.isEmpty(); |
+ } |
+ |
+ public void setReplaceString(String string) { |
+ fReplaceString = string; |
+ } |
+ |
+ private void checkFilesToBeChanged(IFile[] filesToBeChanged, RefactoringStatus resultingStatus) |
+ throws CoreException { |
+ ArrayList<IFile> readOnly = new ArrayList<IFile>(); |
+ for (int i = 0; i < filesToBeChanged.length; i++) { |
+ IFile file = filesToBeChanged[i]; |
+ if (file.isReadOnly()) { |
+ readOnly.add(file); |
+ } |
+ } |
+ IFile[] readOnlyFiles = readOnly.toArray(new IFile[readOnly.size()]); |
+ |
+ IStatus status = ResourcesPlugin.getWorkspace().validateEdit( |
+ readOnlyFiles, |
+ getValidationContext()); |
+ if (status.getSeverity() == IStatus.CANCEL) { |
+ throw new OperationCanceledException(); |
+ } |
+ resultingStatus.merge(RefactoringStatus.create(status)); |
+ if (resultingStatus.hasFatalError()) { |
+ return; |
+ } |
+ resultingStatus.merge(ResourceChangeChecker.checkFilesToBeChanged(filesToBeChanged, null)); |
+ } |
+ |
+ private void collectMatches(Object object) throws CoreException { |
+ if (object instanceof LineElement) { |
+ LineElement lineElement = (LineElement) object; |
+ FileMatch[] matches = lineElement.getMatches(fResult); |
+ for (int i = 0; i < matches.length; i++) { |
+ FileMatch fileMatch = matches[i]; |
+ if (isMatchToBeIncluded(fileMatch)) { |
+ getBucket(fileMatch.getFile()).add(fileMatch); |
+ } |
+ } |
+ } else if (object instanceof IContainer) { |
+ IContainer container = (IContainer) object; |
+ IResource[] members = container.members(); |
+ for (int i = 0; i < members.length; i++) { |
+ collectMatches(members[i]); |
+ } |
+ } else if (object instanceof IFile) { |
+ Match[] matches = fResult.getMatches(object); |
+ if (matches.length > 0) { |
+ Collection<FileMatch> bucket = null; |
+ for (int i = 0; i < matches.length; i++) { |
+ FileMatch fileMatch = (FileMatch) matches[i]; |
+ if (isMatchToBeIncluded(fileMatch)) { |
+ if (bucket == null) { |
+ bucket = getBucket((IFile) object); |
+ } |
+ bucket.add(fileMatch); |
+ } |
+ } |
+ } |
+ } |
+ } |
+ |
+ private String computeReplacementString(Pattern pattern, String originalText, |
+ String replacementText, String lineDelimiter) throws PatternSyntaxException { |
+ if (pattern != null) { |
+ try { |
+ replacementText = PatternConstructor.interpretReplaceEscapes( |
+ replacementText, |
+ originalText, |
+ lineDelimiter); |
+ |
+ Matcher matcher = pattern.matcher(originalText); |
+ StringBuffer sb = new StringBuffer(); |
+ matcher.reset(); |
+ if (matcher.find()) { |
+ matcher.appendReplacement(sb, replacementText); |
+ } else { |
+ return null; |
+ } |
+ matcher.appendTail(sb); |
+ return sb.toString(); |
+ } catch (IndexOutOfBoundsException ex) { |
+ throw new PatternSyntaxException(ex.getLocalizedMessage(), replacementText, -1); |
+ } |
+ } |
+ return replacementText; |
+ } |
+ |
+ private TextChange createFileChange(IFile file, Pattern pattern, Collection<FileMatch> matches, |
+ RefactoringStatus resultingStatus, Collection<MatchGroup> matchGroups) |
+ throws PatternSyntaxException, CoreException { |
+ PositionTracker tracker = InternalSearchUI.getInstance().getPositionTracker(); |
+ |
+ TextFileChange change = new TextFileChange(Messages.format( |
+ SearchMessages.ReplaceRefactoring_group_label_change_for_file, |
+ file.getName()), file); |
+ change.setEdit(new MultiTextEdit()); |
+ |
+ ITextFileBufferManager manager = FileBuffers.getTextFileBufferManager(); |
+ manager.connect(file.getFullPath(), LocationKind.IFILE, null); |
+ try { |
+ ITextFileBuffer textFileBuffer = manager.getTextFileBuffer( |
+ file.getFullPath(), |
+ LocationKind.IFILE); |
+ if (textFileBuffer == null) { |
+ resultingStatus.addError(Messages.format( |
+ SearchMessages.ReplaceRefactoring_error_accessing_file_buffer, |
+ file.getName())); |
+ return null; |
+ } |
+ IDocument document = textFileBuffer.getDocument(); |
+ String lineDelimiter = TextUtilities.getDefaultLineDelimiter(document); |
+ |
+ for (Iterator<FileMatch> iterator = matches.iterator(); iterator.hasNext();) { |
+ FileMatch match = iterator.next(); |
+ int offset = match.getOffset(); |
+ int length = match.getLength(); |
+ Position currentPosition = tracker.getCurrentPosition(match); |
+ if (currentPosition != null) { |
+ offset = currentPosition.offset; |
+ if (length != currentPosition.length) { |
+ resultingStatus.addError(Messages.format( |
+ SearchMessages.ReplaceRefactoring_error_match_content_changed, |
+ file.getName())); |
+ continue; |
+ } |
+ } |
+ |
+ String originalText = getOriginalText(document, offset, length); |
+ if (originalText == null) { |
+ resultingStatus.addError(Messages.format( |
+ SearchMessages.ReplaceRefactoring_error_match_content_changed, |
+ file.getName())); |
+ continue; |
+ } |
+ |
+ String replacementString = computeReplacementString( |
+ pattern, |
+ originalText, |
+ fReplaceString, |
+ lineDelimiter); |
+ if (replacementString == null) { |
+ resultingStatus.addError(Messages.format( |
+ SearchMessages.ReplaceRefactoring_error_match_content_changed, |
+ file.getName())); |
+ continue; |
+ } |
+ |
+ ReplaceEdit replaceEdit = new ReplaceEdit(offset, length, replacementString); |
+ change.addEdit(replaceEdit); |
+ TextEditChangeGroup textEditChangeGroup = new TextEditChangeGroup( |
+ change, |
+ new TextEditGroup( |
+ SearchMessages.ReplaceRefactoring_group_label_match_replace, |
+ replaceEdit)); |
+ change.addTextEditChangeGroup(textEditChangeGroup); |
+ matchGroups.add(new MatchGroup(textEditChangeGroup, match)); |
+ } |
+ } finally { |
+ manager.disconnect(file.getFullPath(), LocationKind.IFILE, null); |
+ } |
+ return change; |
+ } |
+ |
+ private Pattern createSearchPattern(FileSearchQuery query) { |
+ return PatternConstructor.createPattern( |
+ query.getSearchString(), |
+ true, |
+ true, |
+ query.isCaseSensitive(), |
+ false); |
+ } |
+ |
+ private Collection<FileMatch> getBucket(IFile file) { |
+ HashSet<FileMatch> col = fMatches.get(file); |
+ if (col == null) { |
+ col = new HashSet<FileMatch>(); |
+ fMatches.put(file, col); |
+ } |
+ return col; |
+ } |
+ |
+ /** |
+ * Checks whether the match should be included. Also collects ignored matches whose file is linked |
+ * to an already collected match. |
+ * |
+ * @param match the match |
+ * @return <code>true</code> iff the match should be included |
+ */ |
+ private boolean isMatchToBeIncluded(FileMatch match) { |
+ IFile file = match.getFile(); |
+ URI uri = file.getLocationURI(); |
+ if (uri == null) { |
+ return true; |
+ } |
+ |
+ for (Iterator<URI> iter = fAlreadyCollected.keySet().iterator(); iter.hasNext();) { |
+ if (URIUtil.equals(iter.next(), uri)) { |
+ if (file.equals(fAlreadyCollected.get(uri))) { |
+ return true; // another FileMatch for an IFile which already had matches |
+ } |
+ |
+ if (fIgnoredMatches == null) { |
+ fIgnoredMatches = new HashMap<URI, ArrayList<FileMatch>>(); |
+ } |
+ |
+ ArrayList<FileMatch> matches = fIgnoredMatches.get(uri); |
+ if (matches == null) { |
+ matches = new ArrayList<FileMatch>(); |
+ fIgnoredMatches.put(uri, matches); |
+ } |
+ matches.add(match); |
+ |
+ return false; |
+ } |
+ } |
+ |
+ fAlreadyCollected.put(uri, file); |
+ return true; |
+ } |
+ |
+} |