// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.google.collide.client.code.debugging;
import com.google.collide.client.util.ScheduledCommandExecutor;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.Document;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.LineInfo;
import com.google.collide.shared.document.TextChange;
import com.google.collide.shared.document.anchor.Anchor;
import com.google.collide.shared.document.anchor.AnchorManager;
import com.google.collide.shared.document.anchor.AnchorType;
import com.google.collide.shared.document.anchor.AnchorUtils;
import com.google.collide.shared.util.JsonCollections;
import java.util.Comparator;
/**
* Handles an array of breakpoints anchored into a {@link Document}.
*/
class AnchoredBreakpoints {
private static final AnchorType BREAKPOINT_ANCHOR_TYPE = AnchorType.create(
AnchoredBreakpoints.class, "breakpoint");
/**
* A listener that is called when a line that has a breakpoint set on it
* changes.
*/
public interface BreakpointDescriptionListener {
void onBreakpointDescriptionChange(Breakpoint breakpoint, String newText);
}
private static final Comparator<Anchor> anchorComparator = new Comparator<Anchor>() {
@Override
public int compare(Anchor o1, Anchor o2) {
Breakpoint b1 = o1.getValue();
Breakpoint b2 = o2.getValue();
return b1.getLineNumber() - b2.getLineNumber();
}
};
private abstract static class AnchorBatchCommandExecutor extends ScheduledCommandExecutor {
private JsonArray<Anchor> updatedAnchors = JsonCollections.createArray();
@Override
protected final void execute() {
if (updatedAnchors.isEmpty()) {
return;
}
JsonArray<Anchor> anchors = updatedAnchors;
updatedAnchors = JsonCollections.createArray();
executeBatchCommand(anchors);
}
protected abstract void executeBatchCommand(JsonArray<Anchor> anchors);
public void addAnchor(Anchor anchor) {
if (BREAKPOINT_ANCHOR_TYPE.equals(anchor.getType()) && !updatedAnchors.contains(anchor)) {
updatedAnchors.add(anchor);
scheduleDeferred();
}
}
public void removeAnchor(Anchor anchor) {
updatedAnchors.remove(anchor);
}
public void teardown() {
updatedAnchors.clear();
cancel();
}
}
private final AnchorBatchCommandExecutor anchorsLineShiftedCommand =
new AnchorBatchCommandExecutor() {
@Override
protected void executeBatchCommand(JsonArray<Anchor> anchors) {
applyMovedAnchors(anchors);
}
};
private final Anchor.ShiftListener anchorShiftListener = new Anchor.ShiftListener() {
@Override
public void onAnchorShifted(Anchor anchor) {
anchorsLineShiftedCommand.addAnchor(anchor);
}
};
private final AnchorBatchCommandExecutor anchorChangedCommand = new AnchorBatchCommandExecutor() {
@Override
protected void executeBatchCommand(JsonArray<Anchor> anchors) {
applyUpdatedAnchors(anchors);
}
};
/**
* Listener of the document text changes to track breakpoint descriptions.
*/
private class TextListenerImpl implements Document.TextListener, AnchorManager.AnchorVisitor {
@Override
public void onTextChange(Document document, JsonArray<TextChange> textChanges) {
for (int i = 0, n = textChanges.size(); i < n; ++i) {
TextChange textChange = textChanges.get(i);
Line line = textChange.getLine();
Line stopAtLine = textChange.getEndLine().getNextLine();
while (line != stopAtLine) {
AnchorUtils.visitAnchorsOnLine(line, this);
line = line.getNextLine();
}
}
}
@Override
public void visitAnchor(Anchor anchor) {
anchorChangedCommand.addAnchor(anchor);
}
}
private final Document document;
private final DebuggingModel debuggingModel;
private final JsonArray<Breakpoint> breakpoints = JsonCollections.createArray();
private final JsonArray<Anchor> anchors = JsonCollections.createArray();
private final Document.TextListener documentTextListener = new TextListenerImpl();
private BreakpointDescriptionListener breakpointDescriptionListener;
AnchoredBreakpoints(DebuggingModel debuggingModel, Document document) {
this.debuggingModel = debuggingModel;
this.document = document;
}
void teardown() {
document.getTextListenerRegistrar().remove(documentTextListener);
for (int i = 0, n = anchors.size(); i < n; ++i) {
detachAnchor(anchors.get(i));
}
breakpoints.clear();
anchors.clear();
anchorsLineShiftedCommand.teardown();
anchorChangedCommand.teardown();
breakpointDescriptionListener = null;
}
public void setBreakpointDescriptionListener(BreakpointDescriptionListener listener) {
this.breakpointDescriptionListener = listener;
}
public Anchor anchorBreakpoint(Breakpoint breakpoint) {
LineInfo lineInfo = document.getLineFinder().findLine(breakpoint.getLineNumber());
Anchor anchor = document.getAnchorManager().createAnchor(BREAKPOINT_ANCHOR_TYPE,
lineInfo.line(), lineInfo.number(), AnchorManager.IGNORE_COLUMN);
anchor.setRemovalStrategy(Anchor.RemovalStrategy.SHIFT);
anchor.setValue(breakpoint);
anchor.getShiftListenerRegistrar().add(anchorShiftListener);
if (anchors.isEmpty()) {
document.getTextListenerRegistrar().add(documentTextListener);
}
breakpoints.add(breakpoint);
anchors.add(anchor);
return anchor;
}
public boolean removeBreakpoint(Breakpoint breakpoint) {
for (int i = 0, n = breakpoints.size(); i < n; ++i) {
if (breakpoint.equals(breakpoints.get(i))) {
detachAnchor(anchors.get(i));
breakpoints.remove(i);
anchors.remove(i);
if (anchors.isEmpty()) {
document.getTextListenerRegistrar().remove(documentTextListener);
}
return true;
}
}
return false;
}
public boolean contains(Breakpoint breakpoint) {
return breakpoints.contains(breakpoint);
}
public Breakpoint get(int index) {
return breakpoints.get(index);
}
public int size() {
return breakpoints.size();
}
private void detachAnchor(Anchor anchor) {
document.getAnchorManager().removeAnchor(anchor);
anchorsLineShiftedCommand.removeAnchor(anchor);
anchorChangedCommand.removeAnchor(anchor);
anchor.setValue(null);
anchor.getShiftListenerRegistrar().remove(anchorShiftListener);
}
private void applyMovedAnchors(JsonArray<Anchor> anchors) {
// We have to determine what breakpoint to move first in case if we have a
// few consecutive breakpoints set. For example, if we have breakpoints at
// lines 3,4,5, and we insert a new line at the beginning of the document,
// then we should start updating the breakpoints from the end:
// 5->6, 4->5, 3->4, so that not to mess up with the original breakpoints.
// Below is a simple strategy to ensure the correct iteration order in most
// cases (when all breakpoints move in the same direction). In those rare
// cases when we might loose some breakpoints (for example, due to
// collaborative editing), we consider this tolerable.
anchors.sort(anchorComparator);
int deltaSum = 0;
for (int i = 0, n = anchors.size(); i < n; ++i) {
Anchor anchor = anchors.get(i);
Breakpoint breakpoint = anchor.getValue();
deltaSum += anchor.getLineNumber() - breakpoint.getLineNumber();
}
for (int i = 0, n = anchors.size(); i < n; ++i) {
Anchor anchor = anchors.get(deltaSum < 0 ? i : n - 1 - i);
Breakpoint oldBreakpoint = anchor.getValue();
Breakpoint newBreakpoint = new Breakpoint.Builder(oldBreakpoint)
.setLineNumber(anchor.getLineNumber())
.build();
debuggingModel.updateBreakpoint(oldBreakpoint, newBreakpoint);
}
}
private void applyUpdatedAnchors(JsonArray<Anchor> anchors) {
if (breakpointDescriptionListener == null) {
return;
}
for (int i = 0, n = anchors.size(); i < n; ++i) {
Anchor anchor = anchors.get(i);
Breakpoint breakpoint = anchor.getValue();
String newText = anchor.getLine().getText();
breakpointDescriptionListener.onBreakpointDescriptionChange(breakpoint, newText);
}
}
}