diff --git a/platform/platform-resources-en/src/messages/ActionsBundle.properties b/platform/platform-resources-en/src/messages/ActionsBundle.properties index 1722b46d655a..1c05af27102c 100644 --- a/platform/platform-resources-en/src/messages/ActionsBundle.properties +++ b/platform/platform-resources-en/src/messages/ActionsBundle.properties @@ -1014,6 +1014,8 @@ action.Vcs.ShowDiffAction.text=Show Changes action.Vcs.ShowDiffAction.description=Show changes action.Vcs.RollbackChanges.text=Rollback Changes action.Vcs.RollbackChanges.description=Rollback changes +action.RollbackLineStatusChanges.text=Rollback +action.RollbackLineStatusChanges.description=Rollback selected local changes action.Vcs.EditSourceAction.text=Edit Source action.Vcs.EditSourceAction.description=Edit source action.Vcs.ExcludeAction.text=Exclude from Commit diff --git a/platform/platform-resources/src/idea/VcsActions.xml b/platform/platform-resources/src/idea/VcsActions.xml index 935bcc7b4df8..dc15f19282bf 100644 --- a/platform/platform-resources/src/idea/VcsActions.xml +++ b/platform/platform-resources/src/idea/VcsActions.xml @@ -265,6 +265,9 @@ + + + diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/LineStatusTracker.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/LineStatusTracker.java index 1bf0f6f7390c..c6f0dac39d0f 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/LineStatusTracker.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/LineStatusTracker.java @@ -578,6 +578,116 @@ public class LineStatusTracker { } } + public void rollbackChanges(@NotNull SegmentTree lines) { + myApplication.assertWriteAccessAllowed(); + + synchronized (myLock) { + List affectedRanges = new ArrayList(); + + boolean wasEnd = false; + boolean simple = true; + for (Range range : myRanges) { + boolean check; + if (range.getOffset1() == range.getOffset2()) { + check = lines.check(range.getOffset1()); + } + else { + check = lines.check(range.getOffset1(), range.getOffset2()); + } + if (check) { + if (wasEnd) simple = false; + affectedRanges.add(range); + } + else { + if (!affectedRanges.isEmpty()) wasEnd = true; + } + } + + if (simple) { + rollbackChangesSimple(affectedRanges); + } + else { + rollbackChangesComplex(affectedRanges); + } + } + } + + private void rollbackChangesSimple(@NotNull List ranges) { + if (ranges.isEmpty()) return; + + Range first = ranges.get(0); + Range last = ranges.get(ranges.size() - 1); + + byte type = first == last ? first.getType() : Range.MODIFIED; + final Range merged = new Range(first.getOffset1(), last.getOffset2(), first.getUOffset1(), last.getUOffset2(), type); + + // We don't expect complex Insertion/Deletion operation - they shouldn't exist + assert type != Range.MODIFIED || (first.getOffset1() != last.getOffset2() && first.getUOffset1() != last.getUOffset2()); + + rollbackChanges(merged); + } + + private void rollbackChangesComplex(@NotNull List ranges) { + // We can't relay on assumption, that revert of a single change will not affect any other. + // This, among the others, is because of 'magic' ranges for revert, that will affect nearby lines implicitly. + // So it's dangerous to apply ranges ony-by-one and we have to create single atomic modification. + // Usage of Bulk mode will lead to full rebuild of tracker, and therefore will be slow.. + + if (ranges.isEmpty()) return; + if (ranges.size() == 1) { + rollbackChanges(ranges.get(0)); + return; + } + + Range first = ranges.get(0); + Range last = ranges.get(ranges.size() - 1); + + // We don't expect complex Insertion/Deletion operation - they shouldn't exist. + assert first != last && first.getOffset1() != last.getOffset2() && first.getUOffset1() != last.getUOffset2(); + + final int start = getCurrentTextRange(first).getStartOffset(); + final int end = getCurrentTextRange(last).getEndOffset(); + + StringBuilder builder = new StringBuilder(); + + int lastOffset = start; + for (Range range : ranges) { + TextRange textRange = getCurrentTextRange(range); + + builder.append(myDocument.getText(new TextRange(lastOffset, textRange.getStartOffset()))); + lastOffset = textRange.getEndOffset(); + + if (range.getType() == Range.MODIFIED) { + builder.append(getUpToDateContent(range)); + } + else if (range.getType() == Range.INSERTED) { + if (builder.length() > 0) { + builder.deleteCharAt(builder.length() - 1); + } + else { + lastOffset++; + } + } + else if (range.getType() == Range.DELETED) { + CharSequence content = getUpToDateContent(range); + if (range.getOffset2() == getLineCount(myDocument)) { + builder.append('\n').append(content); + } + else { + builder.append(content).append('\n'); + } + } + else { + throw new IllegalArgumentException("Unknown range type: " + range.getType()); + } + } + builder.append(myDocument.getText(new TextRange(lastOffset, end))); + + final String s = builder.toString(); + + myDocument.replaceString(start, end, s); + } + public CharSequence getUpToDateContent(@NotNull Range range) { synchronized (myLock) { TextRange textRange = getUpToDateRange(range); @@ -589,17 +699,23 @@ public class LineStatusTracker { @NotNull TextRange getCurrentTextRange(@NotNull Range range) { - return getRange(range.getType(), range.getOffset1(), range.getOffset2(), Range.DELETED, myDocument); + return getRange(range.getOffset1(), range.getOffset2(), myDocument); } @NotNull TextRange getUpToDateRange(@NotNull Range range) { - return getRange(range.getType(), range.getUOffset1(), range.getUOffset2(), Range.INSERTED, myUpToDateDocument); + return getRange(range.getUOffset1(), range.getUOffset2(), myUpToDateDocument); } + /** + * Return affected range, without non-internal '\n' + * so if last line is not empty, the last symbol will be not '\n' + *

+ * So we consider '\n' not as a part of line, but a separator between lines + */ @NotNull - private static TextRange getRange(byte rangeType, int offset1, int offset2, byte emptyRangeCondition, Document document) { - if (rangeType == emptyRangeCondition) { + private static TextRange getRange(int offset1, int offset2, @NotNull Document document) { + if (offset1 == offset2) { int lineStartOffset = offset1 < getLineCount(document) ? document.getLineStartOffset(offset1) : document.getTextLength(); return new TextRange(lineStartOffset, lineStartOffset); } diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/RollbackLineStatusAction.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/RollbackLineStatusAction.java new file mode 100644 index 000000000000..59697f1222dd --- /dev/null +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/RollbackLineStatusAction.java @@ -0,0 +1,136 @@ +/* + * Copyright 2000-2010 JetBrains s.r.o. + * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 + * 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.intellij.openapi.vcs.ex; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vcs.VcsBundle; +import com.intellij.openapi.vcs.impl.LineStatusTrackerManager; +import com.intellij.openapi.vfs.ReadonlyStatusHandler; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class RollbackLineStatusAction extends DumbAwareAction { + public RollbackLineStatusAction() { + super("Rollback", "Rollback selected changes", AllIcons.Actions.Reset); + } + + @Override + public void update(AnActionEvent e) { + Project project = e.getProject(); + if (project == null) { + e.getPresentation().setEnabled(false); + return; + } + Editor editor = CommonDataKeys.EDITOR.getData(e.getDataContext()); + if (editor == null) { + e.getPresentation().setEnabled(false); + return; + } + LineStatusTracker tracker = LineStatusTrackerManager.getInstance(project).getLineStatusTracker(editor.getDocument()); + if (tracker == null) { + e.getPresentation().setEnabled(false); + return; + } + e.getPresentation().setEnabled(true); + } + + @Override + public void actionPerformed(AnActionEvent e) { + Project project = e.getProject(); + Editor editor = CommonDataKeys.EDITOR.getData(e.getDataContext()); + LineStatusTracker tracker = LineStatusTrackerManager.getInstance(project).getLineStatusTracker(editor.getDocument()); + if (tracker == null) return; + + rollback(tracker, editor, null); + } + + protected static void rollback(@NotNull LineStatusTracker tracker, @Nullable Editor editor, @Nullable Range range) { + if (range != null) { + doRollback(tracker, range); + return; + } + + if (editor == null) return; + Document document = editor.getDocument(); + int totalLines = getLineCount(document); + + SegmentTree lines = new SegmentTree(totalLines + 1); + + List carets = editor.getCaretModel().getAllCarets(); + for (Caret caret : carets) { + if (caret.hasSelection()) { + int line1 = editor.offsetToLogicalPosition(caret.getSelectionStart()).line; + int line2 = editor.offsetToLogicalPosition(caret.getSelectionEnd()).line; + lines.mark(line1, line2 + 1); + if (caret.getSelectionEnd() == document.getTextLength()) lines.mark(totalLines); + } + else { + lines.mark(caret.getLogicalPosition().line); + if (caret.getOffset() == document.getTextLength()) lines.mark(totalLines); + } + } + + doRollback(tracker, lines); + } + + private static void doRollback(@NotNull final LineStatusTracker tracker, @NotNull final Range range) { + execute(tracker, new Runnable() { + @Override + public void run() { + tracker.rollbackChanges(range); + } + }); + } + + private static void doRollback(@NotNull final LineStatusTracker tracker, @NotNull final SegmentTree lines) { + execute(tracker, new Runnable() { + @Override + public void run() { + tracker.rollbackChanges(lines); + } + }); + } + + private static void execute(@NotNull final LineStatusTracker tracker, @NotNull final Runnable task) { + // TODO: is there possible data races? + CommandProcessor.getInstance().executeCommand(tracker.getProject(), new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + if (!tracker.getDocument().isWritable()) { + final ReadonlyStatusHandler.OperationStatus operationStatus = ReadonlyStatusHandler + .getInstance(tracker.getProject()).ensureFilesWritable(tracker.getVirtualFile()); + if (operationStatus.hasReadonlyFiles()) return; + } + task.run(); + } + }); + } + }, VcsBundle.message("command.name.rollback.change"), null); + } + + private static int getLineCount(@NotNull Document document) { + return Math.max(document.getLineCount(), 1); + } +} diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/RollbackLineStatusRangeAction.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/RollbackLineStatusRangeAction.java index ed5846d20ade..f1795b5efc3c 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/RollbackLineStatusRangeAction.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/RollbackLineStatusRangeAction.java @@ -12,41 +12,28 @@ */ package com.intellij.openapi.vcs.ex; -import com.intellij.icons.AllIcons; import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.vcs.VcsBundle; -import com.intellij.openapi.vfs.ReadonlyStatusHandler; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -/** -* @author irengrig -*/ -public class RollbackLineStatusRangeAction extends BaseLineStatusRangeAction { - public RollbackLineStatusRangeAction(final LineStatusTracker lineStatusTracker, final Range range, final Editor editor) { - super(VcsBundle.message("action.name.rollback"), AllIcons.Actions.Reset, lineStatusTracker, range); +public class RollbackLineStatusRangeAction extends RollbackLineStatusAction { + @NotNull private final LineStatusTracker myTracker; + @Nullable private final Editor myEditor; + @NotNull private final Range myRange; + + public RollbackLineStatusRangeAction(@NotNull LineStatusTracker tracker, @NotNull Range range, @Nullable Editor editor) { + myTracker = tracker; + myEditor = editor; + myRange = range; } - public boolean isEnabled() { - return true; + @Override + public void update(AnActionEvent e) { + e.getPresentation().setEnabled(true); } public void actionPerformed(final AnActionEvent e) { - CommandProcessor.getInstance().executeCommand(myLineStatusTracker.getProject(), new Runnable() { - public void run() { - ApplicationManager.getApplication().runWriteAction(new Runnable() { - public void run() { - if (!myLineStatusTracker.getDocument().isWritable()) { - final ReadonlyStatusHandler.OperationStatus operationStatus = ReadonlyStatusHandler - .getInstance(myLineStatusTracker.getProject()).ensureFilesWritable(myLineStatusTracker.getVirtualFile()); - if (operationStatus.hasReadonlyFiles()) return; - } - myLineStatusTracker.rollbackChanges(myRange); - } - }); - } - }, VcsBundle.message("command.name.rollback.change"), null); - + rollback(myTracker, myEditor, myRange); } } diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/SegmentTree.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/SegmentTree.java new file mode 100644 index 000000000000..68307129c13e --- /dev/null +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/SegmentTree.java @@ -0,0 +1,116 @@ +package com.intellij.openapi.vcs.ex; + +import org.jetbrains.annotations.Nullable; + +public class SegmentTree { + private final int myActualLength; + private final int myLength; + + private final Node myRoot; + + public SegmentTree(int length) { + myActualLength = length; + myLength = toUpperSquare(length); + myRoot = new Node(); + } + + public void mark(int pos) { + mark(pos, pos + 1); + } + + public void mark(int start, int end) { + start = correct(0, myActualLength, start); + end = correct(0, myActualLength, end); + + myRoot.mark(0, myLength, start, end); + } + + public boolean check(int pos) { + return check(pos, pos + 1); + } + + public boolean check(int start, int end) { + start = correct(0, myActualLength, start); + end = correct(0, myActualLength, end); + + return myRoot.check(0, myLength, start, end); + } + + private static int toUpperSquare(int value) { + int high = Integer.highestOneBit(value); + return high == value ? value : high * 2; + } + + private static class Node { + @Nullable + public Node myLeft; + + @Nullable + public Node myRight; + + public boolean myMarked; + + public boolean mark(int thisStart, int thisEnd, int start, int end) { + if (myLeft == null && myMarked) return true; + + if (start == end) return false; + + myMarked = true; + + if (thisStart == start && thisEnd == end) { + myLeft = null; + myRight = null; + return true; + } + + if (myLeft == null) { + myLeft = new Node(); + myRight = new Node(); + } + + int mid = thisStart + (thisEnd - thisStart) / 2; + int start1 = correct(thisStart, mid, start); + int end1 = correct(thisStart, mid, end); + int start2 = correct(mid, thisEnd, start); + int end2 = correct(mid, thisEnd, end); + + boolean marked = true; + marked &= myLeft.mark(thisStart, mid, start1, end1); + marked &= myRight.mark(mid, thisEnd, start2, end2); + + if (marked) { + myLeft = null; + myRight = null; + } + + return marked; + } + + public boolean check(int thisStart, int thisEnd, int start, int end) { + if (start == end) return false; + + if (thisStart == start && thisEnd == end) { + return myMarked; + } + + if (myLeft == null) return myMarked; + + int mid = thisStart + (thisEnd - thisStart) / 2; + int start1 = correct(thisStart, mid, start); + int end1 = correct(thisStart, mid, end); + int start2 = correct(mid, thisEnd, start); + int end2 = correct(mid, thisEnd, end); + + if (myLeft.check(thisStart, mid, start1, end1)) return true; + if (myRight.check(mid, thisEnd, start2, end2)) return true; + + return false; + } + } + + private static int correct(int start, int end, int value) { + if (value < start) return start; + if (value > end) return end; + return value; + } +}