com.google.gapid.views.Formatter.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gapid.views.Formatter.java

Source

/*
 * Copyright (C) 2017 Google Inc.
 *
 * 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.gapid.views;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.gapid.proto.service.memory.MemoryProtos.PoolNames;
import com.google.gapid.rpclib.binary.BinaryObject;
import com.google.gapid.rpclib.schema.AnyType;
import com.google.gapid.rpclib.schema.Array;
import com.google.gapid.rpclib.schema.Constant;
import com.google.gapid.rpclib.schema.ConstantSet;
import com.google.gapid.rpclib.schema.Dynamic;
import com.google.gapid.rpclib.schema.Entity;
import com.google.gapid.rpclib.schema.Field;
import com.google.gapid.rpclib.schema.Interface;
import com.google.gapid.rpclib.schema.Map;
import com.google.gapid.rpclib.schema.Method;
import com.google.gapid.rpclib.schema.Pointer;
import com.google.gapid.rpclib.schema.Primitive;
import com.google.gapid.rpclib.schema.Slice;
import com.google.gapid.rpclib.schema.Struct;
import com.google.gapid.rpclib.schema.Type;
import com.google.gapid.service.atom.DynamicAtom;
import com.google.gapid.service.memory.MemoryPointer;
import com.google.gapid.service.memory.MemoryRange;
import com.google.gapid.service.memory.MemorySliceInfo;
import com.google.gapid.service.memory.MemorySliceMetadata;
import com.google.gapid.service.snippets.CanFollow;
import com.google.gapid.service.snippets.Labels;
import com.google.gapid.service.snippets.SnippetObject;
import com.google.gapid.util.IntRange;
import com.google.gapid.widgets.Theme;

import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.StyledString.Styler;

import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

/**
 * Formats varies values to {@link StylingString StylingStrings}.
 */
public class Formatter {
    private Formatter() {
    }

    public static String toString(SnippetObject value, Type type) {
        NoStyleStylingString string = new NoStyleStylingString();
        format(value, type, string, null);
        return string.toString();
    }

    public static void format(SnippetObject value, Type type, StylingString string, Style style) {
        if (type instanceof Primitive) {
            format(value, (Primitive) type, string, style);
        } else if (type instanceof Struct) {
            format(value, (Struct) type, string, style);
        } else if (type instanceof Pointer) {
            format(value, (Pointer) type, string, style);
        } else if (type instanceof Interface) {
            format(value, (Interface) type, string, style);
        } else if (type instanceof Array) {
            format(value, (Array) type, string, style);
        } else if (type instanceof Slice) {
            format(value, (Slice) type, string, style);
        } else if (type instanceof Map) {
            format(value, (Map) type, string, style);
        } else if (type instanceof AnyType) {
            format(value, (AnyType) type, string, style);
        } else {
            format(value, string, style);
        }
    }

    private static void format(Object value, StylingString string, Style style) {
        if (value instanceof SnippetObject) {
            format((SnippetObject) value, string, style);
        } else if (value instanceof DynamicAtom) {
            format((DynamicAtom) value, string, style);
        } else if (value instanceof MemoryPointer) {
            format((MemoryPointer) value, string, style);
        } else if (value instanceof MemoryRange) {
            format((MemoryRange) value, string, style);
        } else {
            string.append(String.valueOf(value), style);
        }
    }

    private static void format(SnippetObject obj, Primitive type, StylingString string, Style style) {
        if (tryConstantFormat(obj, type, string, style)) {
            // successfully formatted as a constant.
            return;
        }

        Object value = obj.getObject();
        // Note: casting to Number in case the value was boxed into a different Number type.
        switch (type.getMethod().getValue()) {
        case Method.BoolValue:
            string.append(String.format("%b", (Boolean) value), style);
            break;
        case Method.StringValue:
            string.appendWithEllipsis(String.valueOf(value), style);
            break;
        case Method.Float32Value:
            string.append(String.format("%f", ((Number) value).floatValue()), style);
            break;
        case Method.Float64Value:
            string.append(String.format("%f", ((Number) value).doubleValue()), style);
            break;
        default:
            string.append(toJavaIntType(type.getMethod(), (Number) value).toString(), style);
            break;
        }
    }

    private static void format(SnippetObject value, @SuppressWarnings("unused") Struct type, StylingString string,
            Style style) {
        format(value, string, style);
    }

