/*
* RnwChunkOptions.java
*
* Copyright (C) 2009-12 by RStudio, Inc.
*
* Unless you have received this program directly from RStudio pursuant
* to the terms of a commercial license agreement with RStudio, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
package org.rstudio.studio.client.workbench.views.source.model;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONString;
import org.rstudio.core.client.StringUtil;
import org.rstudio.studio.client.common.r.RToken;
import org.rstudio.studio.client.common.r.RTokenizer;
import org.rstudio.studio.client.common.rnw.RnwWeave;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Stack;
public class RnwChunkOptions extends JavaScriptObject
{
protected RnwChunkOptions()
{
}
public final ArrayList<String> getOptions()
{
ArrayList<String> options = new ArrayList<String>(
new JSONObject(this).keySet());
Collections.sort(options);
return options;
}
// types: "numeric", "character", "logical", "list", or null for unknown
public final String getOptionType(String name)
{
if (!hasOption(name))
return null;
JSONArray arr = new JSONArray(getOptionTypeNative(name));;
if (arr.size() == 1)
return arr.get(0).isString().stringValue();
else
return "list";
}
public final ArrayList<String> getOptionValues(String name)
{
JSONArray arr = new JSONArray(getOptionTypeNative(name));
ArrayList<String> values = new ArrayList<String>();
for (int i=0; i<arr.size(); i++)
{
JSONArray array = arr.get(i).isArray();
if (array == null)
break;
if (array.size() == 0)
break;
JSONString string = array.get(0).isString();
if (string == null)
break;
values.add(string.stringValue());
}
return values;
}
private native final JavaScriptObject getOptionTypeNative(String name) /*-{
return this[name];
}-*/;
private native final boolean hasOption(String name) /*-{
return typeof(this[name]) != 'undefined';
}-*/;
public static class RnwOptionCompletionResult
{
public String token;
public JsArrayString completions;
}
public final RnwOptionCompletionResult getCompletions(String line,
int optionsStartOffset,
int cursorPos,
RnwWeave rnwWeave)
{
assert cursorPos >= optionsStartOffset :
"cursorPos was less than optionsStartOffset";
String linePart = line.substring(optionsStartOffset, cursorPos);
// This can be pretty simple because Noweb doesn't allow = or , to appear
// in names or values (i.e. no quotes or escaping to make parsing more
// complicated).
String token = null;
JsArrayString completions = JsArrayString.createArray().cast();
ArrayList<String> names = new ArrayList<String>();
ArrayList<String> values = new ArrayList<String>();
parseRnwChunkHeader(linePart, names, values);
assert names.size() == values.size();
String name = names.size() == 0
? null : names.get(names.size()-1);
String value = values.size() == 0
? null : values.get(values.size()-1);
if (value != null)
{
token = value;
// If value is not null, we follow an equal sign; try to complete
// based on value.
completeValue(rnwWeave, name, value, completions);
}
else if (name != null)
{
token = name;
for (String optionName : this.getOptions())
if (optionName.startsWith(name))
completions.push(optionName + "=");
}
RnwOptionCompletionResult result = new RnwOptionCompletionResult();
result.token = token;
result.completions = completions;
return result;
}
private void completeValue(RnwWeave rnwWeave,
String name,
String value,
JsArrayString completions)
{
String optionType = StringUtil.notNull(this.getOptionType(name));
if (optionType.equals("logical"))
{
CompletionOptions options = new CompletionOptions();
options.addOption("TRUE", 0);
options.addOption("FALSE", 0);
if (!rnwWeave.usesCodeForOptions())
{
// Legacy Sweave is case insensitive
options.addOption("true", 1);
options.addOption("false", 1);
options.addOption("True", 2);
options.addOption("False", 2);
}
for (String logical : options.getCompletions(value))
completions.push(logical);
}
else if (optionType.equals("list"))
{
CompletionOptions options = new CompletionOptions();
ArrayList<String> optionValues = this.getOptionValues(name);
if (!rnwWeave.usesCodeForOptions())
{
// Legacy Sweave
for (String optionVal : optionValues)
options.addOption(optionVal, 0);
}
else
{
for (String optionVal : optionValues)
options.addOption("'" + optionVal + "'", 0);
for (String optionVal : optionValues)
options.addOption('"' + optionVal + '"', 1);
}
for (String option : options.getCompletions(value))
completions.push(option);
}
}
private static void parseRnwChunkHeader(String line,
ArrayList<String> names,
ArrayList<String> values)
{
String currentName = null;
String currentValue = null;
int currentPartBegin = 0;
Stack<Integer> braceStack = new Stack<Integer>();
RTokenizer tokenizer = new RTokenizer(line);
for (RToken token; null != (token = tokenizer.nextToken()); )
{
switch (token.getTokenType())
{
case RToken.OPER:
if (token.getContent().equals("=") &&
currentName == null &&
braceStack.empty())
{
String part = line.substring(currentPartBegin,
token.getOffset());
currentName = part;
currentPartBegin = token.getOffset() + token.getLength();
}
break;
case RToken.COMMA:
if (braceStack.empty())
{
String part = line.substring(currentPartBegin,
token.getOffset());
if (currentName == null)
currentName = part;
else
currentValue = part;
names.add(currentName.trim());
values.add(currentValue != null ?
StringUtil.trimLeft(currentValue) :
null);
currentName = null;
currentValue = null;
currentPartBegin = token.getOffset() + token.getLength();
}
break;
case RToken.LBRACE: braceStack.push(RToken.RBRACE); break;
case RToken.LBRACKET: braceStack.push(RToken.RBRACKET); break;
case RToken.LDBRACKET: braceStack.push(RToken.RDBRACKET); break;
case RToken.LPAREN: braceStack.push(RToken.RPAREN); break;
case RToken.RBRACE:
case RToken.RBRACKET:
case RToken.RDBRACKET:
case RToken.RPAREN:
int distance = braceStack.search(token.getTokenType());
if (distance > 0)
{
for (int i = 0; i < distance; i++)
braceStack.pop();
}
break;
}
}
String part = line.substring(currentPartBegin,
line.length());
if (currentName == null)
currentName = part;
else
currentValue = part;
if (currentValue == null)
{
names.add(StringUtil.trimLeft(currentName));
values.add(null);
}
else
{
names.add(currentName.trim());
values.add(StringUtil.trimLeft(currentValue));
}
}
}