IDEA-110611 diff: Ability to revert lines in selection

This commit is contained in:
Aleksey Pivovarov
2014-07-22 13:49:56 +04:00
parent b6cd05a8e6
commit 950a80a0d8
6 changed files with 392 additions and 32 deletions
@@ -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
@@ -265,6 +265,9 @@
<add-to-group group-id="CloseEditorsGroup" anchor="before" relative-to-action="CloseAllUnpinnedEditors"/>
</action>
<action id="RollbackLineStatusChanges" class="com.intellij.openapi.vcs.ex.RollbackLineStatusAction">
</action>
<action id="WelcomeScreen.GetFromVcs" class="com.intellij.openapi.wm.impl.welcomeScreen.GetFromVcsAction"
icon="AllIcons.General.GetProjectfromVCS">
<add-to-group group-id="WelcomeScreen.QuickStart" anchor="first"/>
@@ -578,6 +578,116 @@ public class LineStatusTracker {
}
}
public void rollbackChanges(@NotNull SegmentTree lines) {
myApplication.assertWriteAccessAllowed();
synchronized (myLock) {
List<Range> affectedRanges = new ArrayList<Range>();
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<Range> 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<Range> 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'
* <p/>
* 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);
}
@@ -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<Caret> 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);
}
}
@@ -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);
}
}
@@ -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;
}
}