/*
* Copyright 2011-present Greg Shrago
*
* 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 org.intellij.grammar.psi.impl;
import com.intellij.extapi.psi.PsiFileBase;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.util.Conditions;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.FileViewProvider;
import com.intellij.psi.util.CachedValue;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.JBIterable;
import org.intellij.grammar.BnfFileType;
import org.intellij.grammar.BnfLanguage;
import org.intellij.grammar.KnownAttribute;
import org.intellij.grammar.generator.ParserGeneratorUtil;
import org.intellij.grammar.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* User: gregory
* Date: 13.07.11
* Time: 23:55
*/
public class BnfFileImpl extends PsiFileBase implements BnfFile {
private final CachedValue<Map<String, BnfRule>> myRules;
private final CachedValue<List<BnfAttrs>> myGlobalAttributes;
private final CachedValue<Map<String, List<AttributeInfo>>> myAttributeValues;
public BnfFileImpl(FileViewProvider fileViewProvider) {
super(fileViewProvider, BnfLanguage.INSTANCE);
myRules = CachedValuesManager.getManager(getProject()).createCachedValue(
() -> CachedValueProvider.Result.create(calcRules(), BnfFileImpl.this), false);
myGlobalAttributes = CachedValuesManager.getManager(getProject()).createCachedValue(
() -> CachedValueProvider.Result.create(calcAttributes(), BnfFileImpl.this), false);
myAttributeValues = CachedValuesManager.getManager(getProject()).createCachedValue(
() -> CachedValueProvider.Result.create(calcAttributeValues(), BnfFileImpl.this), false);
}
@NotNull
@Override
public List<BnfRule> getRules() {
return new ArrayList<>(myRules.getValue().values());
}
@Nullable
@Override
public BnfRule getRule(@Nullable String ruleName) {
return ruleName == null ? null : myRules.getValue().get(ruleName);
}
@NotNull
@Override
public List<BnfAttrs> getAttributes() {
return myGlobalAttributes.getValue();
}
@Override
@Nullable
public BnfAttr findAttribute(@Nullable BnfRule rule, @NotNull KnownAttribute<?> knownAttribute, @Nullable String match) {
AttributeInfo result = getMatchingAttributes(rule, knownAttribute, match).first();
if (result == null) return null;
return PsiTreeUtil.getParentOfType(findElementAt(result.attrOffset), BnfAttr.class);
}
public <T> T findAttributeValue(@Nullable BnfRule rule, @NotNull KnownAttribute<T> knownAttribute, @Nullable String match) {
T combined = null;
boolean copied = false;
for (AttributeInfo info : getMatchingAttributes(rule, knownAttribute, match)) {
T cur = knownAttribute.ensureValue(info.value);
if (combined != null && info.pattern == null) continue;
if (knownAttribute == KnownAttribute.PIN && match != null &&
info.pattern == null && !info.global && SUB_EXPRESSION.matcher(match).matches()) {
// do not pin nested sequences for local pin=N
return null;
}
if (!(cur instanceof KnownAttribute.ListValue)) {
return cur;
}
if (combined == null) combined = cur;
else if (copied) ((KnownAttribute.ListValue)combined).addAll((KnownAttribute.ListValue)cur);
else {
copied = true;
KnownAttribute.ListValue copy = new KnownAttribute.ListValue();
copy.addAll((KnownAttribute.ListValue)combined);
copy.addAll((KnownAttribute.ListValue)cur);
combined = (T)copy;
}
}
return combined != null ? combined : knownAttribute.getDefaultValue();
}
private static final Pattern SUB_EXPRESSION = Pattern.compile(".*(_\\d+)+");
@NotNull
private <T> JBIterable<AttributeInfo> getMatchingAttributes(@Nullable BnfRule rule,
@NotNull KnownAttribute<T> knownAttribute,
@Nullable String match) {
List<AttributeInfo> list = myAttributeValues.getValue().get(knownAttribute.getName());
if (list == null) return JBIterable.empty();
BnfAttrs globalAttrs = rule == null ? ContainerUtil.getFirstItem(getAttributes()) : null;
int offset = rule == null ?
globalAttrs == null ? 0 :
globalAttrs.getTextRange().getEndOffset() :
rule.getTextRange().getEndOffset();
if (offset == 0) return JBIterable.empty();
AttributeInfo key = new AttributeInfo(0, offset, true, null, null);
int index = Collections.binarySearch(list, key);
int ruleStartOffset = rule == null ? offset : rule.getTextRange().getStartOffset();
String toMatch = match == null ? rule == null ? null : rule.getName() : match;
return JBIterable.generate(
Math.min(list.size() - 1, index < 0 ? -index - 1 : index),
i -> i > 0 ? i - 1 : null)
.map(i -> {
AttributeInfo info = list.get(i);
if (offset < info.offset || !info.global && ruleStartOffset > info.offset) return null;
if (info.pattern == null || toMatch != null && info.pattern.matcher(toMatch).matches()) {
return info;
}
return null;
}).filter(Conditions.notNull());
}
@NotNull
@Override
public FileType getFileType() {
return BnfFileType.INSTANCE;
}
@Override
public String toString() {
return "BnfFile:" + getName();
}
private Map<String, BnfRule> calcRules() {
Map<String, BnfRule> result = ContainerUtil.newLinkedHashMap();
for (BnfRule o : GrammarUtil.bnfTraverser(this).filter(BnfRule.class)) {
if (!result.containsKey(o.getName())) {
result.put(o.getName(), o);
}
}
return result;
}
private List<BnfAttrs> calcAttributes() {
return GrammarUtil.bnfTraverser(this).filter(BnfAttrs.class).toList();
}
private Map<String, List<AttributeInfo>> calcAttributeValues() {
Map<String, List<AttributeInfo>> result = ContainerUtil.newTroveMap();
for (BnfAttrs attrs : GrammarUtil.bnfTraverser(this).filter(BnfAttrs.class)) {
boolean isRule = attrs.getParent() instanceof BnfRule;
TextRange baseRange = attrs.getTextRange();
List<BnfAttr> attrList = attrs.getAttrList();
for (int pass = 0; pass < 2; pass ++) {
for (int i = attrList.size()-1; i >= 0; i --) {
BnfAttr attr = attrList.get(i);
BnfAttrPattern attrPattern = attr.getAttrPattern();
// pass 0: w/o patterns, pass 1: w/ pattern
if ((pass == 0) == (attrPattern != null)) continue;
Pattern pattern = null;
if (attrPattern != null) {
BnfLiteralExpression expression = attrPattern.getLiteralExpression();
pattern = expression == null ? null : ParserGeneratorUtil.compilePattern(GrammarUtil.unquote(expression.getText()));
}
List<AttributeInfo> list = result.get(attr.getName());
if (list == null) result.put(attr.getName(), list = new ArrayList<>());
Object value = ParserGeneratorUtil.getAttributeValue(attr.getExpression());
int offset = attr.getTextRange().getStartOffset();
int infoOffset = pattern == null? baseRange.getStartOffset() + 1: baseRange.getStartOffset() + (baseRange.getEndOffset() - offset);
list.add(new AttributeInfo(offset, infoOffset, !isRule, pattern, value));
}
}
}
return result;
}
private static class AttributeInfo implements Comparable<AttributeInfo> {
final int attrOffset;
final int offset;
final boolean global;
final Pattern pattern;
final Object value;
private AttributeInfo(int attrOffset, int offset, boolean global, Pattern pattern, Object value) {
this.attrOffset = attrOffset;
this.offset = offset;
this.global = global;
this.pattern = pattern;
this.value = value;
}
@Override
public int compareTo(@NotNull AttributeInfo o) {
return offset - o.offset;
}
@Override
public String toString() {
return (global ? "" : "rule:") + offset + (pattern == null? "" : " (" + pattern + ")") + " = " + value;
}
}
}