org.eclipse.lsp4j.util.SemanticHighlightingTokens.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.lsp4j.util.SemanticHighlightingTokens.java

Source

/******************************************************************************
 * Copyright (c) 2018 TypeFox and others.
 * 
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 * 
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 ******************************************************************************/
package org.eclipse.lsp4j.util;

import static java.util.Collections.emptyList;

import java.nio.ByteBuffer;
import java.util.Base64;
import java.util.List;

import org.eclipse.xtext.xbase.lib.IterableExtensions;

import com.google.common.base.Preconditions;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.Iterables;

/**
 * Utility class for encoding and decoding semantic highlighting tokens into a
 * compact, {@code base64} representation.
 */
public final class SemanticHighlightingTokens {

    private static int LENGTH_SHIFT = 0x0000010;
    private static int SCOPES_MASK = 0x0000FFFF;

    /**
     * Encodes the iterable of tokens into a compact, {@code base64} string. Returns
     * with an empty string if the {@code tokens} argument is {@code null} or empty.
     */
    public static String encode(Iterable<? extends Token> tokens) {
        if (IterableExtensions.isNullOrEmpty(tokens)) {
            return "";
        }
        // 2 stands for: number of elements for the output; the "character" and the
        // "length and scope".
        // 4 stands for: 4 byte for a primitive integer.
        ByteBuffer buffer = ByteBuffer.allocate(Iterables.size(tokens) * 2 * 4);
        for (Token token : tokens) {
            int character = token.character;
            int length = token.length;
            int scope = token.scope;

            int lengthAndScope = length;
            lengthAndScope = lengthAndScope << LENGTH_SHIFT;
            lengthAndScope |= scope;

            buffer.putInt(character);
            buffer.putInt(lengthAndScope);
        }
        return Base64.getEncoder().encodeToString(buffer.array());
    }

    /**
     * Decodes the tokens string and returns with a list of semantic highlighting
     * tokens. Returns with an empty list if the argument is {@code null} or empty.
     */
    public static List<Token> decode(String tokens) {
        if (tokens == null || tokens.length() == 0) {
            return emptyList();
        }
        ByteBuffer buffer = ByteBuffer.wrap(Base64.getDecoder().decode(tokens));
        Builder<Token> builder = ImmutableList.builder();
        while (buffer.hasRemaining()) {
            int character = buffer.getInt();

            int lengthAndScope = buffer.getInt();
            int length = lengthAndScope >>> LENGTH_SHIFT;
            int scope = lengthAndScope & SCOPES_MASK;

            builder.add(new Token(character, length, scope));
        }
        return builder.build();
    }

    /**
     * The bare minimum representation of a semantic highlighting token.
     * 
     * <p>
     * Semantic highlighting tokens are <b>not</b> part of the protocol, as they're
     * sent through the wire as an encoded string. The purpose of this data type is
     * to help working with them.
     */
    public static class Token implements Comparable<Token> {

        private static int[] EMPTY_ARRAY = new int[0];
        private static int MAX_UINT_16 = 65_535; // 2^16 - 1

        /**
         * Converts an iterable of tokens into an array of primitive integers. Returns
         * with an empty array if the argument is {@code null} or empty.
         * 
         * <p>
         * <ul>
         * <li>{@code array[3*i]} is the {@link Token#character character} of the
         * token.</li>
         * <li>{@code array[3*i+1]} is the {@link Token#length length} of the
         * token.</li>
         * <li>{@code array[3*i+2]} is the token {@link Token#scope scope}.</li>
         * </ul>
         */
        public static int[] toIntArray(Iterable<? extends Token> tokens) {
            if (IterableExtensions.isNullOrEmpty(tokens)) {
                return EMPTY_ARRAY;
            }
            int[] array = new int[Iterables.size(tokens) * 3];
            int i = 0;
            for (Token token : tokens) {
                array[i++] = token.character;
                array[i++] = token.length;
                array[i++] = token.scope;
            }
            return array;
        }

        /**
         * Counter-part of the {@link Token#toIntArray}. Converts an array of primitive
         * integers into a list of tokens. If the input is {@code null}, returns with an
         * empty list. Throws an exception if the length of the arguments is not a
         * modulo of {@code 3}.
         * 
         * The elements of the input will be used to create new {@link Token} instances,
         * hence they must comply the requirements described
         * {@link #SemanticHighlightingTokens.Token(int, int, int) here}.
         */
        public static List<Token> fromIntArray(int... input) {
            if (input == null) {
                return emptyList();
            }
            int inputLength = input.length;
            Preconditions.checkArgument(inputLength % 3 == 0,
                    "Invalid input. 'input.length % 3 != 0' Input length was: " + inputLength + ".");
            Builder<Token> builder = ImmutableList.builder();
            for (int i = 0; i < inputLength; i = i + 3) {
                builder.add(new Token(input[i], // character
                        input[i + 1], // length
                        input[i + 2] // scope
                ));
            }
            return builder.build();
        }

        /**
         * The zero-based character offset of the token in the line.
         */
        public final int character;

        /**
         * The length of the token.
         */
        public final int length;

        /**
         * The internal index of the
         * <a href="https://manual.macromates.com/en/language_grammars">TextMate
         * scope</a>.
         */
        public final int scope;

        /**
         * Creates a new highlighting token.
         *
         * @param character
         *            the character offset of the token. Must not be a negative integer.
         * @param length
         *            the length of the token. Must be an integer between {@code 0} and
         *            {@code 2}<sup>16</sup>{@code -1} (inclusive).
         * @param scope
         *            the scope. Must be an integer between {@code 0} and
         *            {@code 2}<sup>16</sup>{@code -1} (inclusive).
         */
        public Token(int character, int length, int scope) {
            Preconditions.checkArgument(character >= 0, "Expected 'character' >= 0. Was: " + character + ".");
            this.character = character;
            this.length = assertUint16(length, "length");
            this.scope = assertUint16(scope, "scope");
        }

        /**
         * Asserts the argument. It must be in a range of {@code 0} and
         * {@code 2}<sup>16</sup>{@code -1} (inclusive). Returns with the argument if
         * valid. Otherwise throws an {@link IllegalArgumentException}.
         */
        private static int assertUint16(int i, String prefix) {
            Preconditions.checkArgument(i >= 0,
                    "Expected: '" + prefix + "' >= 0. '" + prefix + "' was: " + i + ".");
            Preconditions.checkArgument(i <= MAX_UINT_16,
                    "Expected: '" + prefix + "' <= " + MAX_UINT_16 + ". '" + prefix + "' was: " + i + ".");
            return i;
        }

        @Override
        public int compareTo(Token o) {
            return ComparisonChain.start().compare(character, o.character).compare(length, o.length)
                    .compare(scope, o.scope).result();
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + character;
            result = prime * result + length;
            result = prime * result + scope;
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            Token other = (Token) obj;
            if (character != other.character)
                return false;
            if (length != other.length)
                return false;
            if (scope != other.scope)
                return false;
            return true;
        }

    }

    private SemanticHighlightingTokens() {
    }

}