    private static void format(SnippetObject value, @SuppressWarnings("unused") Pointer type, StylingString string,
            Style style) {
        string.append("*", string.structureStyle());
        format(value, string, style);
    }

    private static void format(SnippetObject value, @SuppressWarnings("unused") Interface type,
            StylingString string, Style style) {
        string.append("$", string.structureStyle());
        format(value, string, style);
    }

    private static void format(SnippetObject obj, Array type, StylingString string, Style style) {
        Object value = obj.getObject();
        assert (value instanceof Object[]);
        format(obj, (Object[]) value, type.getValueType(), string, style);
    }

    private static void format(SnippetObject obj, Slice type, StylingString string, Style style) {
        Object value = obj.getObject();
        if (value instanceof Object[]) {
            format(obj, (Object[]) value, type.getValueType(), string, style);
        } else if (value instanceof byte[]) {
            format(obj, (byte[]) value, type.getValueType(), string, style);
        } else {
            assert (false);
        }
    }

    private static final int MAX_DISPLAY = 4;

    private static void format(SnippetObject obj, Object[] array, Type valueType, StylingString string,
            Style style) {
        int count = Math.min(array.length, MAX_DISPLAY);
        string.append("[", string.structureStyle());
        for (int index = 0; index < count; ++index) {
            if (index > 0) {
                string.append(",", string.structureStyle());
            }
            format(obj.elem(array[index]), valueType, string, style);
        }
        if (count < array.length) {
            string.append(", ...", string.structureStyle());
        }
        string.append("]", string.structureStyle());
    }

    private static void format(SnippetObject obj, byte[] array, Type valueType, StylingString string, Style style) {
        int count = Math.min(array.length, MAX_DISPLAY);
        string.append("[", string.structureStyle());
        for (int index = 0; index < count; ++index) {
            if (index > 0) {
                string.append(",", string.structureStyle());
            }
            format(obj.elem(array[index]), valueType, string, style);
        }
        if (count < array.length) {
            string.append(", ...", string.structureStyle());
        }
        string.append("]", string.structureStyle());
    }

    private static void format(SnippetObject value, Map type, StylingString string, Style style) {
        @SuppressWarnings("unchecked")
        java.util.Map<Object, Object> map = (java.util.Map<Object, Object>) value.getObject();
        Iterator<java.util.Map.Entry<Object, Object>> it = map.entrySet().iterator();

        string.append("{", string.structureStyle());
        // TODO - it looks like this is only ever used for empty maps?
        while (it.hasNext()) {
            java.util.Map.Entry<Object, Object> entry = it.next();
            format(value.key(entry), type.getKeyType(), string, style);
            string.append("=", string.structureStyle());
            SnippetObject paramValue = value.elem(entry);
            CanFollow follow = CanFollow.fromSnippets(paramValue.getSnippets());
            string.startLink(follow);
            format(paramValue, type.getValueType(), string, (follow == null) ? style : string.linkStyle());
            string.endLink();
            if (it.hasNext()) {
                string.append(", ", string.structureStyle());
            }
        }
        string.append("}", string.structureStyle());
    }

    private static void format(SnippetObject value, @SuppressWarnings("unused") AnyType type, StylingString string,
            Style style) {
        format(value, string, style);
    }

    private static void format(SnippetObject obj, StylingString string, Style style) {
        if (obj.getObject() instanceof Dynamic) {
            format(obj, (Dynamic) obj.getObject(), string, style);
            return;
        }
        format(obj.getObject(), string, style);
    }

    private static void format(SnippetObject obj, Dynamic dynamic, StylingString string, Style style) {
        MemoryPointer mp = tryMemoryPointer(dynamic);
        if (mp != null) {
            format(mp, string, style);
            return;
        }

        if (dynamic.getFieldCount() == 1 && dynamic.getFieldValue(0) instanceof MemorySliceInfo) {
            format((MemorySliceInfo) dynamic.getFieldValue(0), getSliceMetadata(dynamic), string, style);
            return;
        }

        string.append("{", string.structureStyle());
        for (int index = 0; index < dynamic.getFieldCount(); ++index) {
            if (index > 0) {
                string.append(", ", string.structureStyle());
            }
            Field field = dynamic.getFieldInfo(index);
            SnippetObject paramValue = obj.field(dynamic, index);
            CanFollow follow = CanFollow.fromSnippets(paramValue.getSnippets());
            Style paramStyle = (follow == null) ? style : string.linkStyle();
            string.startLink(follow);
            string.append(field.getName(), paramStyle);
            string.append(":", (follow == null) ? string.structureStyle() : string.linkStyle());
            format(paramValue, field.getType(), string, style);
            string.endLink();
        }
        string.append("}", string.structureStyle());
    }

