// 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.server.documents; import com.google.collide.dto.DocumentSelection; import com.google.collide.dto.server.DtoServerImpls.DocumentSelectionImpl; import com.google.collide.dto.server.DtoServerImpls.FilePositionImpl; import com.google.collide.shared.document.anchor.Anchor; import com.google.collide.shared.document.anchor.Anchor.RemovalStrategy; import com.google.collide.shared.document.anchor.AnchorType; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import java.util.List; import java.util.Map; /** * Helper that tracks the selection (cursor and base positions) for each user. * */ public class SelectionTracker { private class UserSelection { private final String clientId; private VersionedDocument document; private String resourceId; private Anchor cursorAnchor; private Anchor baseAnchor; private UserSelection(String clientId) { this.clientId = clientId; } private synchronized void teardown() { removeAnchors(); } private synchronized void markActive( final String resourceId, VersionedDocument document, DocumentSelection documentSelection) { if (this.document != document) { /* * The user switched files or the VersionedDocument instance swapped underneath us (in the * latter case, just checking the equality of FileEditSessionKeys would not be enough) */ removeAnchors(); this.resourceId = resourceId; this.document = document; } if (documentSelection != null) { // Cursor/selection has moved via explicit movement action moveOrCreateAnchors(document, documentSelection); } } private synchronized void moveOrCreateAnchors( VersionedDocument document, DocumentSelection documentSelection) { if (cursorAnchor != null) { document.moveAnchor(cursorAnchor, documentSelection.getCursorPosition().getLineNumber(), documentSelection.getCursorPosition().getColumn()); document.moveAnchor(baseAnchor, documentSelection.getBasePosition().getLineNumber(), documentSelection.getBasePosition().getColumn()); } else { cursorAnchor = document.addAnchor( CURSOR_ANCHOR_TYPE, documentSelection.getCursorPosition().getLineNumber(), documentSelection.getCursorPosition().getColumn()); cursorAnchor.setRemovalStrategy(RemovalStrategy.SHIFT); baseAnchor = document.addAnchor( BASE_ANCHOR_TYPE, documentSelection.getBasePosition().getLineNumber(), documentSelection.getBasePosition().getColumn()); baseAnchor.setRemovalStrategy(RemovalStrategy.SHIFT); } } private synchronized void removeAnchors() { if (cursorAnchor != null) { document.removeAnchor(cursorAnchor); cursorAnchor = null; } if (baseAnchor != null) { document.removeAnchor(baseAnchor); baseAnchor = null; } } @Override public boolean equals(Object obj) { return obj instanceof UserSelection && clientId.equals(((UserSelection) obj).clientId); } @Override public int hashCode() { return clientId.hashCode(); } } private static final AnchorType CURSOR_ANCHOR_TYPE = AnchorType.create(SelectionTracker.class, "cursor"); private static final AnchorType BASE_ANCHOR_TYPE = AnchorType.create(SelectionTracker.class, "base"); /** * All active selections, keyed by the user's gaia ID. */ private final Map<String, UserSelection> userSelections = Maps.newHashMap(); @VisibleForTesting SelectionTracker() { // TODO: Listen for client disconnections to remove selections. } /** * Releases all resources. */ public void close() { for (UserSelection selection : userSelections.values()) { selection.teardown(); } } /** * Called when the selection for a user changes. * * @param clientId the ID for the user's tab * @param resourceId the key for the file edit session that holds the active selection * @param documentSelection the position of the selection after any mutations currently being * processed */ public void selectionChanged(String clientId, String resourceId, VersionedDocument document, DocumentSelection documentSelection) { UserSelection selection = userSelections.get(clientId); if (selection == null) { selection = new UserSelection(clientId); userSelections.put(clientId, selection); } selection.markActive(resourceId, document, documentSelection); } public List<DocumentSelection> getDocumentSelections(String resourceId) { List<DocumentSelection> selections = Lists.newArrayList(); for (UserSelection userSelection : userSelections.values()) { if (userSelection.resourceId.equals(resourceId)) { Anchor cursorAnchor = userSelection.cursorAnchor; Anchor baseAnchor = userSelection.baseAnchor; if (cursorAnchor != null && baseAnchor != null) { FilePositionImpl basePosition = FilePositionImpl.make() .setColumn(baseAnchor.getColumn()).setLineNumber(baseAnchor.getLineNumber()); FilePositionImpl cursorPosition = FilePositionImpl.make().setColumn( cursorAnchor.getColumn()).setLineNumber(cursorAnchor.getLineNumber()); DocumentSelectionImpl selection = DocumentSelectionImpl.make() .setUserId(userSelection.clientId).setBasePosition(basePosition) .setCursorPosition(cursorPosition); selections.add(selection); } } } return selections; } public void removeSelection(String clientId) { UserSelection selection = userSelections.remove(clientId); if (selection != null) { selection.teardown(); } } }