/*******************************************************************************
* Copyright (c) 2010 Fraunhofer IWU and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Fraunhofer IWU - initial API and implementation
*******************************************************************************/
package net.enilink.komma.edit.assist;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.parboiled.MatcherContext;
import org.parboiled.Rule;
import org.parboiled.buffers.InputBuffer;
import org.parboiled.matchers.AbstractMatcher;
import org.parboiled.matchers.ActionMatcher;
import org.parboiled.matchers.CharIgnoreCaseMatcher;
import org.parboiled.matchers.CharMatcher;
import org.parboiled.matchers.FirstOfMatcher;
import org.parboiled.matchers.Matcher;
import org.parboiled.matchers.OneOrMoreMatcher;
import org.parboiled.matchers.OptionalMatcher;
import org.parboiled.matchers.SequenceMatcher;
import org.parboiled.matchers.ZeroOrMoreMatcher;
import org.parboiled.matchervisitors.DefaultMatcherVisitor;
import org.parboiled.matchervisitors.FollowMatchersVisitor;
import org.parboiled.parserunners.BasicParseRunner;
import org.parboiled.support.Characters;
import org.parboiled.support.Chars;
import org.parboiled.support.ParsingResult;
public class ParboiledProposalProvider implements IContentProposalProvider {
static class CollectProposalsVisitor extends DefaultMatcherVisitor<Boolean> {
Set<Matcher> active = new HashSet<Matcher>();
boolean includeSemanticProposals;
int optional = 0;
String prefix;
ISemanticProposalProvider proposalProvider;
Map<ISemanticProposal, String> semanticProposals;
Characters separators = Characters.of(' ', '\n', '\r', '\t', '\f', Chars.EOI);
StringBuilder word;
LinkedHashSet<Proposal> words = new LinkedHashSet<Proposal>();
public CollectProposalsVisitor(ISemanticProposalProvider proposalProvider,
Map<ISemanticProposal, String> semanticProposals) {
this.proposalProvider = proposalProvider;
this.semanticProposals = semanticProposals;
}
protected boolean addSemanticProposal(Matcher matcher) {
if (proposalProvider == null || !includeSemanticProposals) {
return false;
}
ISemanticProposal proposal = proposalProvider.getProposal(matcher.getLabel());
if (proposal != null && !semanticProposals.containsKey(proposal)) {
semanticProposals.put(proposal, prefix);
return true;
}
return false;
}
protected void addWord(String word) {
if (prefix == null || word.startsWith(prefix)) {
if (word.equals(prefix)) {
return;
}
String content = prefix == null ? word : word.substring(prefix.length());
words.add(new Proposal(content, word));
}
}
@Override
public Boolean defaultValue(AbstractMatcher matcher) {
addSemanticProposal(matcher);
return word.length() > 0;
}
public LinkedHashSet<Proposal> getWords() {
return words;
}
boolean isOptional() {
return optional > 0;
}
public void process(String prefix, boolean includeSemanticProposals, Matcher matcher) {
active.clear();
this.prefix = prefix;
this.includeSemanticProposals = includeSemanticProposals;
word = new StringBuilder();
if (matcher.accept(this) && word.length() > 0) {
addWord(word.toString());
}
}
@Override
public Boolean visit(ActionMatcher matcher) {
return false;
}
@Override
public Boolean visit(CharIgnoreCaseMatcher matcher) {
addSemanticProposal(matcher);
word.append(matcher.charLow);
return false;
}
@Override
public Boolean visit(CharMatcher matcher) {
addSemanticProposal(matcher);
word.append(matcher.character);
return false;
}
@Override
public Boolean visit(FirstOfMatcher matcher) {
if (!active.add(matcher)) {
return false;
}
addSemanticProposal(matcher);
boolean complete = false;
for (Matcher child : matcher.getChildren()) {
int length = word.length();
boolean accept = child.accept(this);
complete |= accept;
if (accept && word.length() > length) {
addWord(word.toString());
}
word.replace(length, word.length(), "");
}
active.remove(matcher);
return complete;
}
@Override
public Boolean visit(OneOrMoreMatcher matcher) {
if (!active.add(matcher)) {
return false;
}
addSemanticProposal(matcher);
try {
return matcher.subMatcher.accept(this);
} finally {
active.remove(matcher);
}
}
@Override
public Boolean visit(OptionalMatcher matcher) {
addSemanticProposal(matcher);
if (word.length() > 0) {
return true;
}
if (!active.add(matcher)) {
return false;
}
try {
optional++;
return matcher.subMatcher.accept(this);
} finally {
optional--;
active.remove(matcher);
}
}
@Override
public Boolean visit(SequenceMatcher matcher) {
if (!active.add(matcher)) {
return false;
}
addSemanticProposal(matcher);
for (Matcher child : matcher.getChildren()) {
if (child.accept(this)) {
return true;
}
}
active.remove(matcher);
return false;
}
@Override
public Boolean visit(ZeroOrMoreMatcher matcher) {
addSemanticProposal(matcher);
if (word.length() > 0) {
return true;
}
if (!active.add(matcher)) {
return false;
}
try {
optional++;
return matcher.subMatcher.accept(this);
} finally {
optional--;
active.remove(matcher);
}
}
}
static class Proposal implements Comparable<Proposal> {
String content;
String label;
public Proposal(String content, String label) {
this.content = content;
this.label = label;
}
@Override
public int compareTo(Proposal o) {
return label.compareTo(o.label);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Proposal other = (Proposal) obj;
if (content == null) {
if (other.content != null)
return false;
} else if (!content.equals(other.content))
return false;
if (label == null) {
if (other.label != null)
return false;
} else if (!label.equals(other.label))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((content == null) ? 0 : content.hashCode());
result = prime * result + ((label == null) ? 0 : label.hashCode());
return result;
}
@Override
public String toString() {
return "{content : \"" + content + "\", label : \"" + label + "\"}";
}
}
static class ProposalParseRunner extends BasicParseRunner<Object> {
int proposalIndex;
ISemanticProposalProvider proposalProvider;
Map<ISemanticProposal, String> semanticProposals = new HashMap<ISemanticProposal, String>();
CollectProposalsVisitor visitor;
ProposalParseRunner(Rule rule, int proposalIndex, ISemanticProposalProvider proposalProvider) {
super(rule);
this.proposalIndex = proposalIndex;
this.proposalProvider = proposalProvider;
}
private String getLastWord(MatcherContext<?> context) {
// determine the last non-whitespace word
String prefix = getPrefix(context);
if (prefix.matches(".*\\s$")) {
return "";
}
String[] words = prefix.split("\\s");
return words[words.length - 1];
}
private String getPrefix(MatcherContext<?> context) {
return context.getInputBuffer().extract(context.getStartIndex(), proposalIndex);
}
public Map<ISemanticProposal, String> getSemanticProposals() {
return semanticProposals;
}
public Set<Proposal> getWords() {
return new TreeSet<Proposal>(visitor.getWords());
}
public <V> boolean match(MatcherContext<V> context) {
boolean matched = context.getMatcher().match(context);
if (!matched) {
// TODO improve performance in this case
if (context.getStartIndex() <= proposalIndex) {
String prefix = getPrefix(context);
if (!prefix.matches(".*\\S.*\\s$")) {
visitor.process(prefix, false, context.getMatcher());
}
}
}
if (proposalIndex == context.getCurrentIndex()) {
String lastWord = getLastWord(context);
List<Matcher> matchers = new FollowMatchersVisitor().getFollowMatchers(context);
for (Matcher matcher : matchers) {
visitor.process(lastWord, true, matcher);
}
if (proposalProvider != null) {
do {
ISemanticProposal proposal = proposalProvider.getProposal(context.getMatcher().getLabel());
if (proposal != null && !semanticProposals.containsKey(proposal)) {
semanticProposals.put(proposal, getPrefix(context));
break;
}
context = context.getParent();
} while (context != null);
}
}
return matched;
}
@Override
public ParsingResult<Object> run(InputBuffer inputBuffer) {
visitor = new CollectProposalsVisitor(proposalProvider, semanticProposals);
return super.run(inputBuffer);
}
}
Rule rule;
ISemanticProposalProvider semanticProposalProvider;
public ParboiledProposalProvider(Rule rule, ISemanticProposalProvider semanticProposalProvider) {
this.rule = rule;
this.semanticProposalProvider = semanticProposalProvider;
}
@Override
public IContentProposal[] getProposals(String contents, int position) {
List<IContentProposal> proposals = new ArrayList<IContentProposal>();
ProposalParseRunner runner = new ProposalParseRunner(rule, position, semanticProposalProvider);
ParsingResult<Object> result = runner.run(contents);
// if (result.hasErrors() || result.resultValue == null) {
// result = new RecoveringParseRunner<Object>(parser.Query())
// .run(contents);
// }
for (Map.Entry<ISemanticProposal, String> entry : runner.getSemanticProposals().entrySet()) {
IContentProposal[] computedProposals = entry.getKey().compute(result, position, entry.getValue());
if (computedProposals != null) {
for (IContentProposal proposal : computedProposals) {
proposals.add(proposal);
}
}
}
for (Proposal proposal : runner.getWords()) {
proposals.add(new ContentProposal(proposal.content, proposal.label, proposal.label));
}
return proposals.toArray(new IContentProposal[proposals.size()]);
}
}