    private static void format(MemorySliceInfo info, MemorySliceMetadata metaData, StylingString string,
            Style style) {
        if (metaData != null) {
            string.append(metaData.getElementTypeName(), style);
        }
        string.append("[", string.structureStyle());
        string.append(String.valueOf(info.getCount()), style);
        string.append("]", string.structureStyle());

        if (info.getPool() != PoolNames.Application_VALUE || info.getBase() != 0) {
            string.append(" (", string.structureStyle());
            MemoryPointer pointer = new MemoryPointer();
            pointer.setAddress(info.getBase());
            pointer.setPool(info.getPool());
            format(pointer, string, style);
            string.append(")", string.structureStyle());
        }
    }

    private static void format(MemoryPointer pointer, StylingString string, Style style) {
        if (PoolNames.Application_VALUE != pointer.getPool()) {
            if (pointer.getAddress() != 0) {
                string.append(toPointerString(pointer.getAddress()) + " ", style);
            }
            string.append("Pool: ", style);
            string.append("0x" + Long.toHexString(pointer.getPool()), style);
        } else {
            string.append(toPointerString(pointer.getAddress()), style);
        }
    }

    private static void format(MemoryRange range, StylingString string, Style style) {
        string.append(Long.toString(range.getSize()), style);
        string.append(" bytes at ", string.structureStyle());
        string.append(toPointerString(range.getBase()), style);
    }

    public static String toString(DynamicAtom atom) {
        NoStyleStylingString string = new NoStyleStylingString();
        format(atom, string, null);
        return string.toString();
    }

    public static void format(DynamicAtom atom, StylingString string, Style style) {
        string.append(atom.getName(), string.labelStyle());
        string.append("(", string.structureStyle());
        int resultIndex = atom.getResultIndex();
        int extrasIndex = atom.getExtrasIndex();
        boolean needComma = false;

        for (int i = 0; i < atom.getFieldCount(); ++i) {
            if (i == resultIndex || i == extrasIndex)
                continue;
            Field field = atom.getFieldInfo(i);
            if (needComma) {
                string.append(", ", string.structureStyle());
            }
            needComma = true;
            SnippetObject paramValue = SnippetObject.param(atom, i);
            CanFollow follow = CanFollow.fromSnippets(paramValue.getSnippets());
            Style paramStyle = (follow == null) ? style : string.linkStyle();
            string.startLink(follow);
            string.append(field.getDeclared(), paramStyle);
            string.append(":", (follow == null) ? string.structureStyle() : string.linkStyle());
            format(paramValue, field.getType(), string, paramStyle);
            string.endLink();
        }

        string.append(")", string.structureStyle());
        if (resultIndex >= 0) {
            string.append("->", string.structureStyle());
            SnippetObject paramValue = SnippetObject.param(atom, resultIndex);
            Field field = atom.getFieldInfo(resultIndex);
            CanFollow follow = CanFollow.fromSnippets(paramValue.getSnippets());
            string.startLink(follow);
            format(paramValue, field.getType(), string, (follow == null) ? style : string.linkStyle());
            string.endLink();
        }
    }

    private static String toPointerString(long pointer) {
        String hex = "0000000" + Long.toHexString(pointer);
        if (hex.length() > 15) {
            return "0x" + hex.substring(hex.length() - 16, hex.length());
        }
        return "0x" + hex.substring(hex.length() - 8, hex.length());
    }

    /**
     * Try to format a primitive value using it's constant name.
     * @return true if obj was formatted as a constant, false means format underlying value.
     */
    private static boolean tryConstantFormat(SnippetObject obj, Primitive type, StylingString string, Style style) {
        Collection<Constant> value = findConstant(obj, type);
        if (!value.isEmpty()) {
            boolean first = true;
            for (Constant constant : value) {
                if (!first) {
                    string.append(" | ", style);
                }
                first = false;
                string.append(constant.getName(), string.identifierStyle());
            }
            return true;
        }
        return false;
    }

