diff --git a/cucumber.eclipse.editor.test/src/main/java/cucumber/eclipse/editor/editors/StepHyperlinkTest.java b/cucumber.eclipse.editor.test/src/main/java/cucumber/eclipse/editor/editors/StepHyperlinkTest.java new file mode 100644 index 00000000..7a79a124 --- /dev/null +++ b/cucumber.eclipse.editor.test/src/main/java/cucumber/eclipse/editor/editors/StepHyperlinkTest.java @@ -0,0 +1,43 @@ +package cucumber.eclipse.editor.editors; + + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; +import org.junit.Before; +import org.junit.Test; + +import cucumber.eclipse.steps.integration.Step; + +public class StepHyperlinkTest { + + private StepHyperlink stepHyperlink; + IRegion region; + + @Before + public void setUp() { + region = new Region(0, 10); + Step step = new Step(); + step.setText("Given I have a cat"); + stepHyperlink = new StepHyperlink(region, step); + } + + @Test + public void shouldHaveATypeLabel() { + assertThat(stepHyperlink.getTypeLabel(), equalTo("Gherkin step")); + } + + @Test + public void shouldHaveAnAlternateText() { + assertThat(stepHyperlink.getHyperlinkText(), equalTo("Open step definition")); + } + + @Test + public void shouldReturnTheExpectedRegion() { + assertThat(stepHyperlink.getHyperlinkRegion(), equalTo(region)); + } + + // should have UI automate test to validate the step hyperlink open. +} diff --git a/cucumber.eclipse.editor.test/src/main/java/cucumber/eclipse/editor/editors/StepMatcherTest.java b/cucumber.eclipse.editor.test/src/main/java/cucumber/eclipse/editor/editors/StepMatcherTest.java index 65607fc8..fb0e6ef6 100644 --- a/cucumber.eclipse.editor.test/src/main/java/cucumber/eclipse/editor/editors/StepMatcherTest.java +++ b/cucumber.eclipse.editor.test/src/main/java/cucumber/eclipse/editor/editors/StepMatcherTest.java @@ -1,7 +1,9 @@ package cucumber.eclipse.editor.editors; +import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import java.util.Arrays; @@ -180,4 +182,18 @@ private Step createStep(String text) { return s; } + @Test + public void shouldReturnExpressionWithoutStartingKeyword() { + String statement = stepMatcher.getTextStatement("en", "Given I have a cat"); + assertThat(statement, equalTo("I have a cat")); + + statement = stepMatcher.getTextStatement("en", "When I carress him"); + assertThat(statement, equalTo("I carress him")); + + statement = stepMatcher.getTextStatement("en", "Then he purrs"); + assertThat(statement, equalTo("he purrs")); + + statement = stepMatcher.getTextStatement("en", "And I am happy"); + assertThat(statement, equalTo("I am happy")); + } } diff --git a/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/GherkinConfiguration.java b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/GherkinConfiguration.java index 4572d0fd..a5fed3ff 100644 --- a/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/GherkinConfiguration.java +++ b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/GherkinConfiguration.java @@ -1,6 +1,9 @@ package cucumber.eclipse.editor.editors; import org.eclipse.jface.preference.IPreferenceStore; + +import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import org.eclipse.core.resources.IMarker; @@ -10,6 +13,7 @@ import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.IContentAssistProcessor; import org.eclipse.jface.text.contentassist.IContentAssistant; +import org.eclipse.jface.text.hyperlink.IHyperlinkDetector; import org.eclipse.jface.text.presentation.IPresentationReconciler; import org.eclipse.jface.text.presentation.PresentationReconciler; import org.eclipse.jface.text.quickassist.IQuickAssistAssistant; @@ -161,4 +165,13 @@ public IReconciler getReconciler(ISourceViewer sourceViewer) { public int getTabWidth(ISourceViewer sourceViewer) { return 2; } + + @Override + public IHyperlinkDetector[] getHyperlinkDetectors(ISourceViewer sourceViewer) { + IHyperlinkDetector[] hyperlinkDetectors = super.getHyperlinkDetectors(sourceViewer); + StepHyperlinkDetector stepHyperlinkDetector = new StepHyperlinkDetector(this.editor); + IHyperlinkDetector[] gherkinHyperlinkDetectors = Arrays.copyOf(hyperlinkDetectors, hyperlinkDetectors.length + 1); + gherkinHyperlinkDetectors[hyperlinkDetectors.length] = stepHyperlinkDetector; + return gherkinHyperlinkDetectors; + } } \ No newline at end of file diff --git a/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/StepHyperlink.java b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/StepHyperlink.java new file mode 100644 index 00000000..ef6073ae --- /dev/null +++ b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/StepHyperlink.java @@ -0,0 +1,59 @@ +package cucumber.eclipse.editor.editors; + +import java.util.HashMap; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.hyperlink.IHyperlink; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.ide.IDE; + +import cucumber.eclipse.steps.integration.Step; + +public class StepHyperlink implements IHyperlink { + + private IRegion region; + private Step step; + + public StepHyperlink(IRegion region, Step step) { + this.region = region; + this.step = step; + } + + @Override + public IRegion getHyperlinkRegion() { + return this.region; + } + + @Override + public String getHyperlinkText() { + return "Open step definition"; + } + + @Override + public String getTypeLabel() { + return "Gherkin step"; + } + + @Override + public void open() { + IResource file = this.step.getSource(); + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + + HashMap map = new HashMap(); + map.put(IMarker.LINE_NUMBER, step.getLineNumber()); + IMarker marker; + try { + marker = file.createMarker(IMarker.TEXT); + marker.setAttributes(map); + IDE.openEditor(page, marker); + marker.delete(); + } catch (CoreException e) { + e.printStackTrace(); + } + } + +} diff --git a/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/StepHyperlinkDetector.java b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/StepHyperlinkDetector.java new file mode 100644 index 00000000..fa1aa5df --- /dev/null +++ b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/StepHyperlinkDetector.java @@ -0,0 +1,107 @@ +package cucumber.eclipse.editor.editors; + +import java.util.List; +import java.util.Set; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.hyperlink.IHyperlink; +import org.eclipse.jface.text.hyperlink.IHyperlinkDetector; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.texteditor.ITextEditor; + +import cucumber.eclipse.editor.markers.MarkerIds; +import cucumber.eclipse.editor.markers.MarkerManager; +import cucumber.eclipse.editor.steps.IStepProvider; +import cucumber.eclipse.steps.integration.Step; +import gherkin.lexer.LexingError; +import gherkin.parser.Parser; + +public class StepHyperlinkDetector implements IHyperlinkDetector { + + private Editor editor; + + public StepHyperlinkDetector(Editor editor) { + this.editor = editor; + } + + @Override + public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks) { + if (region == null || textViewer == null) { + return null; + } + + IDocument document = textViewer.getDocument(); + if (document == null) { + return null; + } + + int offset = region.getOffset(); + int lineStartOffset = 0; + + IRegion lineInfo = null; + String currentLine = null; + try { + lineInfo = document.getLineInformationOfOffset(offset); + lineStartOffset = lineInfo.getOffset(); + currentLine = document.get(lineStartOffset, lineInfo.getLength()); + } catch (BadLocationException e) { + return null; + } + + IStepProvider stepProvider = this.editor.getStepProvider(); + Set steps = stepProvider.getStepsInEncompassingProject(); + + StepMatcher stepMatcher = new StepMatcher(); + String language = DocumentUtil.getDocumentLanguage(document); + + // hack to support scenario outline examples + int currentLineNumber = textViewer.getTextWidget().getLineAtOffset(offset) + 1; + List resolvedStepsExpressions = resolveLineStep(editor, currentLineNumber); + Step step = null; + for (String variant : resolvedStepsExpressions) { + step = stepMatcher.matchSteps(language, steps, variant); + if(step != null) { + break; + } + } + + if (step == null) { + return null; + } + + // define the hyperlink region + String textStatement = stepMatcher.getTextStatement(language, currentLine); + int statementStartOffset = lineStartOffset + currentLine.indexOf(textStatement); + + IRegion stepRegion = new Region(statementStartOffset, textStatement.length()); + + return new IHyperlink[] { new StepHyperlink(stepRegion, step) }; + } + + // go through all examples of current scenario outline and generate step strings with replaced variables values + protected static List resolveLineStep(IEditorPart editorPart, int currentLineNumber) { + ITextEditor editor = (ITextEditor) editorPart; + + IDocument document = editor.getDocumentProvider().getDocument(editorPart.getEditorInput()); + + IFileEditorInput fileEditorInput = (IFileEditorInput) editorPart.getEditorInput(); + IFile featureFile = fileEditorInput.getFile(); + MarkerManager markerManager = new MarkerManager(); + PopupMenuFindStepFormatter findStepFormatter = new PopupMenuFindStepFormatter(currentLineNumber); + Parser p = new Parser(findStepFormatter, false); + try { + p.parse(document.get(), "", 0); + } catch (LexingError l) { + markerManager.add(MarkerIds.LEXING_ERROR, featureFile, IMarker.SEVERITY_ERROR, l.getLocalizedMessage(), 1, 0, 0); + } + return findStepFormatter.getResolvedStepNames(); + } + +} diff --git a/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/StepMatcher.java b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/StepMatcher.java index a7f906aa..40e958ac 100644 --- a/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/StepMatcher.java +++ b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/StepMatcher.java @@ -13,16 +13,39 @@ class StepMatcher { private Pattern groupPatternNonParameterMatch = Pattern.compile("(\\(\\?:.+?\\))"); private Pattern groupPattern = Pattern.compile("(\\(.+?\\))"); + public String getTextStatement(String language, String expression) { + Matcher matcher = getBasicStatementMatcher(language, expression); + if(matcher == null) { + return null; + } + if(matcher.matches()) { + return matcher.group(1); + } + return null; + } - public Step matchSteps(String languageCode, Set steps, String currentLine) { - - //System.out.println("StepMatcher matchSteps() steps = " + steps); - Pattern cukePattern = getLanguageKeyWordMatcher(languageCode); + /** + * Get a matcher to ensure text starts with a basic step keyword : Given, When, + * Then, etc + * + * @param language the document language + * @param text the text to match + * @return a matcher + */ + private Matcher getBasicStatementMatcher(String language, String text) { + Pattern cukePattern = getLanguageKeyWordMatcher(language); if (cukePattern == null) return null; - Matcher matcher = cukePattern.matcher(currentLine); + return cukePattern.matcher(text.trim()); + } + + public Step matchSteps(String languageCode, Set steps, String currentLine) { + + //System.out.println("StepMatcher matchSteps() steps = " + steps); + + Matcher matcher = getBasicStatementMatcher(languageCode, currentLine); if (matcher.matches()) {