/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.waveprotocol.wave.client.editor.util; import java.util.ArrayList; import java.util.function.Consumer; import org.waveprotocol.wave.client.editor.EditorContext; import org.waveprotocol.wave.client.editor.content.misc.CaretAnnotations; import org.waveprotocol.wave.model.document.MutableAnnotationSet; import org.waveprotocol.wave.model.document.RangedAnnotation; import org.waveprotocol.wave.model.document.ReadableAnnotationSet; import org.waveprotocol.wave.model.document.util.Annotations; import org.waveprotocol.wave.model.document.util.Range; import org.waveprotocol.wave.model.document.util.RangedAnnotationImpl; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.util.Preconditions; import org.waveprotocol.wave.model.util.ReadableStringSet; import org.waveprotocol.wave.model.util.ReadableStringSet.Proc; import org.waveprotocol.wave.model.util.ValueUtils; public class EditorAnnotationUtil { /** Un-constructable utility class. */ private EditorAnnotationUtil() {} /** * Finds the first of the given keys that covers the entire selected range, and returns its value. * * @param editor Contains the non-null selection and document * @param keys Keys to look through * @return The first value annotation that covers the range at one of the given keys, else null. */ public static String getFirstAnnotationOverSelection(EditorContext editor, String... keys) { Range range = Preconditions.checkNotNull( editor.getSelectionHelper().getSelectionRange(), "Editor must have selection").asRange(); return getFirstCoveringAnnotationOverRange(editor.getDocument(), editor.getCaretAnnotations(), keys, range.getStart(), range.getEnd()); } /** * Looks through a list of annotation keys, finding the first to cover the given range, * and return its annotation value. If the range is collapsed, it's assumed that the * desired annotation set is stored in the caret parameter. * * @param doc Document to check for annotations. * @param caret Annotations at the current selection. * @param keys Keys to look through * @param start Start of range to check * @param end End of range to check * @return The first value annotation that covers the range at one of the given keys, else null. */ public static String getFirstCoveringAnnotationOverRange(MutableAnnotationSet<String> doc, CaretAnnotations caret, String[] keys, int start, int end) { // iterate through each key: for (String key : keys) { String value = getAnnotationOverRangeIfFull(doc, caret, key, start, end); if (value != null) { return value; } } return null; // none found. } /** * Returns an annotation over the selected range only if the entire range has a single annotation. * If the annotation changes or if the range is not annotated (annotated with null), returns null. * * @param editor Editor whose annotations are to be checked, with non-null selection and doc. * @param key Key of annotation to retrieve */ public static String getAnnotationOverSelectionIfFull(EditorContext editor, String key) { Range range = Preconditions.checkNotNull( editor.getSelectionHelper().getSelectionRange(), "Editor must have selection").asRange(); return getAnnotationOverRangeIfFull(editor.getDocument(), editor.getCaretAnnotations(), key, range.getStart(), range.getEnd()); } /** * Returns an annotation over a range only if the entire range has a single annotation. * If the annotation changes or if the range is not annotated (annotated with null), returns null. * If the range is collapsed it's assumed that the desired annotation set is in the caret param. * * @param doc Document to check for annotations. * @param caret Annotations at the current selection. * @param key Key of annotation to retrieve * @param start Start offset of range. * @param end End offset of range. */ public static String getAnnotationOverRangeIfFull(MutableAnnotationSet<String> doc, CaretAnnotations caret, String key, int start, int end) { if (start == end) { // Try to use the information about the cursor, even if it doesn't match // where the selection is. return caret.getAnnotation(key); } String currentValue = doc.getAnnotation(start, key); if (doc.firstAnnotationChange(start, end, key, currentValue) == -1) { // no change, fully annotated return currentValue; } // change is found, so return: return null; } /** * Sets the annotation key to a particular value over the entire selected range in an editor. * * @param editor Editor to set the annotation, with non-null selection and doc. * @param key Annotation key to set. * @param value Annotation value to set key to. */ public static void setAnnotationOverSelection(EditorContext editor, String key, String value) { Range range = Preconditions.checkNotNull( editor.getSelectionHelper().getSelectionRange(), "Editor must have selection").asRange(); setAnnotationOverRange(editor.getDocument(), editor.getCaretAnnotations(), key, value, range.getStart(), range.getEnd()); } /** * Sets the annotation key to a particular value over an entire range. * If the range is collapsed it's assumed that the desired annotation set is in the caret param. * * @param doc Document to set the annotation in. * @param caret Collapsed-range annotations. * @param key key of annotation to set. * @param value value to set annotation to. * @param start start of range to set over. * @param end end of range to set over. */ public static void setAnnotationOverRange(MutableAnnotationSet<String> doc, CaretAnnotations caret, String key, String value, int start, int end) { // simple switch depending on whether the range is collapsed: if (start == end) { caret.setAnnotation(key, value); } else { doc.setAnnotation(start, end, key, value); } } /** * Clears all annotations for a set of keys over the current selected range. * * @param editor Editor whose annotations are to be cleared, with non-null selection and doc. * @param keys List of annotation keys to clear * @return true if annotations were actually changed */ public static boolean clearAnnotationsOverSelection(EditorContext editor, String... keys) { Range range = Preconditions.checkNotNull( editor.getSelectionHelper().getSelectionRange(), "Editor must have selection").asRange(); return clearAnnotationsOverRange(editor.getDocument(), editor.getCaretAnnotations(), keys, range.getStart(), range.getEnd()); } /** * Clears all annotations over a particular range in the editor's document. * If the range is collapsed it's assumed that the desired annotation set is in the caret param. * * @param doc Document to check for annotations. * @param caret Annotations at the current collapsed range. * @param keys List of annotation keys to clear * @param start Start offset of range. * @param end End offset of range. * @return true if annotations were actually changed */ public static boolean clearAnnotationsOverRange(MutableAnnotationSet<String> doc, CaretAnnotations caret, String[] keys, int start, int end) { boolean wasRemoved = false; if (start == end) { // clear from caret annotation if collapsed range for (String key : keys) { if (caret.getAnnotation(key) != null) { caret.setAnnotation(key, null); // remove if present wasRemoved = true; } } } else { // clear from the entire range for (String key : keys) { if (doc.firstAnnotationChange(start, end, key, null) != -1) { doc.setAnnotation(start, end, key, null); // remove if present wasRemoved = true; } } } return wasRemoved; } /** * Same as {{@link #clearAnnotationsOverRange(MutableAnnotationSet, CaretAnnotations, String[], int, int)} * but allowing params keys as ReadableStringSet */ public static void clearAnnotationsOverRange(MutableAnnotationSet<String> doc, CaretAnnotations caret, ReadableStringSet keys, int start, int end) { if (start == end) { // clear from caret annotation if collapsed range keys.each(new Proc(){ @Override public void apply(String key) { if (caret.getAnnotation(key) != null) { caret.setAnnotation(key, null); // remove if present } } }); } else { // clear from the entire range keys.each(new Proc(){ @Override public void apply(String key) { if (doc.firstAnnotationChange(start, end, key, null) != -1) { doc.setAnnotation(start, end, key, null); // remove if present } } }); } } /** * Finds the range of an adjacent or containing non-null range of contiguous * value for a given annotation key. * * If there are two ranges (the given location being at their boundary), then * prefer the one to the right. * * If there are no ranges (the key is null on either side of the location), * null is returned. * * @param doc * @param key * @param location * @return the range, or null if none found */ public static <V> Range getEncompassingAnnotationRange( final ReadableAnnotationSet<V> doc, String key, int location) { V value = doc.getAnnotation(location, key); if (value == null && location > 0) { value = doc.getAnnotation(location - 1, key); } if (value == null) { return null; } int start = doc.lastAnnotationChange(0, location, key, value); int end = doc.firstAnnotationChange(location, doc.size(), key, value); assert start < end : "Range should not be collapsed"; return new Range(start, end); } /** * Given the editor state, this examines the current caret annotations and adds any that * can be inferred from the position, given the alignment type. * * @param doc Document to check for annotations. * @param caret Current annotation styles at the caret. * @param keys Keys to supplement over the caret styles. * @param location Location of the caret in the document. * @param leftAlign Whether the annotations come from the left or right. */ public static void supplementAnnotations(final MutableAnnotationSet<String> doc, final CaretAnnotations caret, final ReadableStringSet keys, final int location, final boolean leftAlign) { // by default, everything inherits from the left, so for now, no need! if (leftAlign) { return; } // supplement anything that's missing and different: keys.each(new Proc() { @Override public void apply(String key) { if (!caret.hasAnnotation(key)) { String newValue = Annotations.getAlignedAnnotation(doc, location, key, leftAlign); String oldValue = doc.getAnnotation(location - 1, key); if (!ValueUtils.equal(newValue, oldValue)) { caret.setAnnotation(key, newValue); } } } }); } /** * Set an annotation in the interval, if there is any annotation (with same key) within the interval, add the value to it. * * @param doc Document * @param key the annotation key * @param value the annotation value * @param start start location * @param end end location */ public static void setAnnotationWithOverlap(final MutableAnnotationSet<String> doc, String key, String value, int start, int end) { final int[] rRange = { start, end }; doc.rangedAnnotations(start, end, CollectionUtils.newStringSet(key)) .forEach(new Consumer<RangedAnnotation<String>>(){ @Override public void accept(RangedAnnotation<String> anot) { if (!anot.key().equals(key) || anot.value() == null) return; if (rRange[0] >= rRange[1]) return; if (anot.start() <= rRange[0] && anot.end() < rRange[1]) { // Case 1 // // |------ anot -----| // |-------- range ----- // // results: // // anot anot+new // |----|------------|---- range' // int s1 = anot.start(); int e1 = rRange[0]-1; int s2 = rRange[0]; int e2 = anot.end(); if (s1 < e1) { doc.setAnnotation(s1, e1, key, anot.value()); } if (s2 < e2) { doc.setAnnotation(s1, e1, key, anot.value()+","+value); } rRange[0] = e2+1; } else if (rRange[0] <= anot.start() && anot.end() <= rRange[1]) { // Case 2 // // |------ anot -----| // |-------- range ------------------- // // results: // // new anot+new // |-------|-----------------|---- range' // int s1 = rRange[0]; int e1 = anot.start()-1; int s2 = anot.start(); int e2 = anot.end(); if (s1 < e1) { doc.setAnnotation(s1, e1, key, value); } if (s2 < e2) { doc.setAnnotation(s2, e2, key, value+","+anot.value()); } rRange[0] = e2+1; } else if (rRange[0] <= anot.start() && rRange[1] < anot.end()) { // Case 3 // // |-------- anot ------- // |-------- range -----| // // results: // // new anot+new // |-------|------------|---- anot // int s1 = rRange[0]; int e1 = anot.start()-1; int s2 = anot.start(); int e2 = rRange[1]; if (s1 < e1) doc.setAnnotation(s1, s1, key, value); if (s2 < e2) doc.setAnnotation(s2, e2, key, anot.value()+","+value); // the last part of 'anot' will remain cause the // setAnnotation() logic rRange[0] = e2+1; } } }); // if there is still a range not overlapped, create annotation if (rRange[0] < rRange[1]) doc.setAnnotation(rRange[0], rRange[1], key, value); } /** * Collect all annotations with same key and containing same value in the provided range * * @param doc Document * @param key the annotation key * @param value the annotation value * @param start start location * @param end end location * @return */ public static Iterable<RangedAnnotation<String>> getAnnotationSpread(final MutableAnnotationSet<String> doc, String key, String value, int start, int end) { final ArrayList<RangedAnnotation<String>> resultSet = new ArrayList<RangedAnnotation<String>>(); doc.rangedAnnotations(start, end, CollectionUtils.newStringSet(key)) .forEach(new Consumer<RangedAnnotation<String>>(){ @Override public void accept(RangedAnnotation<String> anot) { if (anot.key().equals(key) && anot.value() != null && anot.value().contains(value)) { resultSet.add(new RangedAnnotationImpl<String>(anot)); } } }); return resultSet; } }