    /**
     * @return empty list if not a constant, single value for constants, more values, for bitfileds.
     */
    public static Collection<Constant> findConstant(SnippetObject obj, Primitive type) {
        final ConstantSet constants = ConstantSet.lookup(type);
        if (constants == null || constants.getEntries().length == 0) {
            return Collections.emptyList();
        }

        // first, try and find exact match
        List<Constant> byValue = constants.getByValue(obj.getObject());
        if (byValue != null && byValue.size() != 0) {
            if (byValue.size() == 1) {
                // perfect, we have just 1 match
                return byValue;
            }
            // try and find the best match
            Labels labels = Labels.fromSnippets(obj.getSnippets());
            Constant result = disambiguate(byValue, labels);
            return result == null ? Collections.emptyList() : ImmutableList.of(result);
        }

        // we can not find any exact match,
        // but for a number, maybe we can find a combination of constants that match (bit flags)
        Object value = obj.getObject();
        if (!(value instanceof Number) || value instanceof Double || value instanceof Float) {
            return Collections.emptyList();
        }

        long valueNumber = ((Number) value).longValue();
        long leftToFind = valueNumber;
        Multimap<Number, Constant> resultMap = ArrayListMultimap.create();

        for (Constant constant : constants.getEntries()) {
            long constantValue = ((Number) constant.getValue()).longValue();
            if (Long.bitCount(constantValue) == 1 && (valueNumber & constantValue) != 0) {
                resultMap.put(constantValue, constant);
                leftToFind &= ~constantValue; // remove bit
            }
        }

        // we did not find enough flags to cover this constant
        if (leftToFind != 0) {
            return Collections.emptyList();
        }

        // we found exactly 1 of each constant to cover the whole value
        if (resultMap.keySet().size() == resultMap.size()) {
            return resultMap.values();
        }

        // we have more than 1 matching constant per flag to we need to disambiguate
        Labels labels = Labels.fromSnippets(obj.getSnippets());
        for (Number key : resultMap.keySet()) {
            Collection<Constant> flagConstants = resultMap.get(key);
            if (flagConstants.size() == 1) {
                // perfect, we only have 1 value for this
                continue;
            }

            Constant con = disambiguate(flagConstants, labels);
            if (con != null) {
                // we have several values, but we found 1 to use
                resultMap.replaceValues(key, ImmutableList.of(con));
            } else {
                // we have several values and we don't know what one to use
                return Collections.emptyList();
            }
        }
        // assert all constants are disambiguated now
        assert resultMap.keySet().size() == resultMap.size();
        return resultMap.values();
    }

    private static Constant disambiguate(Collection<Constant> constants, Labels labels) {
        Collection<Constant> preferred;
        if (labels != null) {
            // There are label snippets, use them to disambiguate.
            preferred = labels.preferred(constants);
            if (preferred.size() == 1) {
                return Iterators.get(preferred.iterator(), 0);
            } else if (preferred.size() == 0) {
                // No matches, continue with the unfiltered constants.
                preferred = constants;
            }
        } else {
            preferred = constants;
        }
        // labels wasn't enough, try the heuristic.
        // Using an ambiguity threshold of 8. This side steps the most egregious misinterpretations.
        if (preferred.size() < 8) {
            return pickShortestName(preferred);
        }
        // Nothing worked we will show a numeric value.
        return null;
    }

    private static Constant pickShortestName(Collection<Constant> constants) {
        int len = Integer.MAX_VALUE;
        Constant shortest = null;
        for (Constant constant : constants) {
            int l = constant.getName().length();
            if (l < len) {
                len = l;
                shortest = constant;
            }
        }
        return shortest;
    }

    private static Number toJavaIntType(Method type, Number value) {
        switch (type.getValue()) {
        case Method.Int8Value:
            return value.byteValue();
        case Method.Uint8Value:
            return (short) (value.intValue() & 0xff);
        case Method.Int16Value:
            return value.shortValue();
        case Method.Uint16Value:
            return value.intValue() & 0xffff;
        case Method.Int32Value:
            return value.intValue();
        case Method.Uint32Value:
            return value.longValue() & 0xffffffffL;
        case Method.Int64Value:
            return value.longValue();
        case Method.Uint64Value:
            ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
            buffer.putLong(value.longValue());
            return new BigInteger(1, buffer.array());
        default:
            throw new IllegalArgumentException("not int type: " + type);
        }
    }

