package org.jetbrains.plugins.cucumber.completion;
import com.intellij.codeInsight.TailType;
import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.AutoCompletionPolicy;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.codeInsight.lookup.TailTypeDecorator;
import com.intellij.codeInsight.template.TemplateBuilder;
import com.intellij.codeInsight.template.TemplateBuilderFactory;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.patterns.PsiElementPattern;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileSystemItem;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.plugins.cucumber.psi.*;
import org.jetbrains.plugins.cucumber.psi.impl.GherkinExamplesBlockImpl;
import org.jetbrains.plugins.cucumber.psi.impl.GherkinScenarioOutlineImpl;
import org.jetbrains.plugins.cucumber.steps.AbstractStepDefinition;
import org.jetbrains.plugins.cucumber.steps.CucumberStepsIndex;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.intellij.patterns.PlatformPatterns.psiElement;
/**
* @author yole
*/
public class CucumberCompletionContributor extends CompletionContributor {
private static final Map<String, String> GROUP_TYPE_MAP = new HashMap<>();
private static final Map<String, String> INTERPOLATION_PARAMETERS_MAP = new HashMap<>();
static {
GROUP_TYPE_MAP.put("(.*)", "<string>");
GROUP_TYPE_MAP.put("(.+)", "<string>");
GROUP_TYPE_MAP.put("([^\"]*)", "<string>");
GROUP_TYPE_MAP.put("([^\"]+)", "<string>");
GROUP_TYPE_MAP.put("(\\d*)", "<number>");
GROUP_TYPE_MAP.put("(\\d)", "<number>");
GROUP_TYPE_MAP.put("(\\d+)", "<number>");
GROUP_TYPE_MAP.put("(\\.[\\d]+)", "<number>");
INTERPOLATION_PARAMETERS_MAP.put("#\\{[^\\}]*\\}", "<param>");
}
private static final int SCENARIO_KEYWORD_PRIORITY = 70;
private static final int SCENARIO_OUTLINE_KEYWORD_PRIORITY = 60;
public static final Pattern POSSIBLE_GROUP_PATTERN = Pattern.compile("\\(([^\\)]*)\\)");
public static final Pattern QUESTION_MARK_PATTERN = Pattern.compile("([^\\\\])\\?:?");
public static final Pattern PARAMETERS_PATTERN = Pattern.compile("<string>|<number>|<param>");
public static final String INTELLIJ_IDEA_RULEZZZ = "IntellijIdeaRulezzz";
public CucumberCompletionContributor() {
final PsiElementPattern.Capture<PsiElement> inScenario = psiElement().inside(psiElement().withElementType(GherkinElementTypes.SCENARIOS));
final PsiElementPattern.Capture<PsiElement> inStep = psiElement().inside(psiElement().withElementType(GherkinElementTypes.STEP));
extend(CompletionType.BASIC, psiElement().inFile(psiElement(GherkinFile.class)), new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
ProcessingContext context,
@NotNull CompletionResultSet result) {
final PsiFile psiFile = parameters.getOriginalFile();
if (psiFile instanceof GherkinFile) {
final PsiElement position = parameters.getPosition();
// if element isn't under feature declaration - suggest feature in autocompletion
// but don't suggest scenario keywords inside steps
final PsiElement coveringElement = PsiTreeUtil.getParentOfType(position, GherkinStep.class, GherkinFeature.class, PsiFileSystemItem.class);
if (coveringElement instanceof PsiFileSystemItem) {
addFeatureKeywords(result, psiFile);
} else if (coveringElement instanceof GherkinFeature) {
addScenarioKeywords(result, psiFile, position);
}
}
}
});
extend(CompletionType.BASIC, inScenario.andNot(inStep), new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
ProcessingContext context,
@NotNull CompletionResultSet result) {
addStepKeywords(result, parameters.getOriginalFile());
}
});
extend(CompletionType.BASIC, inStep, new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
ProcessingContext context,
@NotNull CompletionResultSet result) {
addStepDefinitions(result, parameters.getOriginalFile());
}
});
}
private static void addScenarioKeywords(CompletionResultSet result, PsiFile originalFile, PsiElement originalPosition) {
final Project project = originalFile.getProject();
final GherkinKeywordTable table = GherkinKeywordTable.getKeywordsTable(originalFile, project);
final List<String> keywords = new ArrayList<>();
if (!haveBackground(originalFile)) {
keywords.addAll(table.getBackgroundKeywords());
}
final PsiElement prevElement = getPreviousElement(originalPosition);
if (prevElement != null && prevElement.getNode().getElementType() == GherkinTokenTypes.SCENARIO_KEYWORD) {
String scenarioKeyword = (String)table.getScenarioKeywords().toArray()[0];
result = result.withPrefixMatcher(result.getPrefixMatcher().cloneWithPrefix(scenarioKeyword + " " + result.getPrefixMatcher().getPrefix()));
boolean haveColon = false;
final String elementText = originalPosition.getText();
final int rulezzIndex = elementText.indexOf(INTELLIJ_IDEA_RULEZZZ);
if (rulezzIndex >= 0) {
haveColon = elementText.substring(rulezzIndex + INTELLIJ_IDEA_RULEZZZ.length()).trim().startsWith(":");
}
addKeywordsToResult(table.getScenarioOutlineKeywords(), result, !haveColon, SCENARIO_OUTLINE_KEYWORD_PRIORITY, !haveColon);
} else {
addKeywordsToResult(table.getScenarioKeywords(), result, true, SCENARIO_KEYWORD_PRIORITY, true);
addKeywordsToResult(table.getScenarioOutlineKeywords(), result, true, SCENARIO_OUTLINE_KEYWORD_PRIORITY, true);
}
if (PsiTreeUtil.getParentOfType(originalPosition, GherkinScenarioOutlineImpl.class, GherkinExamplesBlockImpl.class) != null) {
keywords.addAll(table.getExampleSectionKeywords());
}
// add to result
addKeywordsToResult(keywords, result, true);
}
private static PsiElement getPreviousElement(PsiElement element) {
PsiElement prevElement = element.getPrevSibling();
if (prevElement != null && prevElement instanceof PsiWhiteSpace) {
prevElement = prevElement.getPrevSibling();
}
return prevElement;
}
private static void addFeatureKeywords(CompletionResultSet result, PsiFile originalFile) {
final Project project = originalFile.getProject();
final GherkinKeywordTable table = GherkinKeywordTable.getKeywordsTable(originalFile, project);
final Collection<String> keywords = table.getFeaturesSectionKeywords();
// add to result
addKeywordsToResult(keywords, result, true);
}
private static void addKeywordsToResult(final Collection<String> keywords,
final CompletionResultSet result,
final boolean withColonSuffix) {
addKeywordsToResult(keywords, result, withColonSuffix, 0, true);
}
private static void addKeywordsToResult(final Collection<String> keywords,
final CompletionResultSet result,
final boolean withColonSuffix, int priority, boolean withSpace) {
for (String keyword : keywords) {
LookupElement element = createKeywordLookupElement(withColonSuffix ? keyword + ":" : keyword, withSpace);
result.addElement(PrioritizedLookupElement.withPriority(element, priority));
}
}
private static LookupElement createKeywordLookupElement(final String keyword, boolean withSpace) {
LookupElement result = LookupElementBuilder.create(keyword);
if (ApplicationManager.getApplication().isUnitTestMode()) {
result = ((LookupElementBuilder)result).withAutoCompletionPolicy(AutoCompletionPolicy.NEVER_AUTOCOMPLETE);
}
if (withSpace) {
result = TailTypeDecorator.withTail(result, TailType.SPACE);
}
return result;
}
private static boolean haveBackground(PsiFile originalFile) {
PsiElement scenarioParent = PsiTreeUtil.getChildOfType(originalFile, GherkinFeature.class);
if (scenarioParent == null) {
scenarioParent = originalFile;
}
final GherkinScenario[] scenarios = PsiTreeUtil.getChildrenOfType(scenarioParent, GherkinScenario.class);
if (scenarios != null) {
for (GherkinScenario scenario : scenarios) {
if (scenario.isBackground()) {
return true;
}
}
}
return false;
}
private static void addStepKeywords(CompletionResultSet result, PsiFile file) {
if (!(file instanceof GherkinFile)) return;
final GherkinFile gherkinFile = (GherkinFile)file;
addKeywordsToResult(gherkinFile.getStepKeywords(), result, false);
}
private static void addStepDefinitions(CompletionResultSet result, PsiFile file) {
result = result.withPrefixMatcher(new CucumberPrefixMatcher(result.getPrefixMatcher().getPrefix()));
final List<AbstractStepDefinition> definitions = CucumberStepsIndex.getInstance(file.getProject()).getAllStepDefinitions(file);
for (AbstractStepDefinition definition : definitions) {
String text = definition.getCucumberRegex();
if (text != null) {
// trim regexp line start/end markers
text = StringUtil.trimStart(text, "^");
text = StringUtil.trimEnd(text, "$");
text = StringUtil.replace(text, "\\\"", "\"");
for (Map.Entry<String, String> group : GROUP_TYPE_MAP.entrySet()) {
text = StringUtil.replace(text, group.getKey(), group.getValue());
}
for (Map.Entry<String, String> group : INTERPOLATION_PARAMETERS_MAP.entrySet()) {
text = text.replaceAll(group.getKey(), group.getValue());
}
final List<TextRange> ranges = new ArrayList<>();
Matcher m = QUESTION_MARK_PATTERN.matcher(text);
if (m.find()) {
text = m.replaceAll("$1");
}
m = POSSIBLE_GROUP_PATTERN.matcher(text);
while (m.find()) {
text = m.replaceAll("$1");
}
m = PARAMETERS_PATTERN.matcher(text);
while (m.find()) {
ranges.add(new TextRange(m.start(), m.end()));
}
final PsiElement element = definition.getElement();
final LookupElementBuilder lookup = element != null
? LookupElementBuilder.create(element, text).bold()
: LookupElementBuilder.create(text);
result.addElement(lookup.withInsertHandler(new StepInsertHandler(ranges)));
}
}
}
private static class StepInsertHandler implements InsertHandler<LookupElement> {
private final List<TextRange> ranges;
private StepInsertHandler(List<TextRange> ranges) {
this.ranges = ranges;
}
@Override
public void handleInsert(final InsertionContext context, LookupElement item) {
if (!ranges.isEmpty()) {
final PsiElement element = context.getFile().findElementAt(context.getStartOffset());
final GherkinStep step = PsiTreeUtil.getParentOfType(element, GherkinStep.class);
if (step != null) {
final TemplateBuilder builder = TemplateBuilderFactory.getInstance().createTemplateBuilder(step);
int off = context.getStartOffset() - step.getTextRange().getStartOffset();
final String stepText = step.getText();
for (TextRange groupRange : ranges) {
final TextRange shiftedRange = groupRange.shiftRight(off);
final String matchedText = shiftedRange.substring(stepText);
builder.replaceRange(shiftedRange, matchedText);
}
builder.run(context.getEditor(), false);
}
}
}
}
}