/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.ide.api.editor.partition; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.che.ide.api.editor.text.BadLocationException; import org.eclipse.che.ide.api.editor.text.BadPositionCategoryException; import org.eclipse.che.ide.api.editor.text.Position; import org.eclipse.che.ide.api.editor.text.TypedPosition; import org.eclipse.che.ide.runtime.Assert; import org.eclipse.che.ide.util.loging.Log; /** Implementation for {@link DocumentPositionMap}. */ public class DocumentPositionMapImpl implements DocumentPositionMap { /** All positions managed by the document ordered by their start positions. */ private final Map<String, List<TypedPosition>> positions = new HashMap<>(); /** All positions managed by the document ordered by there end positions. */ private final Map<String, List<TypedPosition>> endPositions = new HashMap<>(); private int contentLength = 0; @Override public void addPositionCategory(final String category) { if (category == null) { return; } if (!containsPositionCategory(category)) { this.positions.put(category, new ArrayList<TypedPosition>()); this.endPositions.put(category, new ArrayList<TypedPosition>()); } } @Override public boolean containsPosition(String category, int offset, int length) { if (category == null) { return false; } final List<TypedPosition> list = this.positions.get(category); if (list == null) { return false; } final int size = list.size(); if (size == 0) { return false; } int index = computeIndexInPositionList(list, offset, true); if (index < size) { Position p = list.get(index); while (p != null && p.offset == offset) { if (p.length == length) { return true; } ++index; p = (index < size) ? (Position)list.get(index) : null; } } return false; } @Override public boolean containsPositionCategory(final String category) { if (category != null) { return this.positions.containsKey(category); } return false; } protected int computeIndexInPositionList(final List<TypedPosition> positions, final int offset, final boolean orderedByOffset) { if (positions.size() == 0) { return 0; } int left = 0; int right = positions.size() - 1; int mid = 0; Position p = null; while (left < right) { mid = (left + right) / 2; p = positions.get(mid); final int pOffset = getOffset(orderedByOffset, p); if (offset < pOffset) { if (left == mid) { right = left; } else { right = mid - 1; } } else if (offset > pOffset) { if (right == mid) { left = right; } else { left = mid + 1; } } else if (offset == pOffset) { left = right = mid; } } int pos = left; p = positions.get(pos); int pPosition = getOffset(orderedByOffset, p); if (offset > pPosition) { // append to the end pos++; } else { // entry will become the first of all entries with the same offset do { --pos; if (pos < 0) { break; } p = positions.get(pos); pPosition = getOffset(orderedByOffset, p); } while (offset == pPosition); ++pos; } Assert.isTrue(0 <= pos && pos <= positions.size()); return pos; } private int getOffset(boolean orderedByOffset, Position position) { if (orderedByOffset || position.getLength() == 0) { return position.getOffset(); } return position.getOffset() + position.getLength() - 1; } @Override public int computeIndexInCategory(final String category, final int offset) throws BadLocationException, BadPositionCategoryException { if (0 > offset || offset > this.contentLength) { throw new BadLocationException(); } final List<TypedPosition> c = this.positions.get(category); if (c == null) { throw new BadPositionCategoryException(); } return computeIndexInPositionList(c, offset, true); } @Override public List<TypedPosition> getPositions(String category) throws BadPositionCategoryException { if (category == null) { throw new BadPositionCategoryException(); } final List<TypedPosition> c = this.positions.get(category); if (c == null) { throw new BadPositionCategoryException(); } return new ArrayList<>(c); } @Override public List<String> getPositionCategories() { return new ArrayList<>(this.positions.keySet()); } @Override public void removePosition(String category, TypedPosition position) throws BadPositionCategoryException { if (position == null) { return; } if (category == null) { throw new BadPositionCategoryException(); } final List<TypedPosition> c = this.positions.get(category); if (c == null) { throw new BadPositionCategoryException(); } removeFromPositionsList(c, position, true); final List<TypedPosition> endPositions = this.endPositions.get(category); if (endPositions == null) { throw new BadPositionCategoryException(); } removeFromPositionsList(endPositions, position, false); } @Override public void removePositionCategory(String category) throws BadPositionCategoryException { if (category == null) { return; } if (!containsPositionCategory(category)) { throw new BadPositionCategoryException(); } this.positions.remove(category); this.endPositions.remove(category); } private void removeFromPositionsList(List<TypedPosition> positions, TypedPosition position, boolean orderedByOffset) { final int size = positions.size(); // Assume position is somewhere near it was before final int index = computeIndexInPositionList(positions, orderedByOffset ? position.offset : position.offset + position.length - 1, orderedByOffset); if (index < size && positions.get(index) == position) { positions.remove(index); return; } int back = index - 1; int forth = index + 1; while (back >= 0 || forth < size) { if (back >= 0) { if (position == positions.get(back)) { positions.remove(back); return; } back--; } if (forth < size) { if (position == positions.get(forth)) { positions.remove(forth); return; } forth++; } } } /* * @see org.eclipse.jface.text.IDocument#addPosition(java.lang.String, org.eclipse.jface.text.Position) */ @Override public void addPosition(String category, TypedPosition position) throws BadLocationException, BadPositionCategoryException { if ((0 > position.offset) || (0 > position.length) || (position.offset + position.length > this.contentLength)) { throw new BadLocationException(); } if (category == null) { throw new BadPositionCategoryException(); } final List<TypedPosition> list = this.positions.get(category); if (list == null) { throw new BadPositionCategoryException(); } list.add(computeIndexInPositionList(list, position.offset, true), position); final List<TypedPosition> endPositions = this.endPositions.get(category); if (endPositions == null) { throw new BadPositionCategoryException(); } endPositions.add(computeIndexInPositionList(endPositions, position.offset + position.length - 1, false), position); } @Override public void addPosition(final TypedPosition position) throws BadLocationException { try { addPosition(Categories.DEFAULT_CATEGORY, position); } catch (final BadPositionCategoryException e) { Log.warn(DocumentPositionMapImpl.class, "Should not happen: DEFAULT_CATEGORY is not a valid category!"); } } @Override public List<TypedPosition> getPositions(int offset, int length, boolean canStartBefore, boolean canEndAfter) throws BadPositionCategoryException { return getPositions(Categories.DEFAULT_CATEGORY, offset, length, canStartBefore, canEndAfter); } @Override public List<TypedPosition> getPositions(String category, int offset, int length, boolean canStartBefore, boolean canEndAfter) throws BadPositionCategoryException { if (canStartBefore && canEndAfter || (!canStartBefore && !canEndAfter)) { List<TypedPosition> documentPositions; if (canStartBefore && canEndAfter) { if (offset < this.contentLength / 2) { documentPositions = getStartingPositions(category, 0, offset + length); } else { documentPositions = getEndingPositions(category, offset, this.contentLength - offset + 1); } } else { documentPositions = getStartingPositions(category, offset, length); } final List<TypedPosition> list = new ArrayList<TypedPosition>(documentPositions.size()); final Position region = new Position(offset, length); for (final TypedPosition position: documentPositions) { if (isWithinRegion(region, position, canStartBefore, canEndAfter)) { list.add(position); } } return list; } else if (canStartBefore) { final List<TypedPosition> list = getEndingPositions(category, offset, length); return list; } else { Assert.isLegal(canEndAfter && !canStartBefore); final List<TypedPosition> list = getStartingPositions(category, offset, length); return list; } } private List<TypedPosition> getEndingPositions(String category, int offset, int length) throws BadPositionCategoryException { final List<TypedPosition> positions = endPositions.get(category); if (positions == null) { throw new BadPositionCategoryException(); } final int indexStart = computeIndexInPositionList(positions, offset, false); final int indexEnd = computeIndexInPositionList(positions, offset + length, false); return positions.subList(indexStart, indexEnd); } private List<TypedPosition> getStartingPositions(String category, int offset, int length) throws BadPositionCategoryException { final List<TypedPosition> categoryPositions = positions.get(category); if (categoryPositions == null) { throw new BadPositionCategoryException(); } final int indexStart = computeIndexInPositionList(categoryPositions, offset, true); final int indexEnd = computeIndexInPositionList(categoryPositions, offset + length, true); return categoryPositions.subList(indexStart, indexEnd); } private boolean isWithinRegion(Position region, Position position, boolean canStartBefore, boolean canEndAfter) { if (canStartBefore && canEndAfter) { return region.overlapsWith(position.getOffset(), position.getLength()); } else if (canStartBefore) { return region.includes(position.getOffset() + position.getLength() - 1); } else if (canEndAfter) { return region.includes(position.getOffset()); } else { final int start = position.getOffset(); return region.includes(start) && region.includes(start + position.getLength() - 1); } } @Override public void setContentLength(final int newLength) { this.contentLength = newLength; } @Override public void resetPositions() { for (final List<TypedPosition> list : positions.values()) { list.clear(); } for (final List<TypedPosition> list : endPositions.values()) { list.clear(); } } }