    /**
     * Tries to convert a dynamic to a memory pointer if the schema representation is compatible.
     * There are several aliases for Memory.Pointer which are unique types, but we want to format
     * them as pointers.
     *
     * @param dynamic object to attempt to convert to a memory pointer.
     * @return a memory pointer if the conversion is possible, otherwise null.
     */
    private static MemoryPointer tryMemoryPointer(Dynamic dynamic) {
        Entity entity = dynamic.klass().entity();
        Field[] fields = entity.getFields();
        MemoryPointer mp = new MemoryPointer();
        Field[] mpFields = mp.klass().entity().getFields();
        if (mpFields.length != fields.length) {
            return null;
        }
        for (int i = 0; i < fields.length; ++i) {
            if (!fields[i].equals(mpFields[i])) {
                return null;
            }
        }
        long address = ((Long) dynamic.getFieldValue(0)).longValue();
        int poolId = ((Number) dynamic.getFieldValue(1)).intValue();
        mp.setAddress(address);
        mp.setPool(poolId);
        return mp;
    }

    private static MemorySliceMetadata getSliceMetadata(Dynamic dynamic) {
        BinaryObject[] metaData = dynamic.type().getMetadata();
        for (BinaryObject md : metaData) {
            if (md instanceof MemorySliceMetadata) {
                return (MemorySliceMetadata) md;
            }
        }
        return null;
    }

    /**
     * Tagging interface implemented by the various styles.
     */
    public static interface Style {
        // Empty tagging interface.
    }

    /**
     * String consisting of styled segments.
     */
    public static interface StylingString {
        /**
         * @return the default "no style" style.
         */
        public Style defaultStyle();

        /**
         * @return the style to use for structure elements like '{' or ','.
         */
        public Style structureStyle();

        /**
         * @return the style to use for identifiers.
         */
        public Style identifierStyle();

        /**
         * @return the style to use for labels.
         */
        public Style labelStyle();

        /**
         * @return the style to use for links.
         */
        public Style linkStyle();

        /**
         * @return the style to use for errors (e.g. in the report view).
         */
        public Style errorStyle();

        /**
         * @return the style to use for warnings (e.g. in the report view).
         */
        public Style warningStyle();

        /**
         * Appends the given segment with the given style to this string.
         */
        public StylingString append(String text, Style style);

        /**
         * Appends the given segment with the given style to this string, optionally eliding it.
         */
        public StylingString appendWithEllipsis(String text, Style style);

        /**
         * Indicates the start of a link with the given target.
         */
        public void startLink(Object target);

        /**
         * Indicates the end of the most recently started link.
         */
        public void endLink();
    }

    /**
     * {@link StylingString} that doesn't actually style.
     */
    private static class NoStyleStylingString implements StylingString {
        private final StringBuilder string = new StringBuilder();

        public NoStyleStylingString() {
        }

        @Override
        public StylingString append(String text, Style style) {
            string.append(text);
            return this;
        }

        @Override
        public StylingString appendWithEllipsis(String text, Style style) {
            string.append(text);
            return this;
        }

        @Override
        public void startLink(Object target) {
            // Ignore.
        }

        @Override
        public void endLink() {
            // Ignore.
        }

        @Override
        public String toString() {
            return string.toString();
        }

        @Override
        public Style defaultStyle() {
            return null;
        }

        @Override
        public Style structureStyle() {
            return null;
        }

        @Override
        public Style identifierStyle() {
            return null;
        }

        @Override
        public Style labelStyle() {
            return null;
        }

        @Override
        public Style linkStyle() {
            return null;
        }

        @Override
        public Style errorStyle() {
            return null;
        }

        @Override
        public Style warningStyle() {
            return null;
        }
    }

    /**
     * {@link StylingString} that uses the {@link Theme} stylers to style the string.
     */
    private abstract static class ThemedStylingString implements StylingString {
        private final StylerStyle deflt;
        private final StylerStyle structure;
        private final StylerStyle identifier;
        private final StylerStyle label;
        private final StylerStyle link;
        private final StylerStyle error;
        private final StylerStyle warning;

