// 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.client.code.autocomplete.css;
import static com.google.collide.client.code.autocomplete.css.CompletionType.NONE;
import com.google.collide.json.client.JsoArray;
import com.google.common.annotations.VisibleForTesting;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.regexp.shared.SplitResult;
/**
* A completion query for a CSS autocompletion. Processes the relevant context
* in the document.
*
*/
public class CssCompletionQuery {
private static final RegExp REGEXP_SPACES = RegExp.compile("\\s+");
private static final RegExp REGEXP_COLON = RegExp.compile(":");
private static final RegExp REGEXP_SEMICOLON = RegExp.compile(";");
/*
* Depending on the query, we will either have an incomplete property or an
* incomplete value. If there is an incomplete value, the property (name) is
* assumed completed (or incorrect).
*/
private String property = ""; // Current property
/*
* The following two fields are used for filtering existing
* properties/attributes from the list of proposals.
*/
private final JsoArray<String> completedProperties = JsoArray.create();
private final JsArrayString valuesAfter = JsArrayString.createArray().cast();
private final JsArrayString valuesBefore = JsArrayString.createArray().cast();
private String value = "";
private CompletionType completionType;
/**
* Constructs a completion query based on an incomplete string, which is
* everything from the caret back to the beginning of the open CSS declaration
* block.
*
*
* @param textBefore the string to be completed
*/
public CssCompletionQuery(String textBefore, String textAfter) {
completionType = NONE;
parseContext(textBefore, textAfter);
}
public String getValue() {
return value;
}
public JsArrayString getValuesAfter() {
return valuesAfter;
}
public JsArrayString getValuesBefore() {
return valuesBefore;
}
public JsoArray<String> getCompletedProperties() {
return completedProperties;
}
public CompletionType getCompletionType() {
return completionType;
}
public String getProperty() {
return property;
}
private void parseCurrentPropertyAndValues(String incompletePropertyAndValues) {
incompletePropertyAndValues =
incompletePropertyAndValues.substring(incompletePropertyAndValues.indexOf('{') + 1);
SplitResult subParts = REGEXP_COLON.split(incompletePropertyAndValues);
// subParts must have at least one element
property = subParts.get(0).trim();
if (subParts.length() > 1) {
SplitResult valueParts = REGEXP_SPACES.split(subParts.get(1));
if (subParts.get(1).endsWith(" ")) {
for (int i = 0; i < valueParts.length(); i++) {
String trimmed = valueParts.get(i).trim();
if (!trimmed.isEmpty()) {
valuesBefore.push(trimmed);
}
}
} else {
if (valueParts.length() == 1) {
value = subParts.get(1).trim();
} else {
value = valueParts.get(valueParts.length() - 1).trim();
for (int i = 0; i < valueParts.length() - 1; i++) {
String trimmed = valueParts.get(i).trim();
if (!trimmed.isEmpty()) {
valuesBefore.push(trimmed);
}
}
}
}
} else if (incompletePropertyAndValues.endsWith(":")) {
value = "";
}
}
// TODO: Do something useful with textAfter
private void parseContext(String textBefore, String textAfter) {
if (textBefore.isEmpty()) {
completionType = CompletionType.PROPERTY;
return;
} else if (textBefore.endsWith("{")) {
completionType = CompletionType.CLASS;
return;
}
textBefore = textBefore.replaceAll("^\\s+", "");
// Split first on ';'. The last one is the incomplete one.
SplitResult parts = REGEXP_SEMICOLON.split(textBefore);
if ((textBefore.endsWith(";")) || (!parts.get(parts.length() - 1).contains(":"))) {
completionType = CompletionType.PROPERTY;
} else {
completionType = CompletionType.VALUE;
}
int highestCompleteIndex = parts.length() - 2;
if (textBefore.endsWith(";")) {
highestCompleteIndex = parts.length() - 1;
} else {
parseCurrentPropertyAndValues(parts.get(parts.length() - 1));
}
if (parts.length() > 1) {
// Parse the completed properties, which we use for filtering.
for (int i = 0; i <= highestCompleteIndex; i++) {
String completePropertyAndValues = parts.get(i);
SplitResult subParts = REGEXP_COLON.split(completePropertyAndValues);
completedProperties.add(subParts.get(0).trim().toLowerCase());
}
}
// Interpret textAfter
// Everything up to the first ; will be interpreted as being part of the
// current property, so it'll be complete values
parts = REGEXP_SEMICOLON.split(textAfter);
if (parts.length() > 0) {
// We assume that the property+values we are currently working on is not
// completed but can be assumed to end with a newline.
int newlineIndex = parts.get(0).indexOf('\n');
if (newlineIndex != -1) {
String currentValues = parts.get(0).substring(0, newlineIndex);
addToValuesAfter(currentValues);
addToCompletedProperties(parts.get(0).substring(newlineIndex + 1));
} else {
addToValuesAfter(parts.get(0));
}
for (int i = 1; i < parts.length(); i++) {
addToCompletedProperties(parts.get(i));
}
}
}
private void addToCompletedProperties(String completedProps) {
SplitResult completed = REGEXP_SEMICOLON.split(completedProps);
for (int i = 0; i < completed.length(); i++) {
int colonIndex = completed.get(i).indexOf(":");
String trimmed;
if (colonIndex != -1) {
trimmed = completed.get(i).substring(0, colonIndex).trim();
} else {
trimmed = completed.get(i).trim();
}
if (!trimmed.isEmpty()) {
completedProperties.add(trimmed.toLowerCase());
}
}
}
private void addToValuesAfter(String completedVals) {
SplitResult completed = REGEXP_SPACES.split(completedVals);
for (int i = 0; i < completed.length(); i++) {
String trimmed = completed.get(i).trim();
if (!trimmed.isEmpty()) {
valuesAfter.push(trimmed);
}
}
}
@VisibleForTesting
public String getTriggeringString() {
switch (completionType) {
case CLASS:
return "";
case PROPERTY:
return getProperty();
case VALUE:
return getValue();
default:
return null;
}
}
public void setCompletionType(CompletionType type) {
this.completionType = type;
}
}