package org.jetbrains.plugins.cucumber.psi.impl; import com.intellij.lang.ASTNode; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiCheckedRenameElement; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; import com.intellij.psi.TokenType; import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry; import com.intellij.psi.tree.TokenSet; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.IncorrectOperationException; import com.intellij.util.containers.HashSet; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.cucumber.psi.*; import org.jetbrains.plugins.cucumber.psi.refactoring.GherkinChangeUtil; import org.jetbrains.plugins.cucumber.steps.AbstractStepDefinition; import org.jetbrains.plugins.cucumber.steps.reference.CucumberStepReference; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author yole */ public class GherkinStepImpl extends GherkinPsiElementBase implements GherkinStep, PsiCheckedRenameElement { private static final TokenSet TEXT_FILTER = TokenSet .create(GherkinTokenTypes.TEXT, GherkinElementTypes.STEP_PARAMETER, TokenType.WHITE_SPACE, GherkinTokenTypes.STEP_PARAMETER_TEXT, GherkinTokenTypes.STEP_PARAMETER_BRACE); private static final Pattern PARAMETER_SUBSTITUTION_PATTERN = Pattern.compile("<([^>\n\r]+)>"); private final Object LOCK = new Object(); private List<String> mySubstitutions; public GherkinStepImpl(@NotNull ASTNode node) { super(node); } @Override public String toString() { return "GherkinStep:" + getStepName(); } @Nullable public ASTNode getKeyword() { return getNode().findChildByType(GherkinTokenTypes.STEP_KEYWORD); } @Nullable public String getStepName() { return getElementText(); } @Override @NotNull protected String getElementText() { final ASTNode node = getNode(); final ASTNode[] children = node.getChildren(TEXT_FILTER); return StringUtil.join(children, astNode -> astNode.getText(), "").trim(); } @Nullable public GherkinPystring getPystring() { return PsiTreeUtil.findChildOfType(this, GherkinPystring.class); } @Nullable public GherkinTable getTable() { final ASTNode tableNode = getNode().findChildByType(GherkinElementTypes.TABLE); return tableNode == null ? null : (GherkinTable)tableNode.getPsi(); } @Override protected String getPresentableText() { final ASTNode keywordNode = getKeyword(); final String prefix = keywordNode != null ? keywordNode.getText() + ": " : "Step: "; return prefix + getStepName(); } @NotNull @Override public PsiReference[] getReferences() { return ReferenceProvidersRegistry.getReferencesFromProviders(this, GherkinStepImpl.class); } protected void acceptGherkin(GherkinElementVisitor gherkinElementVisitor) { gherkinElementVisitor.visitStep(this); } @NotNull public List<String> getParamsSubstitutions() { synchronized (LOCK) { if (mySubstitutions == null) { final ArrayList<String> substitutions = new ArrayList<>(); // step name final String text = getStepName(); if (StringUtil.isEmpty(text)) { return Collections.emptyList(); } addSubstitutionFromText(text, substitutions); // pystring final GherkinPystring pystring = getPystring(); String pystringText = pystring != null ? pystring.getText() : null; if (!StringUtil.isEmpty(pystringText)) { addSubstitutionFromText(pystringText, substitutions); } // table final GherkinTable table = getTable(); final String tableText = table == null ? null : table.getText(); if (tableText != null) { addSubstitutionFromText(tableText, substitutions); } mySubstitutions = substitutions.isEmpty() ? Collections.emptyList() : substitutions; } return mySubstitutions; } } private static void addSubstitutionFromText(String text, ArrayList<String> substitutions) { final Matcher matcher = PARAMETER_SUBSTITUTION_PATTERN.matcher(text); boolean result = matcher.find(); if (!result) { return; } do { final String substitution = matcher.group(1); if (!StringUtil.isEmpty(substitution) && !substitutions.contains(substitution)) { substitutions.add(substitution); } result = matcher.find(); } while (result); } @Override public void subtreeChanged() { super.subtreeChanged(); clearCaches(); } @Nullable public GherkinStepsHolder getStepHolder() { final PsiElement parent = getParent(); return parent != null ? (GherkinStepsHolder)parent : null; } private void clearCaches() { synchronized (LOCK) { mySubstitutions = null; } } @Nullable public String getSubstitutedName() { List<String> sustitutedNameList = new ArrayList<>(getSubstitutedNameList(1)); return sustitutedNameList.size() > 0 ? sustitutedNameList.get(0) : getStepName(); } public Set<String> getSubstitutedNameList(int maxCount) { final Set<String> result = new HashSet<>(); final GherkinStepsHolder holder = getStepHolder(); final String stepName = getStepName(); if (stepName != null) { if (holder instanceof GherkinScenarioOutline) { final GherkinScenarioOutline outline = (GherkinScenarioOutline)holder; final List<GherkinExamplesBlock> examplesBlocks = outline.getExamplesBlocks(); for (GherkinExamplesBlock examplesBlock : examplesBlocks) { final GherkinTable table = examplesBlock.getTable(); if (table != null) { final List<GherkinTableRow> rows = table.getDataRows(); for (GherkinTableRow row : rows) { result.add(substituteText(stepName, table.getHeaderRow(), row)); if (result.size() == maxCount) { return result; } } } } } } if (result.size() == 0 && stepName != null) { result.add(stepName); } return result; } @NotNull public Set<String> getSubstitutedNameList() { return getSubstitutedNameList(Integer.MAX_VALUE); } private static String substituteText(String stepName, GherkinTableRow headerRow, GherkinTableRow row) { final List<GherkinTableCell> headerCells = headerRow.getPsiCells(); final List<GherkinTableCell> dataCells = row.getPsiCells(); for (int i = 0, headerCellsNumber = headerCells.size(), dataCellsNumber = dataCells.size(); i < headerCellsNumber && i < dataCellsNumber; i++) { final String cellText = headerCells.get(i).getText().trim(); stepName = stepName.replace("<" + cellText + ">", dataCells.get(i).getText().trim()); } return stepName; } @Override public PsiElement setName(@NonNls @NotNull String name) throws IncorrectOperationException { GherkinStep newStep = GherkinChangeUtil.createStep(getKeyword().getText() + " " + name, getProject()); replace(newStep); return newStep; } @Override public String getName() { final ASTNode keyword = getKeyword(); final int keywordLength = keyword != null ? keyword.getTextLength() : 0; return getText().substring(keywordLength).trim(); } @NotNull @Override public Collection<AbstractStepDefinition> findDefinitions() { final List<AbstractStepDefinition> result = new ArrayList<>(); for (final PsiReference reference : getReferences()) { if (reference instanceof CucumberStepReference) { result.addAll(((CucumberStepReference)reference).resolveToDefinitions()); } } return result; } @Override public boolean isRenameAllowed(@Nullable final String newName) { final Collection<AbstractStepDefinition> definitions = findDefinitions(); if (definitions.isEmpty()) { return false; // No sense to rename step with out of definitions } for (final AbstractStepDefinition definition : definitions) { if (!definition.supportsRename(newName)) { return false; //At least one definition does not support renaming } } return true; // Nothing prevents us from renaming } @Override public void checkSetName(final String name) { if (!isRenameAllowed(name)) { throw new IncorrectOperationException(RENAME_BAD_SYMBOLS_MESSAGE); } } }