        public ThemedStylingString(Theme theme) {
            deflt = new StylerStyle(null);
            structure = new StylerStyle(theme.structureStyler());
            identifier = new StylerStyle(theme.identifierStyler());
            label = new StylerStyle(theme.labelStyler());
            link = new StylerStyle(theme.linkStyler());
            error = new StylerStyle(theme.errorStyler());
            warning = new StylerStyle(theme.warningStyler());
        }

        @Override
        public Style defaultStyle() {
            return deflt;
        }

        @Override
        public Style structureStyle() {
            return structure;
        }

        @Override
        public Style identifierStyle() {
            return identifier;
        }

        @Override
        public Style labelStyle() {
            return label;
        }

        @Override
        public Style linkStyle() {
            return link;
        }

        @Override
        public Style errorStyle() {
            return error;
        }

        @Override
        public Style warningStyle() {
            return warning;
        }

        protected static class StylerStyle implements Style {
            public final Styler styler;

            public StylerStyle(Styler styler) {
                this.styler = styler;
            }
        }
    }

    /**
     * {@link StylingString} implementations.
     */
    public static interface LinkableStyledString extends StylingString {
        public static final int MAX_STR_LEN = 45;

        public Object getLinkTarget(int offset);

        /* Do not modify the returned string. Use this only to render this string. */
        public StyledString getString();

        public static LinkableStyledString create(Theme theme) {
            return new LinkableStyledStringImpl(theme);
        }

        /** @return a {@link LinkableStyledString} that ignores the linking part. */
        public static LinkableStyledString ignoring(Theme theme) {
            return new IgnoringLinkableStyledString(theme, false);
        }

        /** @return a {@link LinkableStyledString} that ignores the linking and ellipsis part. */
        public static LinkableStyledString ignoringAndExpanded(Theme theme) {
            return new IgnoringLinkableStyledString(theme, true);
        }

        public static class IgnoringLinkableStyledString extends ThemedStylingString
                implements LinkableStyledString {
            protected final StyledString string = new StyledString();
            private final boolean ignoreEllipsis;

            IgnoringLinkableStyledString(Theme theme, boolean ignoreEllipsis) {
                super(theme);
                this.ignoreEllipsis = ignoreEllipsis;
            }

            @Override
            public StylingString append(String text, Style style) {
                string.append(text, ((StylerStyle) style).styler);
                return this;
            }

            @Override
            public StylingString appendWithEllipsis(String text, Style style) {
                if (ignoreEllipsis) {
                    return append(text, style);
                }
                text = text.replaceAll("[\n\r]+", "[\\\\n]");
                if (text.length() < MAX_STR_LEN + 3) {
                    return append(text, style);
                } else {
                    return append(text.substring(0, MAX_STR_LEN) + "...", style);
                }
            }

            @Override
            public void startLink(Object target) {
                // Ignore.
            }

            @Override
            public void endLink() {
                // Ignore.
            }

            @Override
            public Object getLinkTarget(int offset) {
                return null;
            }

            @Override
            public StyledString getString() {
                return string;
            }
        }

        public static class LinkableStyledStringImpl extends IgnoringLinkableStyledString {
            private final List<Entry> entries = Lists.newArrayList();
            private int currentStart;
            private Object currentTarget;

            LinkableStyledStringImpl(Theme theme) {
                super(theme, false);
            }

            @Override
            public void startLink(Object target) {
                endLink();
                currentStart = string.length();
                currentTarget = target;
            }

            @Override
            public void endLink() {
                if (currentTarget != null) {
                    int end = string.length();
                    if (end > currentStart) {
                        entries.add(new Entry(new IntRange(currentStart, end - 1), currentTarget));
                    }
                    currentTarget = null;
                }
            }

            @Override
            public Object getLinkTarget(int offset) {
                int index = Collections.binarySearch(entries, null,
                        (x, ignored) -> (offset < x.range.from) ? 1 : (offset > x.range.to) ? -1 : 0);
                return (index < 0) ? null : entries.get(index).value;
            }

            private static class Entry {
                public final IntRange range;
                public final Object value;

                public Entry(IntRange range, Object value) {
                    this.range = range;
                    this.value = value;
                }
            }
        }
    }
}