diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 00000000..ec606123
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,172 @@
+# GitHub Actions Workflow is created for testing and preparing the plugin release in the following steps:
+# - Validate Gradle Wrapper.
+# - Run 'test' and 'verifyPlugin' tasks.
+# - Run Qodana inspections.
+# - Run the 'buildPlugin' task and prepare artifact for further tests.
+# - Run the 'runPluginVerifier' task.
+# - Create a draft release.
+#
+# The workflow is triggered on push and pull_request events.
+#
+# GitHub Actions reference: https://help.github.com/en/actions
+#
+## JBIJPPTPL
+
+name: Build
+on:
+ # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g., for dependabot pull requests)
+ push:
+ branches: [ master ]
+ # Trigger the workflow on any pull request
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+
+ # Prepare environment and build the plugin
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.properties.outputs.version }}
+ changelog: ${{ steps.properties.outputs.changelog }}
+ pluginVerifierHomeDir: ${{ steps.properties.outputs.pluginVerifierHomeDir }}
+ steps:
+
+ # Check out the current repository
+ - name: Fetch Sources
+ uses: actions/checkout@v4
+
+ # Set up Java environment for the next steps
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: zulu
+ java-version: 21
+
+ # Setup Gradle
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v4
+
+ # Set environment variables
+ - name: Export Properties
+ id: properties
+ shell: bash
+ run: |
+ PROPERTIES="$(./gradlew properties --console=plain -q)"
+ VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')"
+ CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)"
+
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "pluginVerifierHomeDir=~/.pluginVerifier" >> $GITHUB_OUTPUT
+
+ echo "changelog<> $GITHUB_OUTPUT
+ echo "$CHANGELOG" >> $GITHUB_OUTPUT
+ echo "EOF" >> $GITHUB_OUTPUT
+
+ # Build plugin
+ - name: Build plugin
+ run: ./gradlew buildPlugin
+
+ # Prepare plugin archive content for creating artifact
+ - name: Prepare Plugin Artifact
+ id: artifact
+ shell: bash
+ run: |
+ cd ${{ github.workspace }}/build/distributions
+ FILENAME=`ls *.zip`
+ unzip "$FILENAME" -d content
+
+ echo "filename=${FILENAME:0:-4}" >> $GITHUB_OUTPUT
+
+ # Store already-built plugin as an artifact for downloading
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ steps.artifact.outputs.filename }}
+ path: ./build/distributions/content/*/*
+
+ # Run tests and upload a code coverage report
+ test:
+ name: Test
+ needs: [ build ]
+ runs-on: ubuntu-latest
+ steps:
+
+ # Check out the current repository
+ - name: Fetch Sources
+ uses: actions/checkout@v4
+
+ # Set up Java environment for the next steps
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: zulu
+ java-version: 21
+
+ # Setup Gradle
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v4
+
+ # Run tests
+ - name: Run Tests
+ run: ./gradlew check
+
+ # Collect Tests Result of failed tests
+ - name: Collect Tests Result
+ if: ${{ failure() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: tests-result
+ path: ${{ github.workspace }}/build/reports/tests
+
+ # Run plugin structure verification along with IntelliJ Plugin Verifier
+ verify:
+ name: Verify plugin
+ needs: [ build ]
+ runs-on: ubuntu-latest
+ steps:
+
+ # Free GitHub Actions Environment Disk Space
+ - name: Maximize Build Space
+ uses: jlumbroso/free-disk-space@main
+ with:
+ tool-cache: false
+ large-packages: false
+
+ # Check out the current repository
+ - name: Fetch Sources
+ uses: actions/checkout@v4
+
+ # Set up Java environment for the next steps
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: zulu
+ java-version: 21
+
+ # Setup Gradle
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v4
+
+ # Cache Plugin Verifier IDEs
+ - name: Setup Plugin Verifier IDEs Cache
+ uses: actions/cache@v4
+ with:
+ path: ${{ needs.build.outputs.pluginVerifierHomeDir }}/ides
+ key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }}
+
+ # Run Verify Plugin task and IntelliJ Plugin Verifier tool
+ - name: Run Plugin Verification tasks
+ run: ./gradlew verifyPlugin -Dplugin.verifier.home.dir=${{ needs.build.outputs.pluginVerifierHomeDir }}
+
+ # Collect Plugin Verifier Result
+ - name: Collect Plugin Verifier Result
+ if: ${{ always() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: pluginVerifier-result
+ path: ${{ github.workspace }}/build/reports/pluginVerifier
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..d54b9c72
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,61 @@
+# use glob syntax.
+syntax: glob
+*.ser
+*.class
+*~
+*.bak
+*.off
+*.old
+.DS_Store
+
+# lib'n misc
+#lib/
+testLib/
+file/
+
+# eclipse conf file
+.settings
+.classpath
+.project
+
+
+# building
+/build/
+target/
+bin/
+test-output/
+out/
+
+# Exported plugin for deployment
+IntelliJBehave.zip
+
+# other scm
+.svn
+.CVS
+.hg*
+
+# switch to regexp syntax.
+# syntax: regexp
+# ^\.pc/
+
+# IntelliJ
+*.iml
+*.ipr
+*.iws
+.idea
+.intellijPlatform
+
+# Atlassian Plugin Configuration
+atlassian-ide-plugin.xml
+
+# Plugins
+/providedLib
+
+# Gradle
+.gradle/
+#gradle.properties
+
+# Other files
+/local.properties
+#*.jar
+!gradle/wrapper/gradle-wrapper.jar
diff --git a/.run/Run Build.run.xml b/.run/Run Build.run.xml
new file mode 100644
index 00000000..3326ba1a
--- /dev/null
+++ b/.run/Run Build.run.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+
+
+
\ No newline at end of file
diff --git a/.run/Run IDE with Plugin.run.xml b/.run/Run IDE with Plugin.run.xml
new file mode 100644
index 00000000..d15ff681
--- /dev/null
+++ b/.run/Run IDE with Plugin.run.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+
+
+
\ No newline at end of file
diff --git a/.run/Run Verifications.run.xml b/.run/Run Verifications.run.xml
new file mode 100644
index 00000000..32783f57
--- /dev/null
+++ b/.run/Run Verifications.run.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+ false
+
+
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..8dc590e3
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,98 @@
+# Release notes
+
+## [Unreleased]
+
+## [1.69.2]
+### Changed
+- Improved some of the step definition and annotation handling logic by reducing the number of `Stream` creations.
+- Improved the performance of finding unused step declarations by reducing the number of necessary project content iterations.
+- Improved and deduplicated some logic regarding the step annotations processing for step completions in Story files.
+
+### Fixed
+- [#103](https://github.com/witspirit/IntelliJBehave/issues/103): Improved the performance of the listener that processes changes in step definition methods.
+
+## [1.69.1]
+### Changed
+- JUnit Jupiter test dependencies brought in by jbehave-core are now excluded from the built plugin archive.
+
+## [1.69.0]
+### Changed
+- New supported IDE version range: 2025.1+.
+
+### Fixed
+- Fixed a step reference resolution issue. Contributed by @tomb50
+
+## [1.68.0]
+### Changed
+- New supported IDE version range: 2024.3 - 2025.1-EAP.
+
+## [1.67.5]
+### Fixed
+- Added further exception handling when analyzing Java and Kotlin classes if they are step definition classes.
+
+## [1.67.4]
+### Fixed
+- Added exception handling when analyzing Java classes if they are step definition classes.
+
+## [1.67.3]
+### Fixed
+- Added exception handling when analyzing Kotlin classes if they are step definition classes.
+
+## [1.67.2]
+### Changed
+- Added a minor performance improvement to pattern variant building.
+
+### Fixed
+- [#94](https://github.com/witspirit/IntelliJBehave/issues/94): Improved matching steps with patterns with empty parameter values.
+
+## [1.67.1]
+### Fixed
+- Fixed the K2 compiler compatibility configuration.
+
+## [1.67.0]
+### Changed
+- New supported IDE version range: 2024.2.1 - 2024.3.*.
+The previous version, 1.66.0 (and bugfixes of it) of JBehave Support will remain to support IDEs up to version 2024.2.0.2.
+- Updated the project to use the IntelliJ Platform Gradle Plugin 2.0.
+- Updated the project to use JDK 21.
+- Updated project configuration to make sure the plugin works when the K2 Kotlin compiler is enabled.
+- Removed a couple of deprecated API usage.
+
+### Fixed
+- Fixed the listener that tracks modifications of JBehave step def classes. It no longer fails when it encounters invalid files.
+- Fixed a potential exception during highlighting undefined steps.
+
+## [1.66.0]
+### Changed
+- New supported IDE version range: 2023.2.8 - 2024.2.0.2
+- Added code documentation to a handful of Kotlin step definitions related classes.
+
+## [1.65.0]
+### Changed
+- Added caching for the JBehave step reference resolution and Given-When-Then step annotation lookup.
+
+## [1.64.0]
+### Changed
+- New supported IDE version range: 2023.1.6-2024.2-EAP
+- Did some optimizations on the unused step definition method inspection. Now it also marks the method name instead of
+the entire method.
+- Did some optimizations on the undefined step inspection.
+- Removed duplicate reporting of steps that don't have step definition methods.
+- Removed the class `StoryAnnotator` that didn't seem to do much useful.
+- Improved the implementations of JBehave step reference handling.
+
+## [1.63.0]
+### Changed
+- New supported IDE version range: 2022.2.5-2024.1-EAP
+
+## [1.62.0]
+### Changed
+- New supported IDE version range: 2022.1-2023.3
+- Removed some deprecated API usage, and simplified some related code.
+
+## [1.61.0]
+### Changed
+- Added support for IJ-2023.2 and dropped support for IJ-2021.2.
+- Updated the plugin configuration to be based on the intellij-platform-plugin-template
+- Updated GitHub Actions integration to build and validate the plugin.
+- Removed unnecessary project dependencies.
diff --git a/CHANGELOG_OLD.md b/CHANGELOG_OLD.md
new file mode 100644
index 00000000..e8042054
--- /dev/null
+++ b/CHANGELOG_OLD.md
@@ -0,0 +1,44 @@
+# Release notes - old
+
+## 1.60
+### Changed
+- Added support for IJ-2023.1.
+
+## 1.59
+- Upgraded plugin to support IJ-2022.3.
+- Dropped plugin to support IJ-2021.1.
+- Upgraded kotlin and gradle-intellij-plugin versions.
+- Improved the appearance of the Story file structure view.
+
+## 1.58
+- Upgraded plugin to support IJ-2022.2.
+- Added some run configurations for plugin development.
+
+## 1.57
+- Added structure view for .story files that can be triggered via Ctrl+12 or similar shortcut.
+
+## 1.56
+- Upgraded plugin to support IJ-2022.1.
+- Upgraded gradle, gradle-intellij-plugin and JUnit versions.
+
+## 1.55
+- Upgraded plugin to work with IJ-2021.3.
+
+# 1.54a
+- Fixed packaging of the plugin.
+
+## 1.54
+Important note: This version is no longer available for IntelliJ version prior to 2021.1.
+
+- Upgraded plugin to work with IJ-2021.2.
+- Fixed spellchecking support for .story files.
+- Upgraded gradle to 7.1.1
+- Upgraded kotlin library version to 1.4.32
+
+## 1.53
+- Some small bug fixes to prevent errors
+
+## 1.5
+- Ability to find usages of a step method
+- Inspections speed has been increased
+- JBehave icon has been updated
diff --git a/IntelliJBehave.iml b/IntelliJBehave.iml
deleted file mode 100644
index f72ee216..00000000
--- a/IntelliJBehave.iml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/META-INF/plugin.xml b/META-INF/plugin.xml
deleted file mode 100644
index 90070a4a..00000000
--- a/META-INF/plugin.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
- IntelliJBehave
- IntelliJBehave
-
- The plugin provides the following features:
-
-
-
Basic syntax highlighting for JBehave story files
-
Jump to step definition in Java or Groovy
-
Error Highlighting in story if step was not defined
-
Create new story files from a configurable story template
-
Comment/uncomment lines in story files
-
Code inspections to report unused steps definitions and undefined step usages
\ No newline at end of file
diff --git a/src/com/github/kumaraman21/intellijbehave/codeInspector/UndefinedStepsInspection.java b/src/com/github/kumaraman21/intellijbehave/codeInspector/UndefinedStepsInspection.java
deleted file mode 100644
index 3ab53166..00000000
--- a/src/com/github/kumaraman21/intellijbehave/codeInspector/UndefinedStepsInspection.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.codeInspector;
-
-import com.github.kumaraman21.intellijbehave.parser.StepPsiElement;
-import com.intellij.codeInspection.BaseJavaLocalInspectionTool;
-import com.intellij.codeInspection.ProblemsHolder;
-import com.intellij.psi.PsiElement;
-import com.intellij.psi.PsiElementVisitor;
-import org.jetbrains.annotations.Nls;
-import org.jetbrains.annotations.NotNull;
-
-public class UndefinedStepsInspection extends BaseJavaLocalInspectionTool {
-
- @Nls
- @NotNull
- @Override
- public String getGroupDisplayName() {
- return "JBehave";
- }
-
- @Nls
- @NotNull
- @Override
- public String getDisplayName() {
- return "Undefined step";
- }
-
- @NotNull
- @Override
- public String getShortName() {
- return "UndefinedStep";
- }
-
- @Override
- public String getStaticDescription() {
- return super.getStaticDescription();
- }
-
- @Override
- public boolean isEnabledByDefault() {
- return true;
- }
-
- @NotNull
- @Override
- public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
- return new PsiElementVisitor() {
-
- @Override
- public void visitElement(PsiElement psiElement) {
- super.visitElement(psiElement);
-
- if(! (psiElement instanceof StepPsiElement)) {
- return;
- }
-
- StepPsiElement stepPsiElement = (StepPsiElement) psiElement;
- if(stepPsiElement.getReference().resolve() == null) {
- holder.registerProblem(stepPsiElement, "Step #ref is not defined");
- }
- }
- };
- }
-}
-
diff --git a/src/com/github/kumaraman21/intellijbehave/codeInspector/UndefinedStepsInspectionProvider.java b/src/com/github/kumaraman21/intellijbehave/codeInspector/UndefinedStepsInspectionProvider.java
deleted file mode 100644
index 0b26ac27..00000000
--- a/src/com/github/kumaraman21/intellijbehave/codeInspector/UndefinedStepsInspectionProvider.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.codeInspector;
-
-import com.intellij.codeInspection.InspectionToolProvider;
-
-public class UndefinedStepsInspectionProvider implements InspectionToolProvider {
- public Class[] getInspectionClasses() {
- return new Class[] { UndefinedStepsInspection.class };
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/codeInspector/UnusedStepsInspection.java b/src/com/github/kumaraman21/intellijbehave/codeInspector/UnusedStepsInspection.java
deleted file mode 100644
index b83711af..00000000
--- a/src/com/github/kumaraman21/intellijbehave/codeInspector/UnusedStepsInspection.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.codeInspector;
-
-import com.github.kumaraman21.intellijbehave.parser.StepPsiElement;
-import com.github.kumaraman21.intellijbehave.parser.StoryFileImpl;
-import com.intellij.codeInspection.BaseJavaLocalInspectionTool;
-import com.intellij.codeInspection.ProblemsHolder;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.roots.ContentIterator;
-import com.intellij.openapi.roots.ProjectRootManager;
-import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.psi.*;
-import org.jbehave.core.annotations.*;
-import org.jetbrains.annotations.Nls;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.List;
-import java.util.Set;
-
-import static com.github.kumaraman21.intellijbehave.utility.ProjectUtils.getCurrentProject;
-import static com.google.common.collect.Lists.newArrayList;
-import static com.google.common.collect.Sets.newHashSet;
-
-public class UnusedStepsInspection extends BaseJavaLocalInspectionTool {
-
- private static final List JBEHAVE_ANNOTATIONS = newArrayList(
- Given.class.getName(),
- When.class.getName(),
- Then.class.getName(),
- Alias.class.getName(),
- Aliases.class.getName());
-
- @Nls
- @NotNull
- @Override
- public String getGroupDisplayName() {
- return "JBehave";
- }
-
- @Nls
- @NotNull
- @Override
- public String getDisplayName() {
- return "Unused step declaration";
- }
-
- @NotNull
- @Override
- public String getShortName() {
- return "UnusedStepDeclaration";
- }
-
- @Override
- public String getStaticDescription() {
- return super.getStaticDescription();
- }
-
- @Override
- public boolean isEnabledByDefault() {
- return true;
- }
-
- @NotNull
- @Override
- public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
- return new JavaElementVisitor() {
-
- @Override
- public void visitElement(PsiElement element) {
- super.visitElement(element);
-
- if(! (element instanceof PsiAnnotation)) {
- return;
- }
-
- PsiAnnotation annotation = (PsiAnnotation)element;
- if(! JBEHAVE_ANNOTATIONS.contains(annotation.getQualifiedName())) {
- return;
- }
-
- Project project = getCurrentProject();
- StepUsageFinder stepUsageFinder = new StepUsageFinder(project);
- ProjectRootManager.getInstance(project).getFileIndex().iterateContent(stepUsageFinder);
- Set stepUsages = stepUsageFinder.getStepUsages();
-
- for (StepPsiElement stepUsage : stepUsages) {
- if(stepUsage.getReference().resolve() == annotation) {
- return;
- }
- }
-
- holder.registerProblem(annotation, "Step #ref is never used");
- }
- };
- }
-
- private static class StepUsageFinder implements ContentIterator {
- private Project project;
- private Set stepUsages = newHashSet();
-
- private StepUsageFinder(Project project) {
- this.project = project;
- }
-
- @Override
- public boolean processFile(VirtualFile virtualFile) {
- PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile);
- if (psiFile instanceof StoryFileImpl) {
- List stepPsiElements = ((StoryFileImpl)psiFile).getSteps();
- stepUsages.addAll(stepPsiElements);
- }
- return true;
- }
-
- public Set getStepUsages() {
- return stepUsages;
- }
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/highlighter/Story.flex b/src/com/github/kumaraman21/intellijbehave/highlighter/Story.flex
deleted file mode 100644
index 91b82b0f..00000000
--- a/src/com/github/kumaraman21/intellijbehave/highlighter/Story.flex
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.github.kumaraman21.intellijbehave.highlighter;
-
-import com.intellij.lexer.FlexLexer;
-import com.intellij.psi.tree.IElementType;
-
-%%
-
-%class _StoryLexer
-%implements FlexLexer
-%unicode
-%function advance
-%type IElementType
-%eof{ return;
-%eof}
-
-CRLF= \n | \r | \r\n
-WHITE_SPACE_CHAR=[\ \n\r\t\f]
-TEXT_CHAR=[^\n\r]
-COMMENT=("!--")[^\r\n]*
-
-%state IN_SCENARIO
-%state IN_STEP
-
-%%
-
- {CRLF}+"Scenario:" {TEXT_CHAR}+ { yybegin(IN_SCENARIO); return StoryTokenType.SCENARIO_TEXT; }
- {CRLF}+"Scenario:" {TEXT_CHAR}+ { yybegin(IN_SCENARIO); return StoryTokenType.SCENARIO_TEXT; }
- {CRLF}+"Scenario:" {TEXT_CHAR}+ { yybegin(IN_SCENARIO); return StoryTokenType.SCENARIO_TEXT; }
-
- {CRLF}+"Given" {WHITE_SPACE_CHAR} { yybegin(IN_STEP); return StoryTokenType.STEP_TYPE; }
- {CRLF}+"When" {WHITE_SPACE_CHAR} { yybegin(IN_STEP); return StoryTokenType.STEP_TYPE; }
- {CRLF}+"Then" {WHITE_SPACE_CHAR} { yybegin(IN_STEP); return StoryTokenType.STEP_TYPE; }
- {CRLF}+"And" {WHITE_SPACE_CHAR} { yybegin(IN_STEP); return StoryTokenType.STEP_TYPE; }
- {CRLF}+{WHITE_SPACE_CHAR}* "|" {TEXT_CHAR}* { yybegin(IN_SCENARIO); return StoryTokenType.TABLE_ROW; }
-
- {COMMENT} { return StoryTokenType.COMMENT; }
- .* { return StoryTokenType.STORY_DESCRIPTION; }
- .* { return StoryTokenType.STORY_DESCRIPTION; }
-
- {TEXT_CHAR}* { yybegin(IN_SCENARIO); return StoryTokenType.STEP_TEXT; }
-
-{WHITE_SPACE_CHAR}+ { return StoryTokenType.WHITE_SPACE; }
-. { return StoryTokenType.BAD_CHARACTER; }
diff --git a/src/com/github/kumaraman21/intellijbehave/highlighter/StorySyntaxHighlighter.java b/src/com/github/kumaraman21/intellijbehave/highlighter/StorySyntaxHighlighter.java
deleted file mode 100644
index ff5d8ec7..00000000
--- a/src/com/github/kumaraman21/intellijbehave/highlighter/StorySyntaxHighlighter.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.highlighter;
-
-import com.intellij.lexer.Lexer;
-import com.intellij.openapi.editor.SyntaxHighlighterColors;
-import com.intellij.openapi.editor.colors.CodeInsightColors;
-import com.intellij.openapi.editor.colors.TextAttributesKey;
-import com.intellij.openapi.fileTypes.SyntaxHighlighterBase;
-import com.intellij.psi.tree.IElementType;
-import gnu.trove.THashMap;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.Map;
-
-public class StorySyntaxHighlighter extends SyntaxHighlighterBase {
-
- private static final Map KEYS;
-
- private static final TextAttributesKey STORY_DESCRIPTION_ATTRIBUTES
- = TextAttributesKey.createTextAttributesKey("STORY_DESCRIPTION_ATTRIBUTES",
- SyntaxHighlighterColors.NUMBER.getDefaultAttributes());
- private static final TextAttributesKey SCENARIO_TEXT_ATTRIBUTES
- = TextAttributesKey.createTextAttributesKey("SCENARIO_TEXT_ATTRIBUTES",
- CodeInsightColors.STATIC_FIELD_ATTRIBUTES.getDefaultAttributes());
- private static final TextAttributesKey STEP_TYPE_ATTRIBUTES
- = TextAttributesKey.createTextAttributesKey("STEP_TYPE_ATTRIBUTES",
- SyntaxHighlighterColors.KEYWORD.getDefaultAttributes());
- private static final TextAttributesKey STEP_TEXT_ATTRIBUTES
- = TextAttributesKey.createTextAttributesKey("STEP_TEXT_ATTRIBUTES",
- SyntaxHighlighterColors.STRING.getDefaultAttributes());
- private static final TextAttributesKey COMMENT_ATTRIBUTES
- = TextAttributesKey.createTextAttributesKey("COMMENT_ATTRIBUTES",
- SyntaxHighlighterColors.LINE_COMMENT.getDefaultAttributes());
- private static final TextAttributesKey BAD_CHARACTER_ATTRIBUTES
- = TextAttributesKey.createTextAttributesKey("BAD_CHARACTER_ATTRIBUTES",
- SyntaxHighlighterColors.INVALID_STRING_ESCAPE.getDefaultAttributes());
-
- static {
- KEYS = new THashMap();
-
- KEYS.put(StoryTokenType.STORY_DESCRIPTION, STORY_DESCRIPTION_ATTRIBUTES);
- KEYS.put(StoryTokenType.SCENARIO_TEXT, SCENARIO_TEXT_ATTRIBUTES);
- KEYS.put(StoryTokenType.STEP_TYPE, STEP_TYPE_ATTRIBUTES);
- KEYS.put(StoryTokenType.STEP_TEXT, STEP_TEXT_ATTRIBUTES);
- KEYS.put(StoryTokenType.TABLE_ROW, STEP_TEXT_ATTRIBUTES);
- KEYS.put(StoryTokenType.COMMENT, COMMENT_ATTRIBUTES);
- KEYS.put(StoryTokenType.BAD_CHARACTER, BAD_CHARACTER_ATTRIBUTES);
- }
-
- @NotNull
- @Override
- public Lexer getHighlightingLexer() {
- return new StorySyntaxHighlightingLexer();
- }
-
- @NotNull
- @Override
- public TextAttributesKey[] getTokenHighlights(IElementType tokenType) {
- return new TextAttributesKey[]{KEYS.get(tokenType)};
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/highlighter/StoryTokenType.java b/src/com/github/kumaraman21/intellijbehave/highlighter/StoryTokenType.java
deleted file mode 100644
index ef27b640..00000000
--- a/src/com/github/kumaraman21/intellijbehave/highlighter/StoryTokenType.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.highlighter;
-
-import com.intellij.psi.TokenType;
-import com.intellij.psi.tree.IElementType;
-import org.jetbrains.annotations.NonNls;
-
-import static com.github.kumaraman21.intellijbehave.language.StoryFileType.STORY_FILE_TYPE;
-
-public class StoryTokenType extends IElementType {
-
- public StoryTokenType(@NonNls String debugName) {
- super(debugName, STORY_FILE_TYPE.getLanguage());
- }
-
- public static final IElementType WHITE_SPACE = TokenType.WHITE_SPACE;
- public static final IElementType BAD_CHARACTER = TokenType.BAD_CHARACTER;
-
- public static final IElementType STORY_DESCRIPTION = new StoryTokenType("STORY_DESCRIPTION");
- public static final IElementType SCENARIO_TEXT = new StoryTokenType("SCENARIO_TEXT");
-
- public static final IElementType STEP_TYPE = new StoryTokenType("STEP_TYPE");
- public static final IElementType STEP_TEXT = new StoryTokenType("STEP_TEXT");
-
- public static final IElementType TABLE_ROW = new StoryTokenType("TABLE_ROW");
-
- public static final IElementType COMMENT = new StoryTokenType("COMMENT");
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/highlighter/_StoryLexer.java b/src/com/github/kumaraman21/intellijbehave/highlighter/_StoryLexer.java
deleted file mode 100644
index a2d1c066..00000000
--- a/src/com/github/kumaraman21/intellijbehave/highlighter/_StoryLexer.java
+++ /dev/null
@@ -1,544 +0,0 @@
-/* The following code was generated by JFlex 1.4.3 on 12/21/11 8:00 PM */
-
-package com.github.kumaraman21.intellijbehave.highlighter;
-
-import com.intellij.lexer.FlexLexer;
-import com.intellij.psi.tree.IElementType;
-
-
-/**
- * This class is a scanner generated by
- * JFlex 1.4.3
- * on 12/21/11 8:00 PM from the specification file
- * Story.flex
- */
-class _StoryLexer implements FlexLexer {
- /** initial size of the lookahead buffer */
- private static final int ZZ_BUFFERSIZE = 16384;
-
- /** lexical states */
- public static final int IN_SCENARIO = 2;
- public static final int YYINITIAL = 0;
- public static final int IN_STEP = 4;
-
- /**
- * ZZ_LEXSTATE[l] is the state in the DFA for the lexical state l
- * ZZ_LEXSTATE[l+1] is the state in the DFA for the lexical state l
- * at the beginning of a line
- * l is of the form l = 2*k, k a non negative integer
- */
- private static final int ZZ_LEXSTATE[] = {
- 0, 0, 1, 1, 2, 2
- };
-
- /**
- * Translates characters to character classes
- */
- private static final String ZZ_CMAP_PACKED =
- "\11\0\1\3\1\1\1\0\1\3\1\2\22\0\1\3\1\4\13\0"+
- "\1\5\14\0\1\16\6\0\1\23\5\0\1\17\13\0\1\6\1\21"+
- "\2\0\1\21\11\0\1\12\1\0\1\7\1\24\1\10\2\0\1\22"+
- "\1\14\4\0\1\11\1\15\2\0\1\13\3\0\1\20\5\0\1\25"+
- "\uff83\0";
-
- /**
- * Translates characters to character classes
- */
- private static final char [] ZZ_CMAP = zzUnpackCMap(ZZ_CMAP_PACKED);
-
- /**
- * Translates DFA states to action switch labels.
- */
- private static final int [] ZZ_ACTION = zzUnpackAction();
-
- private static final String ZZ_ACTION_PACKED_0 =
- "\2\1\1\2\1\1\1\3\2\1\1\3\2\1\2\2"+
- "\1\3\1\0\1\1\1\3\3\0\1\4\4\1\1\4"+
- "\1\1\1\0\1\1\3\0\3\1\1\5\1\0\1\1"+
- "\2\0\2\1\1\0\1\1\2\6\1\0\1\1\1\0"+
- "\1\1\1\0\1\1\1\0\1\1\1\0\1\1\2\7";
-
- private static int [] zzUnpackAction() {
- int [] result = new int[57];
- int offset = 0;
- offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result);
- return result;
- }
-
- private static int zzUnpackAction(String packed, int offset, int [] result) {
- int i = 0; /* index in packed string */
- int j = offset; /* index in unpacked array */
- int l = packed.length();
- while (i < l) {
- int count = packed.charAt(i++);
- int value = packed.charAt(i++);
- do result[j++] = value; while (--count > 0);
- }
- return j;
- }
-
-
- /**
- * Translates a state to a row index in the transition table
- */
- private static final int [] ZZ_ROWMAP = zzUnpackRowMap();
-
- private static final String ZZ_ROWMAP_PACKED_0 =
- "\0\0\0\26\0\54\0\102\0\130\0\156\0\204\0\232"+
- "\0\260\0\306\0\334\0\362\0\u0108\0\u011e\0\u0134\0\u014a"+
- "\0\u0160\0\u0176\0\u018c\0\u01a2\0\u01b8\0\u01ce\0\u01e4\0\u01fa"+
- "\0\u0210\0\u0226\0\u023c\0\u0252\0\u0268\0\u027e\0\u0294\0\u02aa"+
- "\0\u02c0\0\u02d6\0\u02ec\0\u0302\0\u0318\0\u032e\0\u0344\0\u035a"+
- "\0\u0370\0\u0386\0\u039c\0\u03b2\0\102\0\u03c8\0\u03de\0\u03f4"+
- "\0\u040a\0\u0420\0\u0436\0\u044c\0\u0462\0\u0478\0\u048e\0\u0478"+
- "\0\u048e";
-
- private static int [] zzUnpackRowMap() {
- int [] result = new int[57];
- int offset = 0;
- offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result);
- return result;
- }
-
- private static int zzUnpackRowMap(String packed, int offset, int [] result) {
- int i = 0; /* index in packed string */
- int j = offset; /* index in unpacked array */
- int l = packed.length();
- while (i < l) {
- int high = packed.charAt(i++) << 16;
- result[j++] = high | packed.charAt(i++);
- }
- return j;
- }
-
- /**
- * The transition table of the DFA
- */
- private static final int [] ZZ_TRANS = zzUnpackTrans();
-
- private static final String ZZ_TRANS_PACKED_0 =
- "\1\4\1\5\1\6\1\7\23\4\1\10\1\11\1\7"+
- "\1\12\21\4\1\13\2\5\1\14\22\13\1\4\1\0"+
- "\24\4\1\0\2\5\1\15\2\0\1\16\17\0\1\4"+
- "\1\5\1\6\1\7\2\4\1\17\20\4\1\15\2\7"+
- "\22\4\1\0\2\10\1\20\2\0\1\16\10\0\1\21"+
- "\1\0\1\22\1\0\1\23\1\0\1\24\1\4\1\10"+
- "\1\11\1\25\2\4\1\17\10\4\1\26\1\4\1\27"+
- "\1\4\1\30\1\4\1\31\1\4\1\0\3\4\1\32"+
- "\20\4\1\13\2\0\24\13\2\15\1\14\22\13\1\0"+
- "\3\15\31\0\1\33\16\0\1\4\1\0\5\4\1\34"+
- "\16\4\1\0\3\20\21\0\1\24\14\0\1\35\33\0"+
- "\1\36\14\0\1\37\14\0\1\24\2\0\23\24\1\4"+
- "\1\20\2\25\21\4\1\31\1\4\1\0\12\4\1\40"+
- "\12\4\1\0\20\4\1\41\4\4\1\0\7\4\1\42"+
- "\14\4\1\31\1\0\1\4\23\31\1\4\1\0\3\4"+
- "\1\43\20\4\10\0\1\44\15\0\1\4\1\0\6\4"+
- "\1\45\15\4\20\0\1\36\15\0\1\46\41\0\1\47"+
- "\1\0\1\4\1\0\16\4\1\41\6\4\1\0\6\4"+
- "\1\50\16\4\1\0\22\4\1\51\1\4\1\43\1\0"+
- "\1\4\23\43\11\0\1\52\14\0\1\4\1\0\7\4"+
- "\1\53\14\4\11\0\1\47\15\0\3\54\22\0\1\4"+
- "\1\0\7\4\1\51\15\4\1\54\2\55\22\4\12\0"+
- "\1\56\13\0\1\4\1\0\10\4\1\57\13\4\41\0"+
- "\1\60\12\0\1\4\1\0\11\4\1\61\12\4\14\0"+
- "\1\62\11\0\1\4\1\0\12\4\1\63\11\4\15\0"+
- "\1\64\10\0\1\4\1\0\13\4\1\65\10\4\16\0"+
- "\1\66\7\0\1\4\1\0\14\4\1\67\7\4\1\70"+
- "\2\0\23\70\1\71\1\0\1\4\23\71";
-
- private static int [] zzUnpackTrans() {
- int [] result = new int[1188];
- int offset = 0;
- offset = zzUnpackTrans(ZZ_TRANS_PACKED_0, offset, result);
- return result;
- }
-
- private static int zzUnpackTrans(String packed, int offset, int [] result) {
- int i = 0; /* index in packed string */
- int j = offset; /* index in unpacked array */
- int l = packed.length();
- while (i < l) {
- int count = packed.charAt(i++);
- int value = packed.charAt(i++);
- value--;
- do result[j++] = value; while (--count > 0);
- }
- return j;
- }
-
-
- /* error codes */
- private static final int ZZ_UNKNOWN_ERROR = 0;
- private static final int ZZ_NO_MATCH = 1;
- private static final int ZZ_PUSHBACK_2BIG = 2;
- private static final char[] EMPTY_BUFFER = new char[0];
- private static final int YYEOF = -1;
- private static java.io.Reader zzReader = null; // Fake
-
- /* error messages for the codes above */
- private static final String ZZ_ERROR_MSG[] = {
- "Unkown internal scanner error",
- "Error: could not match input",
- "Error: pushback value was too large"
- };
-
- /**
- * ZZ_ATTRIBUTE[aState] contains the attributes of state aState
- */
- private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute();
-
- private static final String ZZ_ATTRIBUTE_PACKED_0 =
- "\15\1\1\0\2\1\3\0\7\1\1\0\1\1\3\0"+
- "\4\1\1\0\1\1\2\0\2\1\1\0\1\1\1\11"+
- "\1\1\1\0\1\1\1\0\1\1\1\0\1\1\1\0"+
- "\1\1\1\0\3\1";
-
- private static int [] zzUnpackAttribute() {
- int [] result = new int[57];
- int offset = 0;
- offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result);
- return result;
- }
-
- private static int zzUnpackAttribute(String packed, int offset, int [] result) {
- int i = 0; /* index in packed string */
- int j = offset; /* index in unpacked array */
- int l = packed.length();
- while (i < l) {
- int count = packed.charAt(i++);
- int value = packed.charAt(i++);
- do result[j++] = value; while (--count > 0);
- }
- return j;
- }
-
- /** the current state of the DFA */
- private int zzState;
-
- /** the current lexical state */
- private int zzLexicalState = YYINITIAL;
-
- /** this buffer contains the current text to be matched and is
- the source of the yytext() string */
- private CharSequence zzBuffer = "";
-
- /** this buffer may contains the current text array to be matched when it is cheap to acquire it */
- private char[] zzBufferArray;
-
- /** the textposition at the last accepting state */
- private int zzMarkedPos;
-
- /** the textposition at the last state to be included in yytext */
- private int zzPushbackPos;
-
- /** the current text position in the buffer */
- private int zzCurrentPos;
-
- /** startRead marks the beginning of the yytext() string in the buffer */
- private int zzStartRead;
-
- /** endRead marks the last character in the buffer, that has been read
- from input */
- private int zzEndRead;
-
- /**
- * zzAtBOL == true <=> the scanner is currently at the beginning of a line
- */
- private boolean zzAtBOL = true;
-
- /** zzAtEOF == true <=> the scanner is at the EOF */
- private boolean zzAtEOF;
-
- /** denotes if the user-EOF-code has already been executed */
- private boolean zzEOFDone;
-
-
- _StoryLexer(java.io.Reader in) {
- this.zzReader = in;
- }
-
- /**
- * Creates a new scanner.
- * There is also java.io.Reader version of this constructor.
- *
- * @param in the java.io.Inputstream to read input from.
- */
- _StoryLexer(java.io.InputStream in) {
- this(new java.io.InputStreamReader(in));
- }
-
- /**
- * Unpacks the compressed character translation table.
- *
- * @param packed the packed character translation table
- * @return the unpacked character translation table
- */
- private static char [] zzUnpackCMap(String packed) {
- char [] map = new char[0x10000];
- int i = 0; /* index in packed string */
- int j = 0; /* index in unpacked array */
- while (i < 82) {
- int count = packed.charAt(i++);
- char value = packed.charAt(i++);
- do map[j++] = value; while (--count > 0);
- }
- return map;
- }
-
- public final int getTokenStart(){
- return zzStartRead;
- }
-
- public final int getTokenEnd(){
- return getTokenStart() + yylength();
- }
-
- public void reset(CharSequence buffer, int start, int end,int initialState){
- zzBuffer = buffer;
- zzBufferArray = com.intellij.util.text.CharArrayUtil.fromSequenceWithoutCopying(buffer);
- zzCurrentPos = zzMarkedPos = zzStartRead = start;
- zzPushbackPos = 0;
- zzAtEOF = false;
- zzAtBOL = true;
- zzEndRead = end;
- yybegin(initialState);
- }
-
- /**
- * Refills the input buffer.
- *
- * @return false, iff there was new input.
- *
- * @exception java.io.IOException if any I/O-Error occurs
- */
- private boolean zzRefill() throws java.io.IOException {
- return true;
- }
-
-
- /**
- * Returns the current lexical state.
- */
- public final int yystate() {
- return zzLexicalState;
- }
-
-
- /**
- * Enters a new lexical state
- *
- * @param newState the new lexical state
- */
- public final void yybegin(int newState) {
- zzLexicalState = newState;
- }
-
-
- /**
- * Returns the text matched by the current regular expression.
- */
- public final CharSequence yytext() {
- return zzBuffer.subSequence(zzStartRead, zzMarkedPos);
- }
-
-
- /**
- * Returns the character at position pos from the
- * matched text.
- *
- * It is equivalent to yytext().charAt(pos), but faster
- *
- * @param pos the position of the character to fetch.
- * A value from 0 to yylength()-1.
- *
- * @return the character at position pos
- */
- public final char yycharat(int pos) {
- return zzBufferArray != null ? zzBufferArray[zzStartRead+pos]:zzBuffer.charAt(zzStartRead+pos);
- }
-
-
- /**
- * Returns the length of the matched text region.
- */
- public final int yylength() {
- return zzMarkedPos-zzStartRead;
- }
-
-
- /**
- * Reports an error that occured while scanning.
- *
- * In a wellformed scanner (no or only correct usage of
- * yypushback(int) and a match-all fallback rule) this method
- * will only be called with things that "Can't Possibly Happen".
- * If this method is called, something is seriously wrong
- * (e.g. a JFlex bug producing a faulty scanner etc.).
- *
- * Usual syntax/scanner level error handling should be done
- * in error fallback rules.
- *
- * @param errorCode the code of the errormessage to display
- */
- private void zzScanError(int errorCode) {
- String message;
- try {
- message = ZZ_ERROR_MSG[errorCode];
- }
- catch (ArrayIndexOutOfBoundsException e) {
- message = ZZ_ERROR_MSG[ZZ_UNKNOWN_ERROR];
- }
-
- throw new Error(message);
- }
-
-
- /**
- * Pushes the specified amount of characters back into the input stream.
- *
- * They will be read again by then next call of the scanning method
- *
- * @param number the number of characters to be read again.
- * This number must not be greater than yylength()!
- */
- public void yypushback(int number) {
- if ( number > yylength() )
- zzScanError(ZZ_PUSHBACK_2BIG);
-
- zzMarkedPos -= number;
- }
-
-
- /**
- * Contains user EOF-code, which will be executed exactly once,
- * when the end of file is reached
- */
- private void zzDoEOF() {
- if (!zzEOFDone) {
- zzEOFDone = true;
-
- }
- }
-
-
- /**
- * Resumes scanning until the next regular expression is matched,
- * the end of input is encountered or an I/O-Error occurs.
- *
- * @return the next token
- * @exception java.io.IOException if any I/O-Error occurs
- */
- public IElementType advance() throws java.io.IOException {
- int zzInput;
- int zzAction;
-
- // cached fields:
- int zzCurrentPosL;
- int zzMarkedPosL;
- int zzEndReadL = zzEndRead;
- CharSequence zzBufferL = zzBuffer;
- char[] zzBufferArrayL = zzBufferArray;
- char [] zzCMapL = ZZ_CMAP;
-
- int [] zzTransL = ZZ_TRANS;
- int [] zzRowMapL = ZZ_ROWMAP;
- int [] zzAttrL = ZZ_ATTRIBUTE;
-
- while (true) {
- zzMarkedPosL = zzMarkedPos;
-
- zzAction = -1;
-
- zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL;
-
- zzState = ZZ_LEXSTATE[zzLexicalState];
-
-
- zzForAction: {
- while (true) {
-
- if (zzCurrentPosL < zzEndReadL)
- zzInput = zzBufferL.charAt(zzCurrentPosL++);
- else if (zzAtEOF) {
- zzInput = YYEOF;
- break zzForAction;
- }
- else {
- // store back cached positions
- zzCurrentPos = zzCurrentPosL;
- zzMarkedPos = zzMarkedPosL;
- boolean eof = zzRefill();
- // get translated positions and possibly new buffer
- zzCurrentPosL = zzCurrentPos;
- zzMarkedPosL = zzMarkedPos;
- zzBufferL = zzBuffer;
- zzEndReadL = zzEndRead;
- if (eof) {
- zzInput = YYEOF;
- break zzForAction;
- }
- else {
- zzInput = zzBufferL.charAt(zzCurrentPosL++);
- }
- }
- int zzNext = zzTransL[ zzRowMapL[zzState] + zzCMapL[zzInput] ];
- if (zzNext == -1) break zzForAction;
- zzState = zzNext;
-
- int zzAttributes = zzAttrL[zzState];
- if ( (zzAttributes & 1) == 1 ) {
- zzAction = zzState;
- zzMarkedPosL = zzCurrentPosL;
- if ( (zzAttributes & 8) == 8 ) break zzForAction;
- }
-
- }
- }
-
- // store back cached position
- zzMarkedPos = zzMarkedPosL;
-
- switch (zzAction < 0 ? zzAction : ZZ_ACTION[zzAction]) {
- case 4:
- { yybegin(IN_SCENARIO); return StoryTokenType.TABLE_ROW;
- }
- case 8: break;
- case 2:
- { yybegin(IN_SCENARIO); return StoryTokenType.STEP_TEXT;
- }
- case 9: break;
- case 1:
- { return StoryTokenType.STORY_DESCRIPTION;
- }
- case 10: break;
- case 5:
- { return StoryTokenType.COMMENT;
- }
- case 11: break;
- case 3:
- { return StoryTokenType.WHITE_SPACE;
- }
- case 12: break;
- case 7:
- { yybegin(IN_SCENARIO); return StoryTokenType.SCENARIO_TEXT;
- }
- case 13: break;
- case 6:
- { yybegin(IN_STEP); return StoryTokenType.STEP_TYPE;
- }
- case 14: break;
- default:
- if (zzInput == YYEOF && zzStartRead == zzCurrentPos) {
- zzAtEOF = true;
- zzDoEOF();
- return null;
- }
- else {
- zzScanError(ZZ_NO_MATCH);
- }
- }
- }
- }
-
-
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/language/StoryFileTypeFactory.java b/src/com/github/kumaraman21/intellijbehave/language/StoryFileTypeFactory.java
deleted file mode 100644
index 964eb5bd..00000000
--- a/src/com/github/kumaraman21/intellijbehave/language/StoryFileTypeFactory.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.language;
-
-import com.intellij.openapi.fileTypes.FileTypeConsumer;
-import com.intellij.openapi.fileTypes.FileTypeFactory;
-import org.jetbrains.annotations.NotNull;
-
-public class StoryFileTypeFactory extends FileTypeFactory {
- @Override
- public void createFileTypes(@NotNull FileTypeConsumer fileTypeConsumer) {
- fileTypeConsumer.consume(StoryFileType.STORY_FILE_TYPE, StoryFileType.STORY_FILE_TYPE.getDefaultExtension());
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/parser/StepPsiElement.java b/src/com/github/kumaraman21/intellijbehave/parser/StepPsiElement.java
deleted file mode 100644
index 2d1fb415..00000000
--- a/src/com/github/kumaraman21/intellijbehave/parser/StepPsiElement.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.parser;
-
-import com.github.kumaraman21.intellijbehave.resolver.StepPsiReference;
-import com.intellij.extapi.psi.ASTWrapperPsiElement;
-import com.intellij.lang.ASTNode;
-import com.intellij.psi.PsiReference;
-import org.jbehave.core.steps.StepType;
-import org.jetbrains.annotations.NotNull;
-
-import static org.apache.commons.lang.StringUtils.*;
-
-public class StepPsiElement extends ASTWrapperPsiElement {
- private StepType stepType;
-
- public StepPsiElement(@NotNull ASTNode node, StepType stepType) {
- super(node);
- this.stepType = stepType;
- }
-
- @Override
- public PsiReference getReference() {
- return new StepPsiReference(this);
- }
-
- public StepType getStepType() {
- return stepType;
- }
-
- public String getStepText() {
- return trim(substringAfter(getText(), " "));
- }
-
- public String getActualStepPrefix() {
- return substringBefore(getText(), " ");
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/parser/StoryElementType.java b/src/com/github/kumaraman21/intellijbehave/parser/StoryElementType.java
deleted file mode 100644
index 3d41685d..00000000
--- a/src/com/github/kumaraman21/intellijbehave/parser/StoryElementType.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.parser;
-
-import com.intellij.psi.tree.IElementType;
-import com.intellij.psi.tree.IFileElementType;
-import com.intellij.psi.tree.TokenSet;
-import org.jetbrains.annotations.NonNls;
-import org.jetbrains.annotations.NotNull;
-
-import static com.github.kumaraman21.intellijbehave.language.StoryFileType.STORY_FILE_TYPE;
-
-public class StoryElementType extends IElementType {
-
- public StoryElementType(@NotNull @NonNls String debugName) {
- super(debugName, STORY_FILE_TYPE.getLanguage());
- }
-
- public static final IFileElementType STORY_FILE = new IFileElementType(STORY_FILE_TYPE.getLanguage());
- public static final StoryElementType STORY = new StoryElementType("STORY");
- public static final StoryElementType SCENARIO = new StoryElementType("SCENARIO");
-
- public static final StoryElementType GIVEN_STEP = new StoryElementType("GIVEN_STEP");
- public static final StoryElementType WHEN_STEP = new StoryElementType("WHEN_STEP");
- public static final StoryElementType THEN_STEP = new StoryElementType("THEN_STEP");
-
- public static final TokenSet STEPS_TOKEN_SET = TokenSet.create(GIVEN_STEP, WHEN_STEP, THEN_STEP);
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/parser/StoryFileImpl.java b/src/com/github/kumaraman21/intellijbehave/parser/StoryFileImpl.java
deleted file mode 100644
index fe5d2d38..00000000
--- a/src/com/github/kumaraman21/intellijbehave/parser/StoryFileImpl.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.parser;
-
-import com.github.kumaraman21.intellijbehave.utility.NodeToPsiElement;
-import com.github.kumaraman21.intellijbehave.utility.NodeToStepPsiElement;
-import com.intellij.extapi.psi.PsiFileBase;
-import com.intellij.lang.ASTNode;
-import com.intellij.openapi.fileTypes.FileType;
-import com.intellij.psi.FileViewProvider;
-import com.intellij.psi.PsiElement;
-import com.intellij.psi.tree.TokenSet;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.List;
-
-import static com.github.kumaraman21.intellijbehave.language.StoryFileType.STORY_FILE_TYPE;
-import static com.google.common.collect.Lists.newArrayList;
-import static com.google.common.collect.Lists.transform;
-import static java.util.Arrays.asList;
-
-public class StoryFileImpl extends PsiFileBase {
-
- public StoryFileImpl(FileViewProvider fileViewProvider) {
- super(fileViewProvider, STORY_FILE_TYPE.getLanguage());
- }
-
- @NotNull
- @Override
- public FileType getFileType() {
- return STORY_FILE_TYPE;
- }
-
- @NotNull
- public List getSteps() {
-
- List stepNodes = newArrayList();
-
- for (PsiElement scenario : getScenarios()) {
- ASTNode[] stepNodesOfScenario = scenario.getNode().getChildren(StoryElementType.STEPS_TOKEN_SET);
- stepNodes.addAll(asList(stepNodesOfScenario));
- }
-
- return transform(stepNodes, new NodeToStepPsiElement());
- }
-
- @NotNull
- public List getScenarios() {
- PsiElement story = getStory();
- if (story == null) {
- return newArrayList();
- }
-
- ASTNode[] scenarioNodes = story.getNode().getChildren(TokenSet.create(StoryElementType.SCENARIO));
- return transform(asList(scenarioNodes), new NodeToPsiElement());
- }
-
- public PsiElement getStory() {
- ASTNode[] storyNodes = this.getNode().getChildren(TokenSet.create(StoryElementType.STORY));
-
- if(storyNodes.length > 0) {
- return storyNodes[0].getPsi();
- }
-
- return null;
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/parser/StoryParser.java b/src/com/github/kumaraman21/intellijbehave/parser/StoryParser.java
deleted file mode 100644
index 93c81cf9..00000000
--- a/src/com/github/kumaraman21/intellijbehave/parser/StoryParser.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.parser;
-
-import com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType;
-import com.intellij.lang.ASTNode;
-import com.intellij.lang.PsiBuilder;
-import com.intellij.lang.PsiParser;
-import com.intellij.psi.tree.IElementType;
-import org.jetbrains.annotations.NotNull;
-
-import static com.github.kumaraman21.intellijbehave.utility.StepTypeMappings.STEP_TEXT_TO_STORY_ELEMENT_TYPE_MAPPING;
-
-public class StoryParser implements PsiParser {
- @NotNull
- @Override
- public ASTNode parse(IElementType root, PsiBuilder builder) {
- final PsiBuilder.Marker rootMarker = builder.mark();
-
- parseStory(builder);
-
- rootMarker.done(root);
- return builder.getTreeBuilt();
- }
-
- private void parseStory(PsiBuilder builder) {
- final PsiBuilder.Marker storyMarker = builder.mark();
- parseStoryDescriptionLinesIfPresent(builder);
- parseScenarios(builder);
- storyMarker.done(StoryElementType.STORY);
- }
-
- private void parseStoryDescriptionLinesIfPresent(PsiBuilder builder) {
- if(builder.getTokenType() == StoryTokenType.STORY_DESCRIPTION) {
- while(builder.getTokenType() == StoryTokenType.STORY_DESCRIPTION) {
- parseStoryDescriptionLine(builder);
- }
- }
- }
-
- private void parseStoryDescriptionLine(PsiBuilder builder) {
- builder.advanceLexer();
- }
-
- private void parseScenarios(PsiBuilder builder) {
- if(builder.getTokenType() == StoryTokenType.SCENARIO_TEXT) {
- while(builder.getTokenType() == StoryTokenType.SCENARIO_TEXT) {
- parseScenario(builder);
- }
- }
- else {
- builder.advanceLexer();
- builder.error("Scenario expected");
- }
- }
-
- private void parseScenario(PsiBuilder builder) {
- final PsiBuilder.Marker stepMarker = builder.mark();
- builder.advanceLexer();
- parseSteps(builder);
- parseStoryDescriptionLinesIfPresent(builder);
- stepMarker.done(StoryElementType.SCENARIO);
- }
-
- private void parseSteps(PsiBuilder builder) {
- parseStoryDescriptionLinesIfPresent(builder);
- if(builder.getTokenType() == StoryTokenType.STEP_TYPE) {
-
- StoryElementType previousStepElementType = null;
- while(builder.getTokenType() == StoryTokenType.STEP_TYPE) {
- previousStepElementType = parseStep(builder, previousStepElementType);
- parseStoryDescriptionLinesIfPresent(builder);
- }
- }
- else {
- builder.error("At least one step expected");
- }
- }
-
- private StoryElementType parseStep(PsiBuilder builder, StoryElementType previousStepElementType) {
- final PsiBuilder.Marker stepMarker = builder.mark();
-
- StoryElementType currentStepElementType;
-
- String stepTypeText = builder.getTokenText().trim().toUpperCase();
- if(stepTypeText.equalsIgnoreCase("And")) {
- currentStepElementType = previousStepElementType;
- }
- else {
- currentStepElementType = STEP_TEXT_TO_STORY_ELEMENT_TYPE_MAPPING.get(stepTypeText);
- }
-
- parseStepType(builder);
- parseStepText(builder);
- parseTableIfPresent(builder);
- stepMarker.done(currentStepElementType);
-
- return currentStepElementType;
- }
-
- private void parseStepType(PsiBuilder builder) {
- builder.advanceLexer();
- }
-
- private void parseStepText(PsiBuilder builder) {
- if(builder.getTokenType() == StoryTokenType.STEP_TEXT) {
- builder.advanceLexer();
- }
- else {
- builder.error("Step text expected");
- }
- }
-
- private void parseTableIfPresent(PsiBuilder builder) {
- if(builder.getTokenType() == StoryTokenType.TABLE_ROW) {
- while(builder.getTokenType() == StoryTokenType.TABLE_ROW) {
- parseTableRow(builder);
- }
- }
- }
-
- private void parseTableRow(PsiBuilder builder) {
- builder.advanceLexer();
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/parser/StoryParserDefinition.java b/src/com/github/kumaraman21/intellijbehave/parser/StoryParserDefinition.java
deleted file mode 100644
index 2227a14c..00000000
--- a/src/com/github/kumaraman21/intellijbehave/parser/StoryParserDefinition.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.parser;
-
-import com.github.kumaraman21.intellijbehave.highlighter.StoryLexer;
-import com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType;
-import com.intellij.extapi.psi.ASTWrapperPsiElement;
-import com.intellij.lang.ASTNode;
-import com.intellij.lang.ParserDefinition;
-import com.intellij.lang.PsiParser;
-import com.intellij.lexer.Lexer;
-import com.intellij.openapi.project.Project;
-import com.intellij.psi.FileViewProvider;
-import com.intellij.psi.PsiElement;
-import com.intellij.psi.PsiFile;
-import com.intellij.psi.tree.IElementType;
-import com.intellij.psi.tree.IFileElementType;
-import com.intellij.psi.tree.TokenSet;
-import org.jetbrains.annotations.NotNull;
-
-import static com.github.kumaraman21.intellijbehave.utility.StepTypeMappings.STORY_ELEMENT_TYPE_TO_STEP_TYPE_MAPPING;
-
-public class StoryParserDefinition implements ParserDefinition {
- @NotNull
- @Override
- public Lexer createLexer(Project project) {
- return new StoryLexer();
- }
-
- @Override
- public PsiParser createParser(Project project) {
- return new StoryParser();
- }
-
- @Override
- public IFileElementType getFileNodeType() {
- return StoryElementType.STORY_FILE;
- }
-
- @NotNull
- @Override
- public TokenSet getWhitespaceTokens() {
- return TokenSet.create(StoryTokenType.WHITE_SPACE);
- }
-
- @NotNull
- @Override
- public TokenSet getCommentTokens() {
- return TokenSet.create(StoryTokenType.COMMENT);
- }
-
- @NotNull
- @Override
- public TokenSet getStringLiteralElements() {
- return TokenSet.EMPTY;
- }
-
- @NotNull
- @Override
- public PsiElement createElement(ASTNode node) {
- final IElementType type = node.getElementType();
- if (type == StoryElementType.GIVEN_STEP || type == StoryElementType.WHEN_STEP || type == StoryElementType.THEN_STEP) {
- return new StepPsiElement(node, STORY_ELEMENT_TYPE_TO_STEP_TYPE_MAPPING.get(type));
- }
-
- return new ASTWrapperPsiElement(node);
- }
-
- @Override
- public PsiFile createFile(FileViewProvider fileViewProvider) {
- return new StoryFileImpl(fileViewProvider);
- }
-
- @Override
- public SpaceRequirements spaceExistanceTypeBetweenTokens(ASTNode left, ASTNode right) {
- return SpaceRequirements.MAY;
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotation.java b/src/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotation.java
deleted file mode 100644
index 635fd4e7..00000000
--- a/src/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotation.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.resolver;
-
-import com.intellij.psi.PsiAnnotation;
-import org.jbehave.core.steps.StepType;
-
-public class StepDefinitionAnnotation {
- private StepType stepType;
- private String annotationText;
- private PsiAnnotation annotation;
-
- public StepDefinitionAnnotation(StepType stepType, String annotationText, PsiAnnotation annotation) {
- this.stepType = stepType;
- this.annotationText = annotationText;
- this.annotation = annotation;
- }
-
- public String getAnnotationText() {
- return annotationText;
- }
-
- public StepType getStepType() {
- return stepType;
- }
-
- public PsiAnnotation getAnnotation() {
- return annotation;
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotationConverter.java b/src/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotationConverter.java
deleted file mode 100644
index d39f3ace..00000000
--- a/src/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotationConverter.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.resolver;
-
-import com.intellij.psi.PsiAnnotation;
-import com.intellij.psi.PsiElement;
-import com.intellij.psi.PsiLiteral;
-import org.jbehave.core.annotations.Alias;
-import org.jbehave.core.annotations.Aliases;
-import org.jbehave.core.steps.StepType;
-
-import java.util.Set;
-
-import static com.github.kumaraman21.intellijbehave.utility.StepTypeMappings.ANNOTATION_TO_STEP_TYPE_MAPPING;
-import static com.google.common.collect.Sets.newHashSet;
-import static org.apache.commons.lang.StringUtils.*;
-
-public class StepDefinitionAnnotationConverter {
-
- public Set convertFrom(PsiAnnotation[] annotations) {
-
- Set stepDefinitionAnnotations = newHashSet();
- StepType stepType = null;
-
- for (PsiAnnotation annotation : annotations) {
- // Given, When, Then
- if(ANNOTATION_TO_STEP_TYPE_MAPPING.keySet().contains(annotation.getQualifiedName())) {
- stepType = ANNOTATION_TO_STEP_TYPE_MAPPING.get(annotation.getQualifiedName());
- String annotationText = getTextFromValue(annotation.getParameterList().getAttributes()[0].getValue());
- stepDefinitionAnnotations.add(new StepDefinitionAnnotation(stepType, annotationText, annotation));
- }
- else if(annotation.getQualifiedName().equals(Alias.class.getName())) {
- String annotationText = getTextFromValue(annotation.getParameterList().getAttributes()[0].getValue());
- stepDefinitionAnnotations.add(new StepDefinitionAnnotation(stepType, annotationText, annotation));
- }
- else if(annotation.getQualifiedName().equals(Aliases.class.getName())) {
-
- PsiElement[] values = annotation.getParameterList().getAttributes()[0].getValue().getChildren();
- for (PsiElement value : values) {
- if(value instanceof PsiLiteral) {
- String annotationText = getTextFromValue(value);
- stepDefinitionAnnotations.add(new StepDefinitionAnnotation(stepType, annotationText, annotation));
- }
- }
- }
- }
-
- return stepDefinitionAnnotations;
- }
-
- private static String getTextFromValue(PsiElement value) {
- return remove(removeStart(removeEnd(value.getText(), "\""), "\""), "\\");
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionIterator.java b/src/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionIterator.java
deleted file mode 100644
index 561c7002..00000000
--- a/src/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionIterator.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.resolver;
-
-import com.intellij.openapi.roots.ContentIterator;
-import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.psi.*;
-import org.jbehave.core.steps.StepType;
-
-import java.util.Set;
-
-import static com.github.kumaraman21.intellijbehave.utility.ProjectUtils.getCurrentProject;
-
-public abstract class StepDefinitionIterator implements ContentIterator {
-
- private final StepDefinitionAnnotationConverter stepDefinitionAnnotationConverter = new StepDefinitionAnnotationConverter();
- private StepType stepType;
-
- public StepDefinitionIterator(StepType stepType) {
- this.stepType = stepType;
- }
-
- @Override
- public boolean processFile(VirtualFile virtualFile) {
-
- PsiFile psiFile = PsiManager.getInstance(getCurrentProject()).findFile(virtualFile);
- if (psiFile instanceof PsiClassOwner) {
- PsiClass[] psiClasses = ((PsiClassOwner)psiFile).getClasses();
-
- for (PsiClass psiClass : psiClasses) {
- PsiMethod[] methods = psiClass.getMethods();
-
- for (PsiMethod method : methods) {
- PsiAnnotation[] annotations = method.getModifierList().getApplicableAnnotations();
- Set stepDefinitionAnnotations = stepDefinitionAnnotationConverter.convertFrom(annotations);
-
- for (StepDefinitionAnnotation stepDefinitionAnnotation : stepDefinitionAnnotations) {
- if(stepDefinitionAnnotation.getStepType().equals(stepType)) {
-
- boolean shouldContinue = processStepDefinition(stepDefinitionAnnotation);
- if(shouldContinue == false) {
- return shouldContinue;
- }
- }
- }
- }
- }
- }
-
- return true;
- }
-
- public abstract boolean processStepDefinition(StepDefinitionAnnotation stepDefinitionAnnotation);
-
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/resolver/StepPsiReference.java b/src/com/github/kumaraman21/intellijbehave/resolver/StepPsiReference.java
deleted file mode 100644
index c7cc8f8d..00000000
--- a/src/com/github/kumaraman21/intellijbehave/resolver/StepPsiReference.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.resolver;
-
-import com.github.kumaraman21.intellijbehave.parser.StepPsiElement;
-import com.intellij.openapi.util.Comparing;
-import com.intellij.openapi.util.TextRange;
-import com.intellij.psi.PsiElement;
-import com.intellij.psi.PsiReference;
-import com.intellij.util.IncorrectOperationException;
-import org.jbehave.core.parsers.RegexPrefixCapturingPatternParser;
-import org.jbehave.core.parsers.StepMatcher;
-import org.jbehave.core.parsers.StepPatternParser;
-import org.jbehave.core.steps.StepType;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.List;
-
-import static com.github.kumaraman21.intellijbehave.utility.ProjectUtils.getProjectFileIndex;
-import static com.google.common.collect.Lists.newArrayList;
-import static org.apache.commons.lang.StringUtils.trim;
-
-public class StepPsiReference implements PsiReference {
-
- private StepPsiElement stepPsiElement;
-
- public StepPsiReference(StepPsiElement stepPsiElement) {
- this.stepPsiElement = stepPsiElement;
- }
-
- @Override
- public PsiElement getElement() {
- return stepPsiElement;
- }
-
- @Override
- public TextRange getRangeInElement() {
- return TextRange.from(0, stepPsiElement.getTextLength());
- }
-
- @Override
- public PsiElement resolve() {
- StepType stepType = stepPsiElement.getStepType();
- String stepText = stepPsiElement.getStepText();
-
- StepAnnotationFinder stepAnnotationFinder = new StepAnnotationFinder(stepType, stepText);
- getProjectFileIndex().iterateContent(stepAnnotationFinder);
-
- return stepAnnotationFinder.getMatchingAnnotation();
- }
-
- @NotNull
- @Override
- public Object[] getVariants() {
- StepType stepType = stepPsiElement.getStepType();
- String actualStepPrefix = stepPsiElement.getActualStepPrefix();
-
- StepSuggester stepSuggester = new StepSuggester(stepType, actualStepPrefix);
- getProjectFileIndex().iterateContent(stepSuggester);
-
- return stepSuggester.getSuggestions().toArray();
- }
-
- private static class StepSuggester extends StepDefinitionIterator {
-
- private List suggestions = newArrayList();
- private String actualStepPrefix;
-
- public StepSuggester(StepType stepType, String actualStepPrefix) {
- super(stepType);
- this.actualStepPrefix = actualStepPrefix;
- }
-
- @Override
- public boolean processStepDefinition(StepDefinitionAnnotation stepDefinitionAnnotation) {
- suggestions.add(actualStepPrefix + " " + stepDefinitionAnnotation.getAnnotationText());
- return true;
- }
-
- public List getSuggestions() {
- return suggestions;
- }
- }
-
-private static class StepAnnotationFinder extends StepDefinitionIterator {
-
- private StepType stepType;
- private String stepText;
- private PsiElement matchingAnnotation;
- private StepPatternParser stepPatternParser = new RegexPrefixCapturingPatternParser();
-
- private StepAnnotationFinder(StepType stepType, String stepText) {
- super(stepType);
- this.stepType = stepType;
- this.stepText = stepText;
- }
-
- @Override
- public boolean processStepDefinition(StepDefinitionAnnotation stepDefinitionAnnotation) {
- StepMatcher stepMatcher = stepPatternParser.parseStep(stepType, stepDefinitionAnnotation.getAnnotationText());
-
- if(stepMatcher.matches(stepText)) {
- matchingAnnotation = stepDefinitionAnnotation.getAnnotation();
-
- return false;
- }
- return true;
- }
-
- public PsiElement getMatchingAnnotation() {
- return matchingAnnotation;
- }
-}
-
- @NotNull
- @Override
- public String getCanonicalText() {
- return trim(stepPsiElement.getText());
- }
-
- @Override
- public PsiElement handleElementRename(String s) throws IncorrectOperationException {
- throw new IncorrectOperationException();
- }
-
- @Override
- public PsiElement bindToElement(@NotNull PsiElement psiElement) throws IncorrectOperationException {
- throw new IncorrectOperationException();
- }
-
- @Override
- public boolean isReferenceTo(PsiElement psiElement) {
- return psiElement instanceof StepPsiElement && Comparing.equal(resolve(), psiElement);
- }
-
- @Override
- public boolean isSoft() {
- return false;
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/resolver/StoryAnnotator.java b/src/com/github/kumaraman21/intellijbehave/resolver/StoryAnnotator.java
deleted file mode 100644
index eb35c6f9..00000000
--- a/src/com/github/kumaraman21/intellijbehave/resolver/StoryAnnotator.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.resolver;
-
-import com.github.kumaraman21.intellijbehave.parser.StepPsiElement;
-import com.intellij.lang.annotation.AnnotationHolder;
-import com.intellij.lang.annotation.Annotator;
-import com.intellij.psi.PsiElement;
-import org.jetbrains.annotations.NotNull;
-
-public class StoryAnnotator implements Annotator {
- @Override
- public void annotate(@NotNull PsiElement psiElement, @NotNull AnnotationHolder annotationHolder) {
- if(! (psiElement instanceof StepPsiElement)) {
- return;
- }
-
- StepPsiElement stepPsiElement = (StepPsiElement) psiElement;
- if(stepPsiElement.getReference().resolve() == null) {
- annotationHolder.createErrorAnnotation(psiElement, "No definition found for the step");
- }
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/runner/RunStoryAction.java b/src/com/github/kumaraman21/intellijbehave/runner/RunStoryAction.java
deleted file mode 100644
index d132843f..00000000
--- a/src/com/github/kumaraman21/intellijbehave/runner/RunStoryAction.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.runner;
-
-import com.github.kumaraman21.intellijbehave.settings.JBehaveSettings;
-import com.intellij.execution.*;
-import com.intellij.execution.application.ApplicationConfiguration;
-import com.intellij.execution.executors.DefaultRunExecutor;
-import com.intellij.execution.impl.RunManagerImpl;
-import com.intellij.execution.impl.RunnerAndConfigurationSettingsImpl;
-import com.intellij.execution.runners.ExecutionEnvironment;
-import com.intellij.execution.runners.ProgramRunner;
-import com.intellij.openapi.actionSystem.AnAction;
-import com.intellij.openapi.actionSystem.AnActionEvent;
-import com.intellij.openapi.actionSystem.PlatformDataKeys;
-import com.intellij.openapi.application.Application;
-import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.module.Module;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.roots.ProjectRootManager;
-import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.psi.JavaPsiFacade;
-import com.intellij.psi.PsiClass;
-import com.intellij.psi.search.GlobalSearchScope;
-
-import static com.github.kumaraman21.intellijbehave.runner.StoryRunnerConfigurationType.JBEHAVE_STORY_RUNNER;
-import static com.intellij.openapi.ui.Messages.getErrorIcon;
-import static com.intellij.openapi.ui.Messages.showMessageDialog;
-import static org.apache.commons.lang.StringUtils.isBlank;
-
-public class RunStoryAction extends AnAction {
-
- public void actionPerformed(AnActionEvent e) {
- Application application = ApplicationManager.getApplication();
- JBehaveSettings component = application.getComponent(JBehaveSettings.class);
-
- String storyRunnerName = component.getStoryRunner();
- if (isBlank(storyRunnerName)) {
- showMessageDialog("In order to run a story file you need to first set a main class in the JBehave settings.",
- "No main class found to run the story",
- getErrorIcon());
- return;
- }
-
- VirtualFile file = e.getData(PlatformDataKeys.VIRTUAL_FILE);
- if (file == null) {
- showMessageDialog("Select a file or focus on the story file in the editor to run it.",
- "Story file not selected",
- getErrorIcon());
- return;
- }
-
- Project project = e.getData(PlatformDataKeys.PROJECT);
- final PsiClass storyRunnerClass = JavaPsiFacade.getInstance(project).findClass(storyRunnerName,
- GlobalSearchScope.allScope(project));
- if(storyRunnerClass == null) {
- showMessageDialog("Could not find the specified main class ''" + storyRunnerName + "'.",
- "Main class not found",
- getErrorIcon());
- return;
- }
-
- Module module = ProjectRootManager.getInstance(project).getFileIndex()
- .getModuleForFile(storyRunnerClass.getContainingFile().getVirtualFile());
- if (module == null) {
- showMessageDialog("Could not find the module in which main class to run stories was defined.'" +
- "/n Resetting the main class in the JBehave settings might fix this issue.",
- "Module not found for main class",
- getErrorIcon());
- return;
- }
-
- RunManagerImpl runManager = (RunManagerImpl)RunManager.getInstance(project);
- RunnerAndConfigurationSettingsImpl runnerAndConfigurationSettings = findConfigurationByName(JBEHAVE_STORY_RUNNER, runManager);
- ApplicationConfiguration conf = null;
-
- if (runnerAndConfigurationSettings != null) {
- conf = (ApplicationConfiguration)runnerAndConfigurationSettings.getConfiguration();
- updateConfiguration(storyRunnerName, file, module, conf);
-
- }
- else {
- StoryRunnerConfigurationType type = application.getComponent(StoryRunnerConfigurationType.class);
- runnerAndConfigurationSettings =
- (RunnerAndConfigurationSettingsImpl)runManager.createRunConfiguration(JBEHAVE_STORY_RUNNER, type.getConfigurationFactories()[0]);
- conf = (ApplicationConfiguration)runnerAndConfigurationSettings.getConfiguration();
- updateConfiguration(storyRunnerName, file, module, conf);
- runManager.addConfiguration(runnerAndConfigurationSettings, true);
- }
-
- runManager.setActiveConfiguration(runnerAndConfigurationSettings);
-
- Executor executor = DefaultRunExecutor.getRunExecutorInstance();
- ProgramRunner runner = RunnerRegistry.getInstance().getRunner(executor.getId(), conf);
- ExecutionEnvironment environment = new ExecutionEnvironment(runner, runnerAndConfigurationSettings, project);
-
- try {
- runner.execute(executor, environment);
- }
- catch (ExecutionException e1) {
- JavaExecutionUtil.showExecutionErrorMessage(e1, "Error", project);
- }
- }
-
- private RunnerAndConfigurationSettingsImpl findConfigurationByName(String name, RunManagerImpl runManager) {
- for (RunnerAndConfigurationSettings settings : runManager.getSortedConfigurations()) {
- if (settings.getName().equals(name)) return (RunnerAndConfigurationSettingsImpl)settings;
- }
- return null;
- }
-
- private void updateConfiguration(String mainClassName, VirtualFile file, Module module, ApplicationConfiguration conf) {
- conf.setMainClassName(mainClassName);
- conf.setProgramParameters(file.getPath());
- conf.setModule(module);
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/settings/JBehaveSettings.java b/src/com/github/kumaraman21/intellijbehave/settings/JBehaveSettings.java
deleted file mode 100644
index 677cbf10..00000000
--- a/src/com/github/kumaraman21/intellijbehave/settings/JBehaveSettings.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.settings;
-
-import com.intellij.openapi.components.PersistentStateComponent;
-import com.intellij.openapi.components.ServiceManager;
-import com.intellij.openapi.components.State;
-import com.intellij.openapi.components.Storage;
-import com.intellij.util.xmlb.XmlSerializerUtil;
-
-@State(
- name = "JBehaveSettings",
- storages = {@Storage(
- id = "main",
- file = "$APP_CONFIG$/jbehave_settings.xml"
- )}
-)
-public class JBehaveSettings implements PersistentStateComponent {
- private String storyRunner;
-
- public static JBehaveSettings getInstance(){
- return ServiceManager.getService(JBehaveSettings.class);
- }
-
- @Override
- public JBehaveSettings getState() {
- return this;
- }
-
- @Override
- public void loadState(JBehaveSettings jBehaveSettings) {
- XmlSerializerUtil.copyBean(jBehaveSettings, this);
- }
-
- public String getStoryRunner() {
- return storyRunner;
- }
-
- public void setStoryRunner(String storyRunner) {
- this.storyRunner = storyRunner;
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/settings/JBehaveSettingsPanel.form b/src/com/github/kumaraman21/intellijbehave/settings/JBehaveSettingsPanel.form
deleted file mode 100644
index 55822b1e..00000000
--- a/src/com/github/kumaraman21/intellijbehave/settings/JBehaveSettingsPanel.form
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
diff --git a/src/com/github/kumaraman21/intellijbehave/settings/JBehaveSettingsPanel.java b/src/com/github/kumaraman21/intellijbehave/settings/JBehaveSettingsPanel.java
deleted file mode 100644
index 27454718..00000000
--- a/src/com/github/kumaraman21/intellijbehave/settings/JBehaveSettingsPanel.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.settings;
-
-import com.intellij.execution.configurations.ConfigurationUtil;
-import com.intellij.ide.DataManager;
-import com.intellij.ide.util.ClassFilter;
-import com.intellij.ide.util.TreeJavaClassChooserDialog;
-import com.intellij.openapi.actionSystem.DataContext;
-import com.intellij.openapi.actionSystem.DataKeys;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.ui.TextFieldWithBrowseButton;
-import com.intellij.psi.JavaPsiFacade;
-import com.intellij.psi.PsiClass;
-import com.intellij.psi.search.GlobalSearchScope;
-import com.intellij.psi.util.PsiMethodUtil;
-
-import javax.swing.*;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-
-public class JBehaveSettingsPanel {
-
- private static final ClassFilter MAIN_CLASS_FILTER = new MainClassFilter();
-
- private JPanel contentPane;
- private JLabel storyRunnerLabel;
- private TextFieldWithBrowseButton storyRunnerField;
-
- private JBehaveSettings jBehaveSettings;
-
- public JBehaveSettingsPanel() {
- jBehaveSettings = JBehaveSettings.getInstance();
- storyRunnerField.addActionListener(new BrowseMainClassListener(storyRunnerField));
- }
-
- public void apply() {
- jBehaveSettings.setStoryRunner(storyRunnerField.getText());
- }
-
- public void reset() {
- storyRunnerField.setText(jBehaveSettings.getStoryRunner());
- }
-
- public boolean isModified() {
- return ! storyRunnerField.getText().equals(jBehaveSettings.getStoryRunner());
- }
-
- private class BrowseMainClassListener implements ActionListener {
- private TextFieldWithBrowseButton textField;
-
- public BrowseMainClassListener(TextFieldWithBrowseButton textField) {
- this.textField = textField;
- }
-
- public void actionPerformed(ActionEvent e) {
- DataContext dataContext = DataManager.getInstance().getDataContext(JBehaveSettingsPanel.this.contentPane);
- Project project = DataKeys.PROJECT.getData(dataContext);
-
- // TODO: display error message if project is null
-
- TreeJavaClassChooserDialog dialog = new TreeJavaClassChooserDialog("Main class for running stories",
- project,
- GlobalSearchScope.allScope(project),
- MAIN_CLASS_FILTER,
- null);
-
- final PsiClass currentClass = JavaPsiFacade.getInstance(project).findClass(textField.getText(),
- GlobalSearchScope.allScope(project));
-
- //TODO: this is not working for some reason
- if (currentClass != null) {
- dialog.select(currentClass);
- }
-
- dialog.show();
-
- if (dialog.getExitCode() == TreeJavaClassChooserDialog.CANCEL_EXIT_CODE) {
- return;
- }
-
- textField.setText(dialog.getSelected().getQualifiedName());
- }
- }
-
- public JPanel getContentPane() {
- return this.contentPane;
- }
-
- private static class MainClassFilter implements ClassFilter {
- @Override
- public boolean isAccepted(final PsiClass aClass) {
- return ConfigurationUtil.MAIN_CLASS.value(aClass) && PsiMethodUtil.findMainMethod(aClass) != null;
- }
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/template/JBehaveTemplateLoaderComponent.java b/src/com/github/kumaraman21/intellijbehave/template/JBehaveTemplateLoaderComponent.java
deleted file mode 100644
index f7639a5b..00000000
--- a/src/com/github/kumaraman21/intellijbehave/template/JBehaveTemplateLoaderComponent.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.template;
-
-import com.intellij.ide.fileTemplates.FileTemplate;
-import com.intellij.ide.fileTemplates.FileTemplateManager;
-import com.intellij.openapi.components.ApplicationComponent;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.IOException;
-import java.io.InputStreamReader;
-
-import static com.github.kumaraman21.intellijbehave.language.StoryFileType.STORY_FILE_TYPE;
-import static com.intellij.openapi.util.io.FileUtil.loadTextAndClose;
-
-public class JBehaveTemplateLoaderComponent implements ApplicationComponent {
- @Override
- public void initComponent() {
- FileTemplate template = FileTemplateManager.getInstance().getTemplate(STORY_FILE_TYPE.getName());
- if (template == null) {
- template = FileTemplateManager.getInstance()
- .addTemplate(STORY_FILE_TYPE.getName(), STORY_FILE_TYPE.getDefaultExtension());
- try {
- template.setText(
- loadTextAndClose(new InputStreamReader(getClass().getResourceAsStream("/fileTemplates/JBehave Story.story.ft"))));
- }
- catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
-
- @Override
- public void disposeComponent() {
- // do nothing
- }
-
- @NotNull
- @Override
- public String getComponentName() {
- return this.getClass().getName();
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/utility/NodeToPsiElement.java b/src/com/github/kumaraman21/intellijbehave/utility/NodeToPsiElement.java
deleted file mode 100644
index 0656648c..00000000
--- a/src/com/github/kumaraman21/intellijbehave/utility/NodeToPsiElement.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.utility;
-
-import com.google.common.base.Function;
-import com.intellij.lang.ASTNode;
-import com.intellij.psi.PsiElement;
-
-public class NodeToPsiElement implements Function {
- @Override
- public PsiElement apply(ASTNode astNode) {
- return astNode.getPsi();
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/utility/NodeToStepPsiElement.java b/src/com/github/kumaraman21/intellijbehave/utility/NodeToStepPsiElement.java
deleted file mode 100644
index 66d4a050..00000000
--- a/src/com/github/kumaraman21/intellijbehave/utility/NodeToStepPsiElement.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.utility;
-
-import com.github.kumaraman21.intellijbehave.parser.StepPsiElement;
-import com.google.common.base.Function;
-import com.intellij.lang.ASTNode;
-
-public class NodeToStepPsiElement implements Function {
- @Override
- public StepPsiElement apply(ASTNode astNode) {
- return (StepPsiElement) astNode.getPsi();
- }
-}
diff --git a/src/com/github/kumaraman21/intellijbehave/utility/StepTypeMappings.java b/src/com/github/kumaraman21/intellijbehave/utility/StepTypeMappings.java
deleted file mode 100644
index c3398a8f..00000000
--- a/src/com/github/kumaraman21/intellijbehave/utility/StepTypeMappings.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright 2011-12 Aman Kumar
- *
- * 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 com.github.kumaraman21.intellijbehave.utility;
-
-import com.github.kumaraman21.intellijbehave.parser.StoryElementType;
-import org.jbehave.core.annotations.Given;
-import org.jbehave.core.annotations.Then;
-import org.jbehave.core.annotations.When;
-import org.jbehave.core.steps.StepType;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public class StepTypeMappings {
-
- public static final Map STEP_TYPE_TO_ANNOTATION_MAPPING = new HashMap();
- static {
- STEP_TYPE_TO_ANNOTATION_MAPPING.put(StepType.GIVEN, Given.class.getName());
- STEP_TYPE_TO_ANNOTATION_MAPPING.put(StepType.WHEN, When.class.getName());
- STEP_TYPE_TO_ANNOTATION_MAPPING.put(StepType.THEN, Then.class.getName());
- }
-
- public static final Map ANNOTATION_TO_STEP_TYPE_MAPPING = new HashMap();
- static {
- ANNOTATION_TO_STEP_TYPE_MAPPING.put(Given.class.getName(), StepType.GIVEN);
- ANNOTATION_TO_STEP_TYPE_MAPPING.put(When.class.getName(), StepType.WHEN);
- ANNOTATION_TO_STEP_TYPE_MAPPING.put(Then.class.getName(), StepType.THEN);
- }
-
- public static final Map STORY_ELEMENT_TYPE_TO_STEP_TYPE_MAPPING = new HashMap();
- static {
- STORY_ELEMENT_TYPE_TO_STEP_TYPE_MAPPING.put(StoryElementType.GIVEN_STEP, StepType.GIVEN);
- STORY_ELEMENT_TYPE_TO_STEP_TYPE_MAPPING.put(StoryElementType.WHEN_STEP, StepType.WHEN);
- STORY_ELEMENT_TYPE_TO_STEP_TYPE_MAPPING.put(StoryElementType.THEN_STEP, StepType.THEN);
- }
-
- public static final Map STEP_TEXT_TO_STORY_ELEMENT_TYPE_MAPPING = new HashMap();
- static {
- STEP_TEXT_TO_STORY_ELEMENT_TYPE_MAPPING.put(StepType.GIVEN.name(), StoryElementType.GIVEN_STEP );
- STEP_TEXT_TO_STORY_ELEMENT_TYPE_MAPPING.put(StepType.WHEN.name(), StoryElementType.WHEN_STEP);
- STEP_TEXT_TO_STORY_ELEMENT_TYPE_MAPPING.put(StepType.THEN.name(), StoryElementType.THEN_STEP);
- }
-}
diff --git a/src/inspectionDescriptions/UndefinedStep.html b/src/inspectionDescriptions/UndefinedStep.html
deleted file mode 100644
index ef7e69d1..00000000
--- a/src/inspectionDescriptions/UndefinedStep.html
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-This inspection reports when a JBehave step has not been defined.
-
-
diff --git a/src/inspectionDescriptions/UnusedStepDeclaration.html b/src/inspectionDescriptions/UnusedStepDeclaration.html
deleted file mode 100644
index 58901ce1..00000000
--- a/src/inspectionDescriptions/UnusedStepDeclaration.html
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-This inspection reports when a JBehave step declaration is
- not used in any of the story files.
-
-
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/codeInspector/UndefinedStepInspection.java b/src/main/java/com/github/kumaraman21/intellijbehave/codeInspector/UndefinedStepInspection.java
new file mode 100644
index 00000000..afcc92ab
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/codeInspector/UndefinedStepInspection.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.codeInspector;
+
+import com.github.kumaraman21.intellijbehave.highlighter.StorySyntaxHighlighter;
+import com.github.kumaraman21.intellijbehave.parser.JBehaveStep;
+import com.github.kumaraman21.intellijbehave.resolver.StepPsiReference;
+import com.github.kumaraman21.intellijbehave.service.JavaStepDefinition;
+import com.github.kumaraman21.intellijbehave.utility.ParametrizedString;
+import com.github.kumaraman21.intellijbehave.utility.ParametrizedString.StringToken;
+import com.intellij.codeInspection.*;
+import com.intellij.codeInspection.ex.ProblemDescriptorImpl;
+import com.intellij.openapi.editor.colors.TextAttributesKey;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.PsiReference;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Reports JBehave steps in Story files that have no Java step definition methods.
+ */
+public class UndefinedStepInspection extends LocalInspectionTool {
+
+ @NotNull
+ @Override
+ public String getShortName() {
+ return "UndefinedStep";
+ }
+
+ @NotNull
+ @Override
+ public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
+ return new PsiElementVisitor() {
+
+ @Override
+ public void visitElement(@NotNull PsiElement psiElement) {
+ super.visitElement(psiElement);
+
+ if (!(psiElement instanceof JBehaveStep step)) {
+ return;
+ }
+
+ PsiReference[] references = step.getReferences();
+
+ if (references.length == 1 && references[0] instanceof StepPsiReference reference) {
+ JavaStepDefinition definition = reference.resolveToDefinition();
+
+ if (definition == null) {
+ holder.registerProblem(step, "Step #ref is not defined");
+ } else {
+ highlightParameters(step, definition, holder);
+ }
+ }
+ }
+ };
+ }
+
+
+ private void highlightParameters(JBehaveStep step, JavaStepDefinition javaStepDefinition, ProblemsHolder holder) {
+ String stepText = step.getStepText();
+ String annotationText = javaStepDefinition.getAnnotationTextFor(stepText);
+ //ParametrizedString cannot be created with null content
+ if (annotationText == null) return;
+
+ int offset = step.getStepTextOffset();
+ for (StringToken token : new ParametrizedString(annotationText).tokenize(stepText)) {
+ int length = token.getValue().length();
+ if (token.isIdentifier()) {
+ registerHighlighting(StorySyntaxHighlighter.TABLE_CELL, step, TextRange.from(offset, length), holder);
+ }
+ offset += length;
+ }
+ }
+
+ private static void registerHighlighting(TextAttributesKey attributesKey,
+ JBehaveStep step,
+ TextRange range,
+ ProblemsHolder holder) {
+ final ProblemDescriptor descriptor = new ProblemDescriptorImpl(
+ step, step, "", LocalQuickFix.EMPTY_ARRAY,
+ ProblemHighlightType.INFORMATION, false, range, false, null,
+ holder.isOnTheFly());
+ descriptor.setTextAttributes(attributesKey);
+ holder.registerProblem(descriptor);
+ }
+}
+
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/codeInspector/UnusedStepDeclarationInspection.java b/src/main/java/com/github/kumaraman21/intellijbehave/codeInspector/UnusedStepDeclarationInspection.java
new file mode 100644
index 00000000..1ed360a2
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/codeInspector/UnusedStepDeclarationInspection.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.codeInspector;
+
+import static com.github.kumaraman21.intellijbehave.service.JBehaveUtil.isStepDefinition;
+
+import com.github.kumaraman21.intellijbehave.parser.JBehaveStep;
+import com.github.kumaraman21.intellijbehave.parser.StoryFile;
+import com.github.kumaraman21.intellijbehave.resolver.StepPsiReference;
+import com.github.kumaraman21.intellijbehave.service.JavaStepDefinition;
+import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool;
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ContentIterator;
+import com.intellij.openapi.roots.ProjectRootManager;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.JavaElementVisitor;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiReference;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Reports Java JBehave step definition methods when they are not used in any Story file.
+ */
+public class UnusedStepDeclarationInspection extends AbstractBaseJavaLocalInspectionTool {
+ @NotNull
+ @Override
+ public String getShortName() {
+ return "UnusedStepDeclaration";
+ }
+
+ @NotNull
+ @Override
+ public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
+ var project = holder.getProject();
+ var stepUsageFinder = new StepUsageFinder(project);
+ ProjectRootManager.getInstance(project).getFileIndex().iterateContent(stepUsageFinder);
+ var stepUsages = stepUsageFinder.getStepUsages();
+
+ return new JavaElementVisitor() {
+ @Override
+ public void visitMethod(final @NotNull PsiMethod method) {
+ if (method.getNameIdentifier() == null || !isStepDefinition(method)) {
+ return;
+ }
+
+ for (JBehaveStep step : stepUsages) {
+ PsiReference[] references = step.getReferences();
+
+ if (references.length == 1 && references[0] instanceof StepPsiReference reference) {
+ JavaStepDefinition definition = reference.resolveToDefinition();
+
+ if (definition != null) {
+ PsiMethod annotatedMethod = definition.getAnnotatedMethod();
+ if (annotatedMethod != null && annotatedMethod.isEquivalentTo(method)) {
+ return;
+ }
+ }
+ }
+ }
+
+ holder.registerProblem(method.getNameIdentifier(), "Step #ref is never used");
+ }
+ };
+ }
+
+ private static class StepUsageFinder implements ContentIterator {
+ private final Project project;
+ private final Set stepUsages = new HashSet<>();
+
+ private StepUsageFinder(Project project) {
+ this.project = project;
+ }
+
+ @Override
+ public boolean processFile(VirtualFile virtualFile) {
+ if (virtualFile.isDirectory() || !virtualFile.getFileType().getDefaultExtension().equals("story")) {
+ return true;
+ }
+
+ if (PsiManager.getInstance(project).findFile(virtualFile) instanceof StoryFile storyFile) {
+ stepUsages.addAll(storyFile.getSteps());
+ }
+ return true;
+ }
+
+ public Set getStepUsages() {
+ return stepUsages;
+ }
+ }
+}
diff --git a/src/com/github/kumaraman21/intellijbehave/commenter/StoryCommenter.java b/src/main/java/com/github/kumaraman21/intellijbehave/commenter/StoryCommenter.java
similarity index 98%
rename from src/com/github/kumaraman21/intellijbehave/commenter/StoryCommenter.java
rename to src/main/java/com/github/kumaraman21/intellijbehave/commenter/StoryCommenter.java
index 2a407de7..55642ebd 100644
--- a/src/com/github/kumaraman21/intellijbehave/commenter/StoryCommenter.java
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/commenter/StoryCommenter.java
@@ -20,7 +20,7 @@
public class StoryCommenter implements Commenter {
@Override
public String getLineCommentPrefix() {
- return "!--";
+ return "!-- ";
}
@Override
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/completion/StoryCompletionContributor.java b/src/main/java/com/github/kumaraman21/intellijbehave/completion/StoryCompletionContributor.java
new file mode 100644
index 00000000..452decae
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/completion/StoryCompletionContributor.java
@@ -0,0 +1,181 @@
+package com.github.kumaraman21.intellijbehave.completion;
+
+import org.jbehave.core.i18n.LocalizedKeywords;
+import org.jbehave.core.steps.StepType;
+
+import com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType;
+import com.github.kumaraman21.intellijbehave.parser.JBehaveStep;
+import com.github.kumaraman21.intellijbehave.resolver.StepDefinitionAnnotation;
+import com.github.kumaraman21.intellijbehave.resolver.StepDefinitionIterator;
+import com.github.kumaraman21.intellijbehave.resolver.StepPsiReference;
+import com.github.kumaraman21.intellijbehave.settings.JBehaveSettings;
+import com.github.kumaraman21.intellijbehave.utility.LocalizedStorySupport;
+import com.github.kumaraman21.intellijbehave.utility.ParametrizedString;
+import com.github.kumaraman21.intellijbehave.utility.ScanUtils;
+import com.intellij.codeInsight.completion.CompletionContributor;
+import com.intellij.codeInsight.completion.CompletionParameters;
+import com.intellij.codeInsight.completion.CompletionResultSet;
+import com.intellij.codeInsight.completion.CompletionType;
+import com.intellij.codeInsight.completion.CompletionUtil;
+import com.intellij.codeInsight.completion.PrefixMatcher;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiAnnotation;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.Consumer;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Limitations: see {@link com.github.kumaraman21.intellijbehave.resolver.StepDefinitionAnnotationConverter#convertFrom(PsiAnnotation[])}
+ *
+ * @author @aloyer
+ */
+public class StoryCompletionContributor extends CompletionContributor {
+
+ public StoryCompletionContributor() {
+ }
+
+ @Override
+ public void fillCompletionVariants(CompletionParameters parameters, @NotNull final CompletionResultSet _result) {
+ if (parameters.getCompletionType() == CompletionType.BASIC && JBehaveSettings.getInstance().isStoryAutoCompletion()) {
+ String prefix = CompletionUtil.findReferenceOrAlphanumericPrefix(parameters);
+ CompletionResultSet result = _result.withPrefixMatcher(prefix);
+
+ LocalizedKeywords keywords = lookupLocalizedKeywords(parameters);
+
+ addAllKeywords(result.getPrefixMatcher(), result::addElement, keywords);
+ addAllSteps(parameters,
+ result.getPrefixMatcher(),
+ result::addElement,
+ keywords);
+ }
+ }
+
+ private LocalizedKeywords lookupLocalizedKeywords(CompletionParameters parameters) {
+ String locale = "en";
+ ASTNode localeNode = parameters.getOriginalFile().getNode().findChildByType(StoryTokenType.COMMENT_WITH_LOCALE);
+ if (localeNode != null) {
+ String localeFound = LocalizedStorySupport.checkForLanguageDefinition(localeNode.getText());
+ if (localeFound != null) {
+ locale = localeFound;
+ }
+ }
+ return new LocalizedStorySupport().getKeywords(locale);
+ }
+
+ private static void addAllKeywords(PrefixMatcher prefixMatcher,
+ Consumer consumer,
+ LocalizedKeywords keywords) {
+ addIfMatches(consumer, prefixMatcher, keywords.narrative());
+ addIfMatches(consumer, prefixMatcher, keywords.asA());
+ addIfMatches(consumer, prefixMatcher, keywords.inOrderTo());
+ addIfMatches(consumer, prefixMatcher, keywords.iWantTo());
+ //
+ addIfMatches(consumer, prefixMatcher, keywords.givenStories());
+ addIfMatches(consumer, prefixMatcher, keywords.ignorable());
+ addIfMatches(consumer, prefixMatcher, keywords.scenario());
+ addIfMatches(consumer, prefixMatcher, keywords.examplesTable());
+ //
+ addIfMatches(consumer, prefixMatcher, keywords.given());
+ addIfMatches(consumer, prefixMatcher, keywords.when());
+ addIfMatches(consumer, prefixMatcher, keywords.then());
+ addIfMatches(consumer, prefixMatcher, keywords.and());
+ }
+
+ private static void addIfMatches(Consumer consumer, PrefixMatcher prefixMatchers, String input) {
+ if (prefixMatchers.prefixMatches(input)) {
+ consumer.consume(LookupElementBuilder.create(input));
+ }
+ }
+
+ private static void addAllSteps(CompletionParameters parameters,
+ PrefixMatcher prefixMatcher,
+ Consumer consumer,
+ LocalizedKeywords keywords) {
+ JBehaveStep step = getStepPsiElement(parameters);
+ if (step == null) {
+ return;
+ }
+
+ StepType stepType = step.getStepType();
+ String actualStepPrefix = step.getActualStepPrefix();
+ //
+ String textBeforeCaret = CompletionUtil.findReferenceOrAlphanumericPrefix(parameters);
+
+ // suggest only if at least the actualStepPrefix is complete
+ if (isStepTypeComplete(keywords, textBeforeCaret)) {
+ StepSuggester stepAnnotationFinder = new StepSuggester(prefixMatcher,
+ stepType,
+ actualStepPrefix,
+ textBeforeCaret,
+ consumer,
+ step.getProject());
+ ScanUtils.iterateInContextOf(step, stepAnnotationFinder);
+ }
+ }
+
+ private static boolean isStepTypeComplete(LocalizedKeywords keywords, String input) {
+ return input.startsWith(keywords.given())
+ || input.startsWith(keywords.when())
+ || input.startsWith(keywords.then())
+ || input.startsWith(keywords.and());
+ }
+
+ private static JBehaveStep getStepPsiElement(CompletionParameters parameters) {
+ PsiElement position = parameters.getPosition();
+ PsiElement positionParent = position.getParent();
+ if (positionParent instanceof JBehaveStep step) {
+ return step;
+ } else if (position instanceof StepPsiReference reference) {
+ return reference.getElement();
+ } else if (position instanceof JBehaveStep step) {
+ return step;
+ } else {
+ return null;
+ }
+ }
+
+ private static class StepSuggester extends StepDefinitionIterator {
+
+ private final PrefixMatcher prefixMatcher;
+ private final String actualStepPrefix;
+ private final String textBeforeCaret;
+ private final Consumer consumer;
+
+ private StepSuggester(PrefixMatcher prefixMatcher,
+ StepType stepType,
+ String actualStepPrefix,
+ String textBeforeCaret,
+ Consumer consumer,
+ Project project) {
+ super(stepType, project);
+ this.prefixMatcher = prefixMatcher;
+ this.actualStepPrefix = actualStepPrefix;
+ this.textBeforeCaret = textBeforeCaret;
+ this.consumer = consumer;
+ }
+
+ @Override
+ public boolean processStepDefinition(StepDefinitionAnnotation stepDefinitionAnnotation) {
+ StepType annotationStepType = stepDefinitionAnnotation.stepType();
+ if (annotationStepType != getStepType()) {
+ return true;
+ }
+ String annotationText = stepDefinitionAnnotation.annotationText();
+ String adjustedAnnotationText = actualStepPrefix + " " + annotationText;
+
+ String complete = new ParametrizedString(adjustedAnnotationText).complete(textBeforeCaret);
+ if (StringUtil.isNotEmpty(complete)) {
+ PsiAnnotation matchingAnnotation = stepDefinitionAnnotation.annotation();
+ consumer.consume(LookupElementBuilder.create(matchingAnnotation, textBeforeCaret + complete));
+ } else if (prefixMatcher.prefixMatches(adjustedAnnotationText)) {
+ PsiAnnotation matchingAnnotation = stepDefinitionAnnotation.annotation();
+ consumer.consume(LookupElementBuilder.create(matchingAnnotation, adjustedAnnotationText));
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/com/github/kumaraman21/intellijbehave/creator/CreateStoryAction.java b/src/main/java/com/github/kumaraman21/intellijbehave/creator/CreateStoryAction.java
similarity index 67%
rename from src/com/github/kumaraman21/intellijbehave/creator/CreateStoryAction.java
rename to src/main/java/com/github/kumaraman21/intellijbehave/creator/CreateStoryAction.java
index bd894f1d..89415f5c 100644
--- a/src/com/github/kumaraman21/intellijbehave/creator/CreateStoryAction.java
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/creator/CreateStoryAction.java
@@ -15,9 +15,10 @@
*/
package com.github.kumaraman21.intellijbehave.creator;
+import static com.github.kumaraman21.intellijbehave.language.StoryFileType.STORY_FILE_TYPE;
+
import com.intellij.ide.IdeBundle;
import com.intellij.ide.actions.CreateElementActionBase;
-import com.intellij.ide.fileTemplates.FileTemplate;
import com.intellij.ide.fileTemplates.FileTemplateManager;
import com.intellij.ide.highlighter.HtmlFileType;
import com.intellij.openapi.actionSystem.AnActionEvent;
@@ -28,7 +29,6 @@
import com.intellij.openapi.fileTypes.FileTypes;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
-import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
@@ -36,7 +36,7 @@
import com.intellij.psi.codeStyle.CodeStyleManager;
import org.jetbrains.annotations.NotNull;
-import static com.github.kumaraman21.intellijbehave.language.StoryFileType.STORY_FILE_TYPE;
+import java.util.function.Consumer;
public class CreateStoryAction extends CreateElementActionBase {
@@ -44,33 +44,30 @@ public CreateStoryAction() {
super("Create New Story File", STORY_FILE_TYPE.getDescription(), STORY_FILE_TYPE.getIcon());
}
- @NotNull
@Override
- protected PsiElement[] invokeDialog(Project project, PsiDirectory directory) {
- CreateElementActionBase.MyInputValidator validator = new CreateElementActionBase.MyInputValidator(project, directory);
+ protected void invokeDialog(@NotNull Project project, @NotNull PsiDirectory directory, @NotNull Consumer super PsiElement[]> elementsConsumer) {
+ var validator = new CreateElementActionBase.MyInputValidator(project, directory);
Messages.showInputDialog(project, "Enter a new file name:", "New Story File", Messages.getQuestionIcon(), "", validator);
- return validator.getCreatedElements();
+ elementsConsumer.accept(validator.getCreatedElements());
}
@NotNull
@Override
- protected PsiElement[] create(String newName, PsiDirectory directory) throws Exception {
- final FileTemplate template = FileTemplateManager.getInstance().getTemplate(STORY_FILE_TYPE.getName());
+ protected PsiElement @NotNull [] create(@NotNull String newName, PsiDirectory directory) throws Exception {
+ final var template = FileTemplateManager.getDefaultInstance().getTemplate(STORY_FILE_TYPE.getName());
String fileName = getFileName(newName);
Project project = directory.getProject();
directory.checkCreateFile(fileName);
- PsiFile psiFile = PsiFileFactory.getInstance(project)
- .createFileFromText(fileName, STORY_FILE_TYPE, template.getText());
+ var psiFile = PsiFileFactory.getInstance(project).createFileFromText(fileName, STORY_FILE_TYPE, template.getText());
if (template.isReformatCode()) {
CodeStyleManager.getInstance(project).reformat(psiFile);
}
psiFile = (PsiFile)directory.add(psiFile);
- final VirtualFile virtualFile = psiFile.getVirtualFile();
- FileEditorManager.getInstance(project).openFile(virtualFile, true);
+ FileEditorManager.getInstance(project).openFile(psiFile.getVirtualFile(), true);
return new PsiElement[]{psiFile};
}
@@ -80,28 +77,23 @@ protected String getErrorTitle() {
return "Cannot Create Story File";
}
+ @NotNull
@Override
- protected String getCommandName() {
- return "Create Story File";
- }
-
- @Override
- protected String getActionName(PsiDirectory directory, String newName) {
+ protected String getActionName(PsiDirectory directory, @NotNull String newName) {
return IdeBundle.message("progress.creating.file", STORY_FILE_TYPE.getName(), newName, directory.getName());
}
- public void update(final AnActionEvent e) {
+ @Override
+ public void update(final @NotNull AnActionEvent e) {
super.update(e);
Presentation presentation = e.getPresentation();
- final FileTypeManager manager = FileTypeManager.getInstance();
- final FileType fileType = manager.getFileTypeByExtension(HtmlFileType.DOT_DEFAULT_EXTENSION);
+ final FileType fileType = FileTypeManager.getInstance().getFileTypeByExtension(HtmlFileType.DOT_DEFAULT_EXTENSION);
if (fileType == FileTypes.PLAIN_TEXT) {
- presentation.setEnabled(false);
- presentation.setVisible(false);
+ presentation.setEnabledAndVisible(false);
}
}
private String getFileName(String name) {
- return name + "." + STORY_FILE_TYPE.getDefaultExtension();
- }
+ return name.endsWith("." + STORY_FILE_TYPE.getDefaultExtension()) ? name : name + "." + STORY_FILE_TYPE.getDefaultExtension();
+ }
}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/LexicalState.java b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/LexicalState.java
new file mode 100644
index 00000000..9106c6c5
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/LexicalState.java
@@ -0,0 +1,32 @@
+package com.github.kumaraman21.intellijbehave.highlighter;
+
+/**
+ * @author @aloyer
+ */
+public enum LexicalState {
+ YYINITIAL(_StoryLexer.YYINITIAL),
+ IN_DIRECTIVE(_StoryLexer.IN_DIRECTIVE),
+ IN_STORY(_StoryLexer.IN_STORY),
+ IN_SCENARIO(_StoryLexer.IN_SCENARIO),
+ IN_GIVEN(_StoryLexer.IN_GIVEN),
+ IN_WHEN(_StoryLexer.IN_WHEN),
+ IN_THEN(_StoryLexer.IN_THEN),
+ IN_META(_StoryLexer.IN_META),
+ IN_TABLE(_StoryLexer.IN_TABLE),
+ IN_EXAMPLES(_StoryLexer.IN_EXAMPLES);
+
+ private final int lexerId;
+
+ LexicalState(int lexerId) {
+ this.lexerId = lexerId;
+ }
+
+ public static LexicalState fromLexer(int lexerId) {
+ for (LexicalState state : values()) {
+ if (state.lexerId == lexerId) {
+ return state;
+ }
+ }
+ throw new IllegalArgumentException("Unsupported lexer id: " + lexerId);
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/Story.flex b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/Story.flex
new file mode 100644
index 00000000..07dcf3a8
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/Story.flex
@@ -0,0 +1,263 @@
+package com.github.kumaraman21.intellijbehave.highlighter;
+
+import com.intellij.lexer.FlexLexer;
+import com.intellij.psi.tree.IElementType;
+import java.util.Stack;
+
+%%
+
+%{
+ private Stack yystates = new Stack () {{ push(YYINITIAL); }};
+ private int currentStepStart = 0;
+ public boolean trace = false;
+
+ public void yystatePush(int yystate) {
+ if(trace) System.out.println(">>>> PUSH: " + LexicalState.fromLexer(yystate) + " [" + reverseAndMap(yystates) + "]");
+ yybegin(yystate);
+ yystates.push(yystate);
+ }
+
+ private String reverseAndMap(Stack yystates) {
+ StringBuilder builder = new StringBuilder();
+ for(int i=yystates.size()-1; i>=0; i--) {
+ if(builder.length()>0)
+ builder.append(", ");
+ builder.append(LexicalState.fromLexer(yystates.get(i)));
+ }
+ return builder.toString();
+ }
+
+ public void yystatePopNPush(int yystate) {
+ yystatePopNPush(1, yystate);
+ }
+
+ public void yystatePopNPush(int nb, int yystate) {
+ if(trace) System.out.println(">>>> POP'n PUSH : #" + nb + ", " + LexicalState.fromLexer(yystate) + " [" + reverseAndMap(yystates) + "]");
+ for (int i = 0; i < nb; i++) {
+ yystatePop();
+ }
+ yystatePush(yystate);
+ }
+
+ public int yystatePop() {
+ int popped = yystates.pop();
+ if(trace) System.out.println(">>>> POP : " + LexicalState.fromLexer(popped) + " [" + reverseAndMap(yystates) + "]");
+ if(!yystates.isEmpty()) {
+ yybegin(yystates.peek());
+ }// otherwise hopes a push will follow right after
+ return popped;
+ }
+
+ public final int lastIndexOfCrLf(final CharSequence source) {
+ final int length = source.length();
+ boolean foundRfOrRn = false;
+
+ for (int i = length - 1; i >= 0; i--) {
+ final char c = source.charAt(i);
+ if (c == '\r' || c == '\n') {
+ foundRfOrRn = true;
+ } else {
+ if (foundRfOrRn) {
+ return i + 1;
+ }
+ }
+ }
+
+ if (foundRfOrRn) {
+ return 0;
+ } else {
+ return -1;
+ }
+ }
+
+ public void retrieveMultilineText() {
+ yypushback(yytext().length() - lastIndexOfCrLf(yytext()));
+ if(currentStepStart != 0) {
+ zzStartRead = currentStepStart;
+ }
+ }
+
+ public void setStepStart() {
+ if(currentStepStart==0){
+ currentStepStart = getTokenStart();
+ }
+ }
+
+ public boolean checkAhead(char c) {
+
+ if (zzMarkedPos >= zzBuffer.length()) {
+ return false;
+ }
+ return zzBuffer.charAt(zzMarkedPos) == c;
+ }
+%}
+
+%class _StoryLexer
+%implements FlexLexer
+%unicode
+%function advance
+%type IElementType
+
+CRLF = \r|\n|\r\n
+BlankChar = [ \t\f]
+InputChar = [^\r\n]
+WhiteSpace = {CRLF} | {BlankChar}
+NonWhiteSpace = [^ \n\r\t\f]
+TableCellChar = [^\r\n\|]
+NonMetaKey = [^@\r\n]
+AnyKey = {InputChar}|{InputChar}{CRLF}
+KeyWords = "Scenario: " | "Meta:" | "Examples:" | "Given " | "When " | "Then " | "And " | "!--" | "|"
+
+%state IN_DIRECTIVE
+%state IN_STORY
+%state IN_SCENARIO
+%state IN_GIVEN
+%state IN_WHEN
+%state IN_THEN
+%state IN_META
+%state IN_TABLE
+%state IN_EXAMPLES
+%eof{
+ return;
+%eof}
+
+%%
+
+ {
+ ( "Scenario: "
+ | "Meta:"
+ | "Examples:"
+ | "Given " | "When " | "Then " | "And "
+ | "!--"
+ | "|" ) {InputChar}+ { yystatePush(IN_DIRECTIVE); yypushback(yytext().length()); }
+ {CRLF} { yystatePush(IN_STORY); yypushback(yytext().length()); }
+ {InputChar}+ { return StoryTokenType.STORY_DESCRIPTION; }
+}
+
+ {
+ "Scenario: " { yystatePopNPush(2, IN_SCENARIO); return StoryTokenType.SCENARIO_TYPE; }
+ "Meta:" { yystatePopNPush(2, IN_META); return StoryTokenType.META; }
+ "Examples:" { yystatePopNPush(2, IN_EXAMPLES); return StoryTokenType.EXAMPLE_TYPE; }
+ "Given " { yystatePopNPush(2, IN_GIVEN); currentStepStart = 0; return StoryTokenType.GIVEN_TYPE; }
+ "When " { yystatePopNPush(2, IN_WHEN); currentStepStart = 0; return StoryTokenType.WHEN_TYPE; }
+ "Then " { yystatePopNPush(2, IN_THEN); currentStepStart = 0; return StoryTokenType.THEN_TYPE; }
+ "!--" {InputChar}* { yystatePop(); return StoryTokenType.COMMENT; }
+ "|" { yystatePopNPush(1, IN_TABLE); return StoryTokenType.TABLE_DELIM; }
+ {WhiteSpace}+ { return StoryTokenType.WHITE_SPACE; }
+}
+
+ {
+ {CRLF}
+ ( "Scenario: "
+ | "Meta:"
+ | "Examples:"
+ | "Given " | "When " | "Then " | "And "
+ | "!--"
+ | "|" ) { yystatePush(IN_DIRECTIVE); yypushback(yytext().length()); }
+ {InputChar}+ { return StoryTokenType.STORY_DESCRIPTION; }
+ {CRLF} { return StoryTokenType.WHITE_SPACE; }
+}
+
+
+ {
+ {CRLF}
+ ( "Scenario: "
+ | "Meta:"
+ | "Examples:"
+ | "Given " | "When " | "Then " | "And "
+ | "!--"
+ | "|" ) { yystatePush(IN_DIRECTIVE); yypushback(yytext().length()); }
+ {InputChar}+ { return StoryTokenType.SCENARIO_TEXT; }
+ {CRLF} { return StoryTokenType.WHITE_SPACE; }
+}
+
+
+ {
+ "@" {NonWhiteSpace}* { return StoryTokenType.META_KEY; }
+ {CRLF}
+ ( "Scenario: "
+ | "Meta:"
+ | "Examples:"
+ | "Given " | "When " | "Then " | "And "
+ | "!--"
+ | "|" ) { yystatePush(IN_DIRECTIVE); yypushback(yytext().length()); }
+ {NonMetaKey}+ { return StoryTokenType.META_TEXT; }
+ {CRLF} { return StoryTokenType.WHITE_SPACE; }
+}
+
+ {
+ {CRLF}
+ ( "Scenario: "
+ | "Meta:"
+ | "Examples:"
+ | "Given " | "When " | "Then "
+ | "!--"
+ | "|" ) { yystatePush(IN_DIRECTIVE); yypushback(yytext().length()); }
+ "And "{InputChar}+{CRLF}
+ ("And " | "Given " | "When " | "Then "
+ | {InputChar}) { yypushback(yytext().length() - 4); currentStepStart = 0; return StoryTokenType.GIVEN_TYPE; }
+ {InputChar}+{CRLF}
+ ("And " | "Given " | "When " | "Then "
+ | "| "
+ | "") { retrieveMultilineText(); return StoryTokenType.STEP_TEXT; }
+ {InputChar}+{CRLF}{InputChar} { setStepStart(); }
+ {CRLF} { return StoryTokenType.WHITE_SPACE; }
+}
+
+ {
+ {CRLF}
+ ( "Scenario: "
+ | "Meta:"
+ | "Examples:"
+ | "Given " | "When " | "Then "
+ | "!--"
+ | "|" ) { yystatePush(IN_DIRECTIVE); yypushback(yytext().length()); }
+ "And "{InputChar}+{CRLF}
+ ("And " | "Given " | "When " | "Then "
+ | {InputChar}) { yypushback(yytext().length() - 4); currentStepStart = 0; return StoryTokenType.WHEN_TYPE; }
+ {InputChar}+{CRLF}
+ ("And " | "Given " | "When " | "Then "
+ | "| "
+ | "") { retrieveMultilineText(); return StoryTokenType.STEP_TEXT; }
+ {InputChar}+{CRLF}{InputChar} { setStepStart(); }
+ {CRLF} { return StoryTokenType.WHITE_SPACE; }
+}
+
+ {
+ {CRLF}
+ ( "Scenario: "
+ | "Meta:"
+ | "Examples:"
+ | "Given " | "When " | "Then "
+ | "!--"
+ | "|" ) { yystatePush(IN_DIRECTIVE); yypushback(yytext().length()); }
+ "And "{InputChar}+{CRLF}
+ ("And " | "Given " | "When " | "Then "
+ | {InputChar}) { yypushback(yytext().length() - 4); currentStepStart = 0; return StoryTokenType.THEN_TYPE; }
+ {InputChar}+{CRLF}
+ ("And " | "Given " | "When " | "Then "
+ | "| "
+ | "") { retrieveMultilineText(); return StoryTokenType.STEP_TEXT; }
+ {InputChar}+{CRLF}{InputChar} { setStepStart(); }
+ {CRLF} { return StoryTokenType.WHITE_SPACE; }
+}
+
+ {
+ {CRLF}
+ ( "Scenario: "
+ | "Meta:"
+ | "Examples:"
+ | "Given " | "When " | "Then " | "And "
+ | "!--"
+ | "|" ) { yystatePush(IN_DIRECTIVE); yypushback(yytext().length()); }
+ {WhiteSpace} { return StoryTokenType.WHITE_SPACE; }
+}
+
+ {
+ {TableCellChar}+ { return StoryTokenType.TABLE_CELL; }
+ "|" { return StoryTokenType.TABLE_DELIM; }
+ {CRLF} { yystatePop(); yypushback(1); }
+}
+
+. { return StoryTokenType.BAD_CHARACTER; }
+
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StoryColorsAndFontsPage.java b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StoryColorsAndFontsPage.java
new file mode 100644
index 00000000..28642d93
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StoryColorsAndFontsPage.java
@@ -0,0 +1,93 @@
+package com.github.kumaraman21.intellijbehave.highlighter;
+
+import com.intellij.openapi.editor.colors.TextAttributesKey;
+import com.intellij.openapi.fileTypes.SyntaxHighlighter;
+import com.intellij.openapi.options.colors.AttributesDescriptor;
+import com.intellij.openapi.options.colors.ColorDescriptor;
+import com.intellij.openapi.options.colors.ColorSettingsPage;
+
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import javax.swing.*;
+import java.util.Map;
+
+/**
+ * @author @aloyer
+ */
+public class StoryColorsAndFontsPage implements ColorSettingsPage {
+
+ @NotNull
+ public String getDisplayName() {
+ return "JBehave";
+ }
+
+ @Nullable
+ public Icon getIcon() {
+ return null;//IntelliJBehaveIcons.ICON_16x16;
+ }
+
+ @NotNull
+ public AttributesDescriptor[] getAttributeDescriptors() {
+ return ATTRS;
+ }
+
+ private static final AttributesDescriptor[] ATTRS = new AttributesDescriptor[]{
+ new AttributesDescriptor("Story description", StorySyntaxHighlighter.STORY_DESCRIPTION),//
+ new AttributesDescriptor("Scenario keyword", StorySyntaxHighlighter.SCENARIO_TYPE),//
+ new AttributesDescriptor("Scenario text", StorySyntaxHighlighter.SCENARIO_TEXT),//
+ new AttributesDescriptor("Step keyword", StorySyntaxHighlighter.STEP_TYPE),//
+ new AttributesDescriptor("Step text", StorySyntaxHighlighter.STEP_TEXT), //
+ new AttributesDescriptor("Table delimiter", StorySyntaxHighlighter.TABLE_DELIM),
+ new AttributesDescriptor("Table cell", StorySyntaxHighlighter.TABLE_CELL),//
+ new AttributesDescriptor("Meta keyword", StorySyntaxHighlighter.META_TYPE),//
+ new AttributesDescriptor("Meta key", StorySyntaxHighlighter.META_KEY),//
+ new AttributesDescriptor("Meta text", StorySyntaxHighlighter.META_TEXT), //
+ new AttributesDescriptor("Line comment", StorySyntaxHighlighter.LINE_COMMENT),//
+ new AttributesDescriptor("Bad Character", StorySyntaxHighlighter.BAD_CHARACTER)
+ };
+
+ @NotNull
+ public ColorDescriptor[] getColorDescriptors() {
+ return new ColorDescriptor[0];
+ }
+
+ @NotNull
+ public SyntaxHighlighter getHighlighter() {
+ return new StorySyntaxHighlighter();
+ }
+
+ @NonNls
+ @NotNull
+ public String getDemoText() {
+ return "Narrative: \n" + //
+ "In order to play a game\n" + //
+ "As a player\n" + //
+ "I want to be able to create and manage my account\n" + //
+ "\n" + //
+ "Scenario: An unknown user cannot be logged\n" + //
+ "\n" + //
+ "Meta:\n" + //
+ "@author mccallum\n" + //
+ "@skip\n" + //
+ "\n" + //
+ "Given i am the user with nickname: \"weird\"\n" + //
+ "When i try to login using the password \"soweird\"\n" + //
+ "!-- TODO: Then i get an error message of type \"Wrong Credentials\"\n" + //
+ "\n" + //
+ "\n" + //
+ "Scenario: A known user cannot be logged using a wrong password\n" + //
+ "\n" + //
+ "Given the following existing users:\n" + //
+ "| nickname | password |\n" + //
+ "| Travis | PacMan |\n" + //
+ "Given i am the user with nickname: \"Travis\"\n" + //
+ "When i try to login using the password \"McCallum\"\n" + //
+ "Then i get an error message of type \"Wrong Credentials\"";
+ }
+
+ @Nullable
+ public Map getAdditionalHighlightingTagToDescriptorMap() {
+ return null;
+ }
+}
diff --git a/src/com/github/kumaraman21/intellijbehave/highlighter/StoryLexer.java b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLexer.java
similarity index 80%
rename from src/com/github/kumaraman21/intellijbehave/highlighter/StoryLexer.java
rename to src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLexer.java
index 9354d6e9..99d35347 100644
--- a/src/com/github/kumaraman21/intellijbehave/highlighter/StoryLexer.java
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLexer.java
@@ -20,7 +20,11 @@
import java.io.Reader;
public class StoryLexer extends FlexAdapter {
- public StoryLexer() {
- super(new _StoryLexer((Reader)null));
- }
+ public StoryLexer() {
+ super(new _StoryLexer((Reader) null));
+ }
+
+ public LexicalState lexerState() {
+ return LexicalState.fromLexer(getFlex().yystate());
+ }
}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLexerFactory.java b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLexerFactory.java
new file mode 100644
index 00000000..a3d1cf26
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLexerFactory.java
@@ -0,0 +1,18 @@
+package com.github.kumaraman21.intellijbehave.highlighter;
+
+import com.intellij.lexer.Lexer;
+
+/**
+ * @author @aloyer
+ */
+public class StoryLexerFactory {
+ public static final boolean USE_LOCALIZED = true;
+
+ public Lexer createLexer() {
+ if (USE_LOCALIZED) {
+ return new StoryLocalizedLexer();
+ } else {
+ return new StoryLexer();
+ }
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLocalizedLexer.java b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLocalizedLexer.java
new file mode 100644
index 00000000..c82e68a8
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLocalizedLexer.java
@@ -0,0 +1,491 @@
+package com.github.kumaraman21.intellijbehave.highlighter;
+
+import com.github.kumaraman21.intellijbehave.utility.CharTree;
+import com.github.kumaraman21.intellijbehave.utility.JBKeyword;
+import com.github.kumaraman21.intellijbehave.utility.LocalizedStorySupport;
+import com.intellij.lexer.LexerBase;
+import com.intellij.openapi.util.text.Strings;
+import com.intellij.psi.tree.IElementType;
+import org.jbehave.core.i18n.LocalizedKeywords;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author @aloyer
+ */
+public class StoryLocalizedLexer extends LexerBase {
+
+ /**
+ * lexical states
+ */
+ public enum State {
+ YYINITIAL,
+ IN_DISPATCH,
+ IN_STORY,
+ IN_SCENARIO,
+ IN_STEP,
+ IN_TABLE,
+ IN_STEP_TABLE,
+ IN_META,
+ IN_EXAMPLES, IN_OTHER_TABLE
+ }
+
+ private final LocalizedStorySupport kwSupport;
+ //
+ private LocalizedKeywords keywords;
+ private CharTree charTree;
+ private CharSequence buffer = Strings.EMPTY_CHAR_SEQUENCE ;
+ //private int startOffset;
+ private int endOffset;
+ private State state = State.YYINITIAL;
+ private int position;
+ private IElementType tokenType;
+ private int currentTokenStart;
+
+ public StoryLocalizedLexer() {
+ this(new LocalizedStorySupport());
+ }
+
+ public StoryLocalizedLexer(LocalizedStorySupport kwSupport) {
+ this.kwSupport = kwSupport;
+ changeLocale("en");
+ }
+
+ public void changeLocale(String locale) {
+ keywords = kwSupport.getKeywords(locale);
+ charTree = new CharTree('/', null);
+ for (JBKeyword kw : JBKeyword.values()) {
+ String asString = kw.asString(keywords);
+ charTree.push(asString, kw);
+ }
+ }
+
+ @Override
+ public void start(@NotNull CharSequence buffer, int startOffset, int endOffset, int initialState) {
+ this.buffer = buffer;
+ //this.startOffset = startOffset;
+ this.position = startOffset;
+ this.endOffset = endOffset;
+ this.state = State.values()[initialState];
+ this.tokenType = null;
+ }
+
+ @Override
+ public int getState() {
+ advanceIfRequired();
+ return state.ordinal();
+ }
+
+ @Override
+ public IElementType getTokenType() {
+ advanceIfRequired();
+ return tokenType;
+ }
+
+ @Override
+ public int getTokenStart() {
+ advanceIfRequired();
+ return currentTokenStart;
+ }
+
+ @Override
+ public int getTokenEnd() {
+ advanceIfRequired();
+ return position;
+ }
+
+ @NotNull
+ @Override
+ public CharSequence getBufferSequence() {
+ return buffer;
+ }
+
+ @Override
+ public int getBufferEnd() {
+ return endOffset;
+ }
+
+ @Override
+ public void advance() {
+ advanceIfRequired();
+ tokenType = null;
+ }
+
+ private void advanceIfRequired() {
+ if (tokenType == null) {
+ locateToken();
+ }
+ }
+
+ public int getPosition() {
+ return position;
+ }
+
+ private void locateToken() {
+ if (tokenType != null || position >= endOffset) {
+ return;
+ }
+
+ this.currentTokenStart = position;
+
+ if (consume(keywords.ignorable())) {
+ consume(INPUT_CHAR);
+ if (state == State.YYINITIAL) {
+ String locale = LocalizedStorySupport.checkForLanguageDefinition(tokenText());
+ if (locale != null) {
+ changeLocale(locale);
+ tokenType = StoryTokenType.COMMENT_WITH_LOCALE;
+ return;
+ }
+ }
+ tokenType = StoryTokenType.COMMENT;
+ return;
+ }
+
+ switch (state) {
+ case YYINITIAL:
+ case IN_STORY: {
+ CharTree.Entry entry = charTree.lookup(buffer, position);
+ if (entry.hasValue()) {
+ tokenType = tokenType(entry.value);
+ position += entry.length;
+ return;
+ } else if (consume(CRLF)) {
+ tokenType = StoryTokenType.WHITE_SPACE;
+ return;
+ } else {
+ consume(INPUT_CHAR);
+ tokenType = StoryTokenType.STORY_DESCRIPTION;
+ return;
+ }
+ }
+ case IN_DISPATCH: {
+ if (consume(CRLF) || consume(SPACES)) {
+ tokenType = StoryTokenType.WHITE_SPACE;
+ return;
+ }
+ CharTree.Entry entry = charTree.lookup(buffer, position);
+ tokenType = tokenType(entry.value);
+ // Prevent getting stuck in one place
+ if (tokenType == StoryTokenType.BAD_CHARACTER) {
+ position++;
+ } else {
+ position += entry.length;
+ }
+ return;
+ }
+ case IN_SCENARIO: {
+ if (consume(CRLF)) {
+ tokenType = StoryTokenType.WHITE_SPACE;
+ //
+ CharTree.Entry entry = charTree.lookup(buffer, position);
+ if (entry.hasValue()) {
+ switch (entry.value) {
+ case Given:
+ case When:
+ case Then:
+ case And:
+ case Meta:
+ case ExamplesTable:
+ case Narrative:
+ case AsA:
+ case IWantTo:
+ case InOrderTo:
+ case Scenario:
+ state = State.IN_DISPATCH;
+ return;
+ case ExamplesTableHeaderSeparator:
+ case ExamplesTableValueSeparator:
+ state = State.IN_OTHER_TABLE;
+ return;
+ }
+ }
+ return;
+ } else {
+ consume(INPUT_CHAR);
+ tokenType = StoryTokenType.SCENARIO_TEXT;
+ return;
+ }
+ }
+ case IN_META: {
+ CharTree.Entry entry = charTree.lookup(buffer, position);
+ if (entry.hasValue()) {
+ switch (entry.value) {
+ case MetaProperty:
+ position += entry.length;
+ consume(META_PROPERTY_CHARS);
+ tokenType = StoryTokenType.META_KEY;
+ return;
+ default:
+ tokenType = tokenType(entry.value);
+ position += entry.length;
+ return;
+ }
+ } else if (consume(SPACES)) {
+ tokenType = StoryTokenType.WHITE_SPACE;
+ return;
+ } else if (consume(CRLF)) {
+ tokenType = StoryTokenType.WHITE_SPACE;
+
+ //
+ entry = charTree.lookup(buffer, position);
+ if (entry.hasValue()) {
+ switch (entry.value) {
+ case Given:
+ case When:
+ case Then:
+ case And:
+ case Meta:
+ case ExamplesTable:
+ case Narrative:
+ case AsA:
+ case IWantTo:
+ case InOrderTo:
+ case Scenario:
+ state = State.IN_DISPATCH;
+ return;
+ case ExamplesTableHeaderSeparator:
+ case ExamplesTableValueSeparator:
+ state = State.IN_OTHER_TABLE;
+ return;
+
+ }
+ }
+ return;
+ } else {
+ consumeUntil(META_PROPERTY_CHARS, keywords.metaProperty());
+ tokenType = StoryTokenType.META_TEXT;
+ return;
+ }
+ }
+ case IN_STEP: {
+ if (consume(CRLF)) {
+ tokenType = StoryTokenType.WHITE_SPACE;
+
+ //
+ CharTree.Entry entry = charTree.lookup(buffer, position);
+ if (entry.hasValue()) {
+ switch (entry.value) {
+ case Given:
+ case When:
+ case Then:
+ case And:
+ case Meta:
+ case ExamplesTable:
+ case Narrative:
+ case AsA:
+ case IWantTo:
+ case InOrderTo:
+ case Scenario:
+ state = State.IN_DISPATCH;
+ return;
+ case ExamplesTableHeaderSeparator:
+ case ExamplesTableValueSeparator:
+ state = State.IN_STEP_TABLE;
+ return;
+ }
+ }
+ return;
+ } else {
+ consume(INPUT_CHAR);
+ tokenType = StoryTokenType.STEP_TEXT;
+ return;
+ }
+ }
+ case IN_OTHER_TABLE:
+ case IN_STEP_TABLE:
+ case IN_TABLE: {
+ if (consume(CRLF)) {
+ tokenType = StoryTokenType.WHITE_SPACE;
+ //
+ CharTree.Entry entry = charTree.lookup(buffer, position);
+ if (entry.hasValue()) {
+ switch (entry.value) {
+ case Given:
+ case When:
+ case Then:
+ case And:
+ case Meta:
+ case ExamplesTable:
+ state = State.IN_EXAMPLES;
+ break;
+ case Narrative:
+ case AsA:
+ case IWantTo:
+ case InOrderTo:
+ case Scenario:
+ state = State.IN_DISPATCH;
+ return;
+ }
+ }
+ return;
+ } else if (consume(keywords.examplesTableHeaderSeparator())) {
+ tokenType = StoryTokenType.TABLE_DELIM;
+ return;
+ } else if (consume(keywords.examplesTableValueSeparator())) {
+ tokenType = StoryTokenType.TABLE_DELIM;
+ return;
+ } else if (consumeUntil(INPUT_CHAR, keywords.examplesTableHeaderSeparator(), keywords.examplesTableValueSeparator())) {
+ tokenType = StoryTokenType.TABLE_CELL;
+ return;
+ }
+ }
+ case IN_EXAMPLES:
+ if (consume(c -> CRLF.accept(c) || SPACES.accept(c))) {
+ tokenType = StoryTokenType.WHITE_SPACE;
+ } else {
+ CharTree.Entry entry = charTree.lookup(buffer, position);
+ if (!entry.hasValue()) {
+ consume(INPUT_CHAR);
+ tokenType = StoryTokenType.EXAMPLE_TYPE;
+ } else {
+ tokenType = tokenType(entry.value);
+ position += entry.length;
+ }
+ }
+ break;
+ default:
+ throw new UnsupportedOperationException("State: " + state.name());
+ }
+ }
+
+ private CharSequence tokenText() {
+ return buffer.subSequence(this.currentTokenStart, this.position);
+ }
+
+ private boolean matchesAhead(String text) {
+ if (position + text.length() > endOffset) {
+ return false;
+ }
+ for (int i = 0; i < text.length(); i++) {
+ if (text.charAt(i) != buffer.charAt(position + i)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean consume(String data) {
+ if (matchesAhead(data)) {
+ position += data.length();
+ return true;
+ }
+ return false;
+ }
+
+ private boolean consumeUntil(CharFilter filter, String stopWord) {
+ int previousPosition = position;
+ while (position < endOffset && !matchesAhead(stopWord) && filter.accept(buffer.charAt(position))) {
+ position++;
+ }
+ return position != previousPosition;
+ }
+
+ private boolean consumeUntil(CharFilter filter, String stopWord1, String stopWord2) {
+ int previousPosition = position;
+ while (position < endOffset && !(matchesAhead(stopWord1) || matchesAhead(stopWord2))
+ && filter.accept(buffer.charAt(position))) {
+ position++;
+ }
+ return position != previousPosition;
+ }
+
+ /**
+ * Increment the position as long as the filter matches the current character
+ * @return whether the position has changed as a result of a call to this function
+ */
+ private boolean consume(CharFilter filter) {
+ int previousPosition = position;
+ while (position < endOffset && filter.accept(buffer.charAt(position))) {
+ position++;
+ }
+ return position != previousPosition;
+ }
+
+ public State lexerState() {
+ return state;
+ }
+
+ public interface CharFilter {
+ boolean accept(char c);
+ }
+
+ private static CharFilter SPACES = new CharFilter() {
+ @Override
+ public boolean accept(char c) {
+ return c == ' ' || c == '\t';
+ }
+ };
+
+ private static CharFilter CRLF = new CharFilter() {
+ @Override
+ public boolean accept(char c) {
+ return c == '\r' || c == '\n';
+ }
+ };
+
+ private static CharFilter INPUT_CHAR = new CharFilter() {
+ @Override
+ public boolean accept(char c) {
+ return c != '\r' && c != '\n';
+ }
+ };
+
+ private static CharFilter META_PROPERTY_CHARS = new CharFilter() {
+ @Override
+ public boolean accept(char c) {
+ return !SPACES.accept(c) && !CRLF.accept(c);
+ }
+ };
+
+ private IElementType tokenType(JBKeyword value) {
+ if (value == null) {
+ return StoryTokenType.BAD_CHARACTER;
+ }
+ switch (value) {
+ case Given:
+ state = State.IN_STEP;
+ return StoryTokenType.STEP_TYPE_GIVEN;
+ case When:
+ state = State.IN_STEP;
+ return StoryTokenType.STEP_TYPE_WHEN;
+ case Then:
+ state = State.IN_STEP;
+ return StoryTokenType.STEP_TYPE_THEN;
+ case And:
+ state = State.IN_STEP;
+ return StoryTokenType.STEP_TYPE_AND;
+ case Ignorable:
+ case ExamplesTableIgnorableSeparator:
+ return StoryTokenType.COMMENT;
+ case Narrative:
+ case AsA:
+ case InOrderTo:
+ case IWantTo:
+ state = State.IN_STORY;
+ return StoryTokenType.NARRATIVE_TYPE;
+ case ExamplesTable:
+ state = State.IN_EXAMPLES;
+ return StoryTokenType.EXAMPLE_TYPE;
+ case ExamplesTableHeaderSeparator:
+ case ExamplesTableValueSeparator:
+ state = State.IN_TABLE;
+ return StoryTokenType.TABLE_DELIM;
+ case GivenStories:
+ return StoryTokenType.GIVEN_STORIES;
+ case Meta:
+ state = State.IN_META;
+ return StoryTokenType.META;
+ case Scenario:
+ state = State.IN_SCENARIO;
+ return StoryTokenType.SCENARIO_TYPE;
+
+ case MetaProperty:
+ break;
+ case ExamplesTableRow:
+ break;
+ }
+ return StoryTokenType.BAD_CHARACTER;
+ }
+
+
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StorySyntaxHighlighter.java b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StorySyntaxHighlighter.java
new file mode 100644
index 00000000..3f571f95
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StorySyntaxHighlighter.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.highlighter;
+
+import com.intellij.ide.highlighter.JavaHighlightingColors;
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.editor.DefaultLanguageHighlighterColors;
+import com.intellij.openapi.editor.HighlighterColors;
+import com.intellij.openapi.editor.colors.TextAttributesKey;
+import com.intellij.openapi.fileTypes.SyntaxHighlighterBase;
+import com.intellij.psi.tree.IElementType;
+import gnu.trove.THashMap;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Map;
+
+import static com.intellij.openapi.editor.colors.TextAttributesKey.*;
+
+public class StorySyntaxHighlighter extends SyntaxHighlighterBase {
+
+ private static final Map ATTRIBUTES = new THashMap();
+
+ @NotNull
+ @Override
+ public Lexer getHighlightingLexer() {
+ return new StorySyntaxHighlightingLexer();
+ }
+
+ @NotNull
+ @Override
+ public TextAttributesKey[] getTokenHighlights(IElementType tokenType) {
+ return pack(ATTRIBUTES.get(tokenType));
+ }
+
+ @NonNls
+ public static final String STORY_DESCRIPTION_ID = "JBEHAVE.STORY_DESCRIPTION";
+ @NonNls
+ public static final String SCENARIO_TYPE_ID = "JBEHAVE.SCENARIO_TYPE";
+ @NonNls
+ public static final String SCENARIO_TEXT_ID = "JBEHAVE.SCENARIO_TEXT";
+ @NonNls
+ public static final String STEP_TYPE_ID = "JBEHAVE.STEP_TYPE";
+ @NonNls
+ public static final String STEP_TEXT_ID = "JBEHAVE.STEP_TEXT";
+ @NonNls
+ public static final String TABLE_DELIM_ID = "JBEHAVE.TABLE_DELIM";
+ @NonNls
+ public static final String TABLE_CELL_ID = "JBEHAVE.TABLE_CELL";
+ @NonNls
+ public static final String META_TYPE_ID = "JBEHAVE.META_TYPE";
+ @NonNls
+ public static final String META_KEY_ID = "JBEHAVE.META_KEY";
+ @NonNls
+ public static final String META_TEXT_ID = "JBEHAVE.META_TEXT";
+ @NonNls
+ public static final String LINE_COMMENT_ID = "JBEHAVE.COMMENT";
+ @NonNls
+ public static final String BAD_CHARACTER_ID = "JBEHAVE.BAD_CHARACTER";
+
+ // Registering TextAttributes
+ static {
+ createKey(STORY_DESCRIPTION_ID, DefaultLanguageHighlighterColors.NUMBER);
+ createKey(SCENARIO_TYPE_ID, JavaHighlightingColors.STATIC_FIELD_ATTRIBUTES);
+ createKey(SCENARIO_TEXT_ID, JavaHighlightingColors.STATIC_FIELD_ATTRIBUTES);
+ createKey(STEP_TYPE_ID, DefaultLanguageHighlighterColors.KEYWORD);
+ createKey(STEP_TEXT_ID, HighlighterColors.TEXT);
+ createKey(TABLE_DELIM_ID, DefaultLanguageHighlighterColors.BRACES);
+ createKey(TABLE_CELL_ID, DefaultLanguageHighlighterColors.STRING);
+ createKey(META_TYPE_ID, DefaultLanguageHighlighterColors.KEYWORD);
+ createKey(META_KEY_ID, DefaultLanguageHighlighterColors.STRING);
+ createKey(META_TEXT_ID, DefaultLanguageHighlighterColors.STRING);
+ createKey(LINE_COMMENT_ID, DefaultLanguageHighlighterColors.LINE_COMMENT);
+ createKey(BAD_CHARACTER_ID, DefaultLanguageHighlighterColors.INVALID_STRING_ESCAPE);
+ }
+
+ public static TextAttributesKey STORY_DESCRIPTION = createTextAttributesKey(STORY_DESCRIPTION_ID);
+ public static TextAttributesKey SCENARIO_TYPE = createTextAttributesKey(SCENARIO_TYPE_ID);
+ public static TextAttributesKey SCENARIO_TEXT = createTextAttributesKey(SCENARIO_TEXT_ID);
+ public static TextAttributesKey STEP_TYPE = createTextAttributesKey(STEP_TYPE_ID);
+ public static TextAttributesKey STEP_TEXT = createTextAttributesKey(STEP_TEXT_ID);
+ public static TextAttributesKey TABLE_DELIM = createTextAttributesKey(TABLE_DELIM_ID);
+ public static TextAttributesKey TABLE_CELL = createTextAttributesKey(TABLE_CELL_ID);
+ public static TextAttributesKey META_TYPE = createTextAttributesKey(META_TYPE_ID);
+ public static TextAttributesKey META_KEY = createTextAttributesKey(META_KEY_ID);
+ public static TextAttributesKey META_TEXT = createTextAttributesKey(META_TEXT_ID);
+ public static TextAttributesKey LINE_COMMENT = createTextAttributesKey(LINE_COMMENT_ID);
+ public static TextAttributesKey BAD_CHARACTER = createTextAttributesKey(BAD_CHARACTER_ID);
+
+ static {
+ ATTRIBUTES.put(StoryTokenType.STORY_DESCRIPTION, STORY_DESCRIPTION);
+ ATTRIBUTES.put(StoryTokenType.NARRATIVE_TYPE, STORY_DESCRIPTION);
+ ATTRIBUTES.put(StoryTokenType.NARRATIVE_TEXT, STORY_DESCRIPTION);
+ ATTRIBUTES.put(StoryTokenType.SCENARIO_TYPE, SCENARIO_TYPE);
+ ATTRIBUTES.put(StoryTokenType.SCENARIO_TEXT, SCENARIO_TEXT);
+ ATTRIBUTES.put(StoryTokenType.GIVEN_TYPE, STEP_TYPE);
+ ATTRIBUTES.put(StoryTokenType.WHEN_TYPE, STEP_TYPE);
+ ATTRIBUTES.put(StoryTokenType.THEN_TYPE, STEP_TYPE);
+ ATTRIBUTES.put(StoryTokenType.STEP_TYPE_GIVEN, STEP_TYPE);
+ ATTRIBUTES.put(StoryTokenType.STEP_TYPE_WHEN, STEP_TYPE);
+ ATTRIBUTES.put(StoryTokenType.STEP_TYPE_THEN, STEP_TYPE);
+ ATTRIBUTES.put(StoryTokenType.STEP_TYPE_AND, STEP_TYPE);
+ ATTRIBUTES.put(StoryTokenType.STEP_TEXT, STEP_TEXT);
+ ATTRIBUTES.put(StoryTokenType.TABLE_DELIM, TABLE_DELIM);
+ ATTRIBUTES.put(StoryTokenType.TABLE_CELL, TABLE_CELL);
+ ATTRIBUTES.put(StoryTokenType.META, META_TYPE);
+ ATTRIBUTES.put(StoryTokenType.META_KEY, META_KEY);
+ ATTRIBUTES.put(StoryTokenType.META_TEXT, META_TEXT);
+ ATTRIBUTES.put(StoryTokenType.COMMENT, LINE_COMMENT);
+ ATTRIBUTES.put(StoryTokenType.COMMENT_WITH_LOCALE, LINE_COMMENT);
+ ATTRIBUTES.put(StoryTokenType.BAD_CHARACTER, BAD_CHARACTER);
+ }
+
+ private static TextAttributesKey createKey(String externalName, TextAttributesKey textAttributesKey) {
+ return createTextAttributesKey(externalName, textAttributesKey);
+ }
+}
diff --git a/src/com/github/kumaraman21/intellijbehave/highlighter/StorySyntaxHighlightingLexer.java b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StorySyntaxHighlightingLexer.java
similarity index 89%
rename from src/com/github/kumaraman21/intellijbehave/highlighter/StorySyntaxHighlightingLexer.java
rename to src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StorySyntaxHighlightingLexer.java
index 0a98ab08..51e11d68 100644
--- a/src/com/github/kumaraman21/intellijbehave/highlighter/StorySyntaxHighlightingLexer.java
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StorySyntaxHighlightingLexer.java
@@ -18,7 +18,7 @@
import com.intellij.lexer.LayeredLexer;
public class StorySyntaxHighlightingLexer extends LayeredLexer {
- public StorySyntaxHighlightingLexer() {
- super(new StoryLexer());
- }
+ public StorySyntaxHighlightingLexer() {
+ super(new StoryLexerFactory().createLexer());
+ }
}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StoryTokenType.java b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StoryTokenType.java
new file mode 100644
index 00000000..c05c07e7
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/StoryTokenType.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.highlighter;
+
+import com.intellij.psi.TokenType;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.tree.TokenSet;
+import org.jetbrains.annotations.NonNls;
+
+import static com.github.kumaraman21.intellijbehave.language.StoryFileType.STORY_FILE_TYPE;
+
+public class StoryTokenType extends IElementType {
+
+ public static final IElementType WHITE_SPACE = TokenType.WHITE_SPACE;
+ public static final IElementType BAD_CHARACTER = TokenType.BAD_CHARACTER;
+
+ public static final IElementType STORY_DESCRIPTION = new StoryTokenType("STORY_DESCRIPTION");
+ public static final IElementType SCENARIO_TYPE = new StoryTokenType("SCENARIO_TYPE");
+ public static final IElementType SCENARIO_TEXT = new StoryTokenType("SCENARIO_TEXT");
+
+ public static final IElementType GIVEN_TYPE = new StoryTokenType("GIVEN_TYPE");
+ public static final IElementType WHEN_TYPE = new StoryTokenType("WHEN_TYPE");
+ public static final IElementType THEN_TYPE = new StoryTokenType("THEN_TYPE");
+ public static final IElementType STEP_TYPE_GIVEN = new StoryTokenType("STEP_TYPE_GIVEN");
+ public static final IElementType STEP_TYPE_WHEN = new StoryTokenType("STEP_TYPE_WHEN");
+ public static final IElementType STEP_TYPE_THEN = new StoryTokenType("STEP_TYPE_THEN");
+ public static final IElementType STEP_TYPE_AND = new StoryTokenType("STEP_TYPE_AND");
+
+ public static final TokenSet STEP_TYPES = TokenSet.create(GIVEN_TYPE, WHEN_TYPE, THEN_TYPE, STEP_TYPE_GIVEN, STEP_TYPE_WHEN, STEP_TYPE_THEN, STEP_TYPE_AND);
+
+ public static final IElementType STEP_TEXT = new StoryTokenType("STEP_TEXT");
+
+ public static final IElementType TABLE_DELIM = new StoryTokenType("TABLE_DELIM");
+ public static final IElementType TABLE_CELL = new StoryTokenType("TABLE_CELL");
+
+ public static final IElementType COMMENT = new StoryTokenType("COMMENT");
+ public static final IElementType COMMENT_WITH_LOCALE = new StoryTokenType("COMMENT_WITH_LOCALE");
+
+ public static final IElementType META = new StoryTokenType("META");
+ public static final IElementType META_KEY = new StoryTokenType("META_KEY");
+ public static final IElementType META_TEXT = new StoryTokenType("META_TEXT");
+ public static final IElementType EXAMPLE_TYPE = new StoryTokenType("EXAMPLE_TYPE");
+ public static final IElementType GIVEN_STORIES = new StoryTokenType("GIVEN_STORIES");
+
+ public static final IElementType NARRATIVE_TYPE = new StoryTokenType("NARRATIVE_TYPE");
+ public static final IElementType NARRATIVE_TEXT = new StoryTokenType("NARRATIVE_TEXT");
+
+ private final String key;
+
+ public StoryTokenType(@NonNls String debugName) {
+ super(debugName, STORY_FILE_TYPE.getLanguage());
+ this.key = debugName;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof StoryTokenType
+ && ((StoryTokenType) other).key.equals(key);
+ }
+
+ @Override
+ public int hashCode() {
+ return key.hashCode();
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/_StoryLexer.java b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/_StoryLexer.java
new file mode 100644
index 00000000..7aa7b9af
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/_StoryLexer.java
@@ -0,0 +1,789 @@
+/* The following code was generated by JFlex 1.4.3 on 1/21/14 11:33 AM */
+
+package com.github.kumaraman21.intellijbehave.highlighter;
+
+import java.util.Stack;
+
+import com.intellij.lexer.FlexLexer;
+import com.intellij.psi.tree.IElementType;
+
+
+/**
+ * This class is a scanner generated by
+ * JFlex 1.4.3
+ * on 1/21/14 11:33 AM from the specification file
+ * Story.flex
+ */
+class _StoryLexer implements FlexLexer {
+ /** initial size of the lookahead buffer */
+ private static final int ZZ_BUFFERSIZE = 16384;
+
+ /** lexical states */
+ public static final int IN_META = 14;
+ public static final int IN_GIVEN = 8;
+ public static final int IN_DIRECTIVE = 2;
+ public static final int IN_THEN = 12;
+ public static final int YYINITIAL = 0;
+ public static final int IN_EXAMPLES = 18;
+ public static final int IN_SCENARIO = 6;
+ public static final int IN_WHEN = 10;
+ public static final int IN_TABLE = 16;
+ public static final int IN_STORY = 4;
+
+ /**
+ * ZZ_LEXSTATE[l] is the state in the DFA for the lexical state l
+ * ZZ_LEXSTATE[l+1] is the state in the DFA for the lexical state l
+ * at the beginning of a line
+ * l is of the form l = 2*k, k a non negative integer
+ */
+ private static final int ZZ_LEXSTATE[] = {
+ 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7,
+ 8, 8, 9, 9
+ };
+
+ /**
+ * Translates characters to character classes
+ */
+ private static final String ZZ_CMAP_PACKED =
+ "\11\0\1\3\1\2\1\0\1\3\1\1\22\0\1\17\1\37\13\0"+
+ "\1\40\14\0\1\16\5\0\1\5\1\35\3\0\1\22\1\0\1\30"+
+ "\5\0\1\20\5\0\1\6\1\34\2\0\1\32\11\0\1\12\1\0"+
+ "\1\7\1\36\1\10\2\0\1\33\1\14\2\0\1\26\1\24\1\11"+
+ "\1\15\1\25\1\0\1\13\1\27\1\21\1\0\1\31\1\0\1\23"+
+ "\3\0\1\4\uff83\0";
+
+ /**
+ * Translates characters to character classes
+ */
+ private static final char [] ZZ_CMAP = zzUnpackCMap(ZZ_CMAP_PACKED);
+
+ /**
+ * Translates DFA states to action switch labels.
+ */
+ private static final int [] ZZ_ACTION = zzUnpackAction();
+
+ private static final String ZZ_ACTION_PACKED_0 =
+ "\12\0\1\1\2\2\10\1\1\3\1\4\1\5\7\3"+
+ "\2\4\1\6\1\3\2\4\3\3\1\7\1\10\1\11"+
+ "\2\12\1\13\1\4\1\14\7\1\7\0\1\14\10\0"+
+ "\2\15\3\0\5\1\6\0\1\16\7\0\5\17\3\0"+
+ "\3\1\13\0\1\15\6\0\2\1\1\0\1\20\2\0"+
+ "\1\21\1\22\10\0\2\1\2\0\1\23\2\0\6\15"+
+ "\2\1\4\0\5\24\5\25\5\26\1\1\16\0\1\27"+
+ "\7\0\1\30";
+
+ private static int [] zzUnpackAction() {
+ int [] result = new int[198];
+ int offset = 0;
+ offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackAction(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+
+ /**
+ * Translates a state to a row index in the transition table
+ */
+ private static final int [] ZZ_ROWMAP = zzUnpackRowMap();
+
+ private static final String ZZ_ROWMAP_PACKED_0 =
+ "\0\0\0\41\0\102\0\143\0\204\0\245\0\306\0\347"+
+ "\0\u0108\0\u0129\0\u014a\0\u016b\0\u018c\0\u01ad\0\u01ce\0\u01ef"+
+ "\0\u0210\0\u0231\0\u0252\0\u0273\0\u0294\0\u018c\0\u02b5\0\u018c"+
+ "\0\u02d6\0\u02f7\0\u0318\0\u0339\0\u035a\0\u037b\0\u039c\0\u03bd"+
+ "\0\u03de\0\u03ff\0\u0420\0\u0441\0\u0462\0\u0483\0\u04a4\0\u04c5"+
+ "\0\u04e6\0\u0507\0\u0528\0\u0549\0\u018c\0\u018c\0\u018c\0\u01ad"+
+ "\0\u056a\0\u058b\0\u05ac\0\u05cd\0\u05ee\0\u060f\0\u0630\0\u0651"+
+ "\0\u0672\0\u0693\0\u06b4\0\u06d5\0\u06f6\0\u0717\0\u018c\0\u0738"+
+ "\0\u0759\0\u077a\0\u079b\0\u07bc\0\u07dd\0\u07fe\0\u0420\0\u081f"+
+ "\0\u0840\0\u0861\0\u0882\0\u08a3\0\u08c4\0\u08e5\0\u0906\0\u0927"+
+ "\0\u0948\0\u0969\0\u098a\0\u09ab\0\u09cc\0\u09ed\0\u0a0e\0\u0a2f"+
+ "\0\u0a50\0\u0a71\0\u0a92\0\u0ab3\0\u0ad4\0\u0af5\0\u0b16\0\u018c"+
+ "\0\u0b37\0\u0b58\0\u0b79\0\u0b9a\0\u0bbb\0\u0bdc\0\u0bfd\0\u0c1e"+
+ "\0\u0c3f\0\u0c60\0\u0c81\0\u0ca2\0\u0cc3\0\u0ce4\0\u0d05\0\u0d26"+
+ "\0\u0d47\0\u0d68\0\u0d89\0\u0daa\0\u0dcb\0\u018c\0\u0dec\0\u0e0d"+
+ "\0\u0e2e\0\u0e4f\0\u0e70\0\u0e91\0\u0eb2\0\u0ed3\0\u0ef4\0\u018c"+
+ "\0\u0f15\0\u0f36\0\u018c\0\u018c\0\u0f57\0\u0f78\0\u0f99\0\u0fba"+
+ "\0\u0b37\0\u0fdb\0\u0ffc\0\u101d\0\u103e\0\u105f\0\u1080\0\u10a1"+
+ "\0\u018c\0\u10c2\0\u10e3\0\u1104\0\u1125\0\u1146\0\u1167\0\u1188"+
+ "\0\u11a9\0\u11ca\0\u11eb\0\u120c\0\u122d\0\u124e\0\u126f\0\u018c"+
+ "\0\u0b37\0\u1290\0\u12b1\0\u12d2\0\u018c\0\u0b37\0\u12f3\0\u1314"+
+ "\0\u1335\0\u018c\0\u0b37\0\u1356\0\u1377\0\u1398\0\u13b9\0\u13da"+
+ "\0\u13fb\0\u141c\0\u143d\0\u145e\0\u147f\0\u14a0\0\u14c1\0\u14e2"+
+ "\0\u1503\0\u1524\0\u1545\0\u1566\0\u1587\0\u018c\0\u15a8\0\u15c9"+
+ "\0\u15ea\0\u160b\0\u162c\0\u164d\0\u166e\0\u018c";
+
+ private static int [] zzUnpackRowMap() {
+ int [] result = new int[198];
+ int offset = 0;
+ offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackRowMap(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int high = packed.charAt(i++) << 16;
+ result[j++] = high | packed.charAt(i++);
+ }
+ return j;
+ }
+
+ /**
+ * The transition table of the DFA
+ */
+ private static final int [] ZZ_TRANS = zzUnpackTrans();
+
+ private static final String ZZ_TRANS_PACKED_0 =
+ "\1\13\1\14\1\15\1\13\1\16\1\13\1\17\11\13"+
+ "\1\20\1\13\1\21\5\13\1\22\1\13\1\23\1\13"+
+ "\1\23\1\24\1\13\1\25\1\13\1\26\3\27\1\30"+
+ "\1\26\1\31\10\26\1\27\1\32\1\26\1\33\5\26"+
+ "\1\34\1\26\1\35\1\26\1\36\2\26\1\37\1\26"+
+ "\1\13\1\40\1\41\36\13\1\42\1\40\1\41\36\42"+
+ "\1\43\1\44\1\45\32\43\1\46\4\43\1\44\1\45"+
+ "\32\43\1\47\4\43\1\44\1\45\32\43\1\50\3\43"+
+ "\1\51\1\40\1\41\2\51\1\52\33\51\1\53\1\54"+
+ "\1\55\1\53\1\56\34\53\1\26\1\40\1\41\1\57"+
+ "\13\26\1\57\21\26\1\13\2\0\36\13\2\0\1\15"+
+ "\77\0\1\60\2\0\36\60\1\13\2\0\4\13\1\61"+
+ "\32\13\2\0\5\13\1\62\31\13\2\0\20\13\1\63"+
+ "\16\13\2\0\11\13\1\64\25\13\2\0\30\13\1\65"+
+ "\6\13\2\0\6\13\1\66\30\13\2\0\35\13\1\67"+
+ "\1\0\3\27\13\0\1\27\30\0\1\70\41\0\1\71"+
+ "\53\0\1\72\31\0\1\73\57\0\1\74\40\0\1\75"+
+ "\45\0\1\76\2\0\1\41\1\0\1\77\1\0\1\100"+
+ "\11\0\1\101\1\0\1\102\5\0\1\103\1\0\1\104"+
+ "\1\0\1\104\1\105\1\0\1\106\5\0\1\77\1\0"+
+ "\1\100\11\0\1\101\1\0\1\102\5\0\1\103\1\0"+
+ "\1\104\1\0\1\104\1\105\1\0\1\106\1\0\1\42"+
+ "\2\0\36\42\1\107\1\110\1\111\36\107\2\0\1\45"+
+ "\1\0\1\77\1\0\1\100\11\0\1\101\1\0\1\102"+
+ "\5\0\1\103\1\0\1\104\1\0\1\104\2\0\1\106"+
+ "\5\0\1\77\1\0\1\100\11\0\1\101\1\0\1\102"+
+ "\5\0\1\103\1\0\1\104\1\0\1\104\2\0\1\106"+
+ "\1\0\1\107\1\110\1\111\6\107\1\112\30\107\1\110"+
+ "\1\111\6\107\1\113\30\107\1\110\1\111\6\107\1\114"+
+ "\27\107\1\51\2\0\2\51\1\0\33\51\1\52\3\0"+
+ "\13\52\1\0\21\52\1\53\2\0\1\53\1\0\34\53"+
+ "\2\0\1\55\36\0\1\13\2\0\5\13\1\115\31\13"+
+ "\2\0\16\13\1\116\20\13\2\0\7\13\1\117\27\13"+
+ "\2\0\26\13\1\65\10\13\2\0\5\13\1\120\31\13"+
+ "\2\0\33\13\1\121\3\13\2\0\35\13\1\16\10\0"+
+ "\1\122\51\0\1\123\31\0\1\124\57\0\1\125\17\0"+
+ "\1\126\40\0\1\127\70\0\1\130\7\0\1\131\41\0"+
+ "\1\132\53\0\1\133\31\0\1\134\57\0\1\135\16\0"+
+ "\1\136\67\0\1\137\1\140\1\0\1\111\1\140\1\141"+
+ "\23\140\1\142\1\140\1\143\1\140\1\143\1\144\4\140"+
+ "\2\0\1\140\1\141\23\140\1\142\1\140\1\143\1\140"+
+ "\1\143\1\144\3\140\1\107\1\110\1\111\33\107\1\145"+
+ "\3\107\1\110\1\111\33\107\1\146\3\107\1\110\1\111"+
+ "\33\107\1\147\2\107\1\13\2\0\6\13\1\150\30\13"+
+ "\2\0\7\13\1\151\27\13\2\0\21\13\1\152\15\13"+
+ "\2\0\6\13\1\121\30\13\2\0\14\13\1\16\21\13"+
+ "\11\0\1\153\41\0\1\154\52\0\1\155\24\0\1\156"+
+ "\41\0\1\157\40\0\1\160\27\0\1\130\2\0\36\130"+
+ "\10\0\1\161\51\0\1\162\31\0\1\163\57\0\1\135"+
+ "\17\0\1\164\66\0\1\165\42\0\1\77\17\0\1\166"+
+ "\35\0\1\167\57\0\1\170\16\0\1\171\27\0\1\107"+
+ "\1\110\1\111\14\107\1\172\22\107\1\110\1\111\14\107"+
+ "\1\173\22\107\1\110\1\111\14\107\1\174\21\107\1\13"+
+ "\2\0\7\13\1\175\27\13\2\0\13\13\1\16\23\13"+
+ "\2\0\22\13\1\176\13\13\12\0\1\177\44\0\1\200"+
+ "\47\0\1\201\24\0\1\202\46\0\1\203\40\0\1\204"+
+ "\32\0\1\205\41\0\1\206\52\0\1\207\25\0\1\165"+
+ "\46\0\1\77\52\0\1\170\17\0\1\210\66\0\1\211"+
+ "\2\0\1\212\1\110\1\111\36\212\1\213\1\110\1\111"+
+ "\36\213\1\214\1\110\1\111\36\214\1\13\2\0\10\13"+
+ "\1\215\26\13\2\0\23\13\1\216\12\13\13\0\1\217"+
+ "\53\0\1\220\31\0\1\221\33\0\1\222\44\0\1\77"+
+ "\47\0\1\223\24\0\1\211\27\0\1\212\1\224\1\225"+
+ "\36\212\1\213\1\226\1\227\36\213\1\214\1\230\1\231"+
+ "\36\214\1\13\2\0\11\13\1\232\25\13\2\0\5\13"+
+ "\1\233\30\13\14\0\1\234\34\0\1\235\43\0\1\236"+
+ "\53\0\1\237\12\0\1\240\1\0\1\225\1\240\1\241"+
+ "\23\240\1\242\1\240\1\243\1\240\1\243\1\244\4\240"+
+ "\2\0\1\240\1\241\23\240\1\242\1\240\1\243\1\240"+
+ "\1\243\1\244\3\240\1\245\1\0\1\227\1\245\1\246"+
+ "\23\245\1\247\1\245\1\250\1\245\1\250\1\251\4\245"+
+ "\2\0\1\245\1\246\23\245\1\247\1\245\1\250\1\245"+
+ "\1\250\1\251\3\245\1\252\1\0\1\231\1\252\1\253"+
+ "\23\252\1\254\1\252\1\255\1\252\1\255\1\256\4\252"+
+ "\2\0\1\252\1\253\23\252\1\254\1\252\1\255\1\252"+
+ "\1\255\1\256\3\252\1\13\2\0\12\13\1\257\24\13"+
+ "\2\0\24\13\1\151\11\13\15\0\1\260\52\0\1\261"+
+ "\25\0\1\262\34\0\1\263\44\0\1\264\57\0\1\265"+
+ "\16\0\1\266\43\0\1\267\57\0\1\270\16\0\1\271"+
+ "\43\0\1\272\57\0\1\273\16\0\1\274\27\0\1\13"+
+ "\2\0\13\13\1\121\22\13\16\0\1\275\40\0\1\276"+
+ "\37\0\1\277\52\0\1\206\42\0\1\265\17\0\1\300"+
+ "\66\0\1\301\33\0\1\270\17\0\1\302\66\0\1\303"+
+ "\33\0\1\273\17\0\1\304\66\0\1\305\21\0\1\306"+
+ "\37\0\1\165\33\0\1\301\46\0\1\240\32\0\1\303"+
+ "\46\0\1\245\32\0\1\305\46\0\1\252\21\0";
+
+ private static int [] zzUnpackTrans() {
+ int [] result = new int[5775];
+ int offset = 0;
+ offset = zzUnpackTrans(ZZ_TRANS_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackTrans(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ value--;
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+
+ /* error codes */
+ private static final int ZZ_UNKNOWN_ERROR = 0;
+ private static final int ZZ_NO_MATCH = 1;
+ private static final int ZZ_PUSHBACK_2BIG = 2;
+ private static final char[] EMPTY_BUFFER = new char[0];
+ private static final int YYEOF = -1;
+ private static java.io.Reader zzReader = null; // Fake
+
+ /* error messages for the codes above */
+ private static final String ZZ_ERROR_MSG[] = {
+ "Unkown internal scanner error",
+ "Error: could not match input",
+ "Error: pushback value was too large"
+ };
+
+ /**
+ * ZZ_ATTRIBUTE[aState] contains the attributes of state aState
+ */
+ private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute();
+
+ private static final String ZZ_ATTRIBUTE_PACKED_0 =
+ "\12\0\2\1\1\11\10\1\1\11\1\1\1\11\24\1"+
+ "\3\11\10\1\7\0\1\11\10\0\2\1\3\0\5\1"+
+ "\6\0\1\1\7\0\1\11\4\1\3\0\3\1\13\0"+
+ "\1\11\6\0\2\1\1\0\1\11\2\0\2\11\10\0"+
+ "\2\1\2\0\1\11\2\0\10\1\4\0\1\11\4\1"+
+ "\1\11\4\1\1\11\5\1\16\0\1\11\7\0\1\11";
+
+ private static int [] zzUnpackAttribute() {
+ int [] result = new int[198];
+ int offset = 0;
+ offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackAttribute(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+ /** the current state of the DFA */
+ private int zzState;
+
+ /** the current lexical state */
+ private int zzLexicalState = YYINITIAL;
+
+ /** this buffer contains the current text to be matched and is
+ the source of the yytext() string */
+ private CharSequence zzBuffer = "";
+
+ /** this buffer may contains the current text array to be matched when it is cheap to acquire it */
+ private char[] zzBufferArray;
+
+ /** the textposition at the last accepting state */
+ private int zzMarkedPos;
+
+ /** the textposition at the last state to be included in yytext */
+ private int zzPushbackPos;
+
+ /** the current text position in the buffer */
+ private int zzCurrentPos;
+
+ /** startRead marks the beginning of the yytext() string in the buffer */
+ private int zzStartRead;
+
+ /** endRead marks the last character in the buffer, that has been read
+ from input */
+ private int zzEndRead;
+
+ /**
+ * zzAtBOL == true <=> the scanner is currently at the beginning of a line
+ */
+ private boolean zzAtBOL = true;
+
+ /** zzAtEOF == true <=> the scanner is at the EOF */
+ private boolean zzAtEOF;
+
+ /** denotes if the user-EOF-code has already been executed */
+ private boolean zzEOFDone;
+
+ /* user code: */
+ private Stack yystates = new Stack () {{ push(YYINITIAL); }};
+ private int currentStepStart = 0;
+ public boolean trace = false;
+
+ public void yystatePush(int yystate) {
+ if(trace) System.out.println(">>>> PUSH: " + LexicalState.fromLexer(yystate) + " [" + reverseAndMap(yystates) + "]");
+ yybegin(yystate);
+ yystates.push(yystate);
+ }
+
+ private String reverseAndMap(Stack yystates) {
+ StringBuilder builder = new StringBuilder();
+ for(int i=yystates.size()-1; i>=0; i--) {
+ if(builder.length()>0)
+ builder.append(", ");
+ builder.append(LexicalState.fromLexer(yystates.get(i)));
+ }
+ return builder.toString();
+ }
+
+ public void yystatePopNPush(int yystate) {
+ yystatePopNPush(1, yystate);
+ }
+
+ public void yystatePopNPush(int nb, int yystate) {
+ if(trace) System.out.println(">>>> POP'n PUSH : #" + nb + ", " + LexicalState.fromLexer(yystate) + " [" + reverseAndMap(yystates) + "]");
+ for (int i = 0; i < nb; i++) {
+ yystatePop();
+ }
+ yystatePush(yystate);
+ }
+
+ public int yystatePop() {
+ int popped = yystates.pop();
+ if(trace) System.out.println(">>>> POP : " + LexicalState.fromLexer(popped) + " [" + reverseAndMap(yystates) + "]");
+ if(!yystates.isEmpty()) {
+ yybegin(yystates.peek());
+ }// otherwise hopes a push will follow right after
+ return popped;
+ }
+
+ public final int lastIndexOfCrLf(final CharSequence source) {
+ final int length = source.length();
+ boolean foundRfOrRn = false;
+
+ for (int i = length - 1; i >= 0; i--) {
+ final char c = source.charAt(i);
+ if (c == '\r' || c == '\n') {
+ foundRfOrRn = true;
+ } else {
+ if (foundRfOrRn) {
+ return i + 1;
+ }
+ }
+ }
+
+ if (foundRfOrRn) {
+ return 0;
+ } else {
+ return -1;
+ }
+ }
+
+ public void retrieveMultilineText() {
+ yypushback(yytext().length() - lastIndexOfCrLf(yytext()));
+ if(currentStepStart != 0) {
+ zzStartRead = currentStepStart;
+ }
+ }
+
+ public void setStepStart() {
+ if(currentStepStart==0){
+ currentStepStart = getTokenStart();
+ }
+ }
+
+ public boolean checkAhead(char c) {
+
+ if (zzMarkedPos >= zzBuffer.length()) {
+ return false;
+ }
+ return zzBuffer.charAt(zzMarkedPos) == c;
+ }
+
+
+ _StoryLexer(java.io.Reader in) {
+ this.zzReader = in;
+ }
+
+ /**
+ * Creates a new scanner.
+ * There is also java.io.Reader version of this constructor.
+ *
+ * @param in the java.io.Inputstream to read input from.
+ */
+ _StoryLexer(java.io.InputStream in) {
+ this(new java.io.InputStreamReader(in));
+ }
+
+ /**
+ * Unpacks the compressed character translation table.
+ *
+ * @param packed the packed character translation table
+ * @return the unpacked character translation table
+ */
+ private static char [] zzUnpackCMap(String packed) {
+ char [] map = new char[0x10000];
+ int i = 0; /* index in packed string */
+ int j = 0; /* index in unpacked array */
+ while (i < 106) {
+ int count = packed.charAt(i++);
+ char value = packed.charAt(i++);
+ do map[j++] = value; while (--count > 0);
+ }
+ return map;
+ }
+
+ public final int getTokenStart(){
+ return zzStartRead;
+ }
+
+ public final int getTokenEnd(){
+ return getTokenStart() + yylength();
+ }
+
+ public void reset(CharSequence buffer, int start, int end,int initialState){
+ zzBuffer = buffer;
+ zzBufferArray = com.intellij.util.text.CharArrayUtil.fromSequenceWithoutCopying(buffer);
+ zzCurrentPos = zzMarkedPos = zzStartRead = start;
+ zzPushbackPos = 0;
+ zzAtEOF = false;
+ zzAtBOL = true;
+ zzEndRead = end;
+ yybegin(initialState);
+ }
+
+ /**
+ * Refills the input buffer.
+ *
+ * @return false, iff there was new input.
+ *
+ * @exception java.io.IOException if any I/O-Error occurs
+ */
+ private boolean zzRefill() throws java.io.IOException {
+ return true;
+ }
+
+
+ /**
+ * Returns the current lexical state.
+ */
+ public final int yystate() {
+ return zzLexicalState;
+ }
+
+
+ /**
+ * Enters a new lexical state
+ *
+ * @param newState the new lexical state
+ */
+ public final void yybegin(int newState) {
+ zzLexicalState = newState;
+ }
+
+
+ /**
+ * Returns the text matched by the current regular expression.
+ */
+ public final CharSequence yytext() {
+ return zzBuffer.subSequence(zzStartRead, zzMarkedPos);
+ }
+
+
+ /**
+ * Returns the character at position pos from the
+ * matched text.
+ *
+ * It is equivalent to yytext().charAt(pos), but faster
+ *
+ * @param pos the position of the character to fetch.
+ * A value from 0 to yylength()-1.
+ *
+ * @return the character at position pos
+ */
+ public final char yycharat(int pos) {
+ return zzBufferArray != null ? zzBufferArray[zzStartRead+pos]:zzBuffer.charAt(zzStartRead+pos);
+ }
+
+
+ /**
+ * Returns the length of the matched text region.
+ */
+ public final int yylength() {
+ return zzMarkedPos-zzStartRead;
+ }
+
+
+ /**
+ * Reports an error that occured while scanning.
+ *
+ * In a wellformed scanner (no or only correct usage of
+ * yypushback(int) and a match-all fallback rule) this method
+ * will only be called with things that "Can't Possibly Happen".
+ * If this method is called, something is seriously wrong
+ * (e.g. a JFlex bug producing a faulty scanner etc.).
+ *
+ * Usual syntax/scanner level error handling should be done
+ * in error fallback rules.
+ *
+ * @param errorCode the code of the errormessage to display
+ */
+ private void zzScanError(int errorCode) {
+ String message;
+ try {
+ message = ZZ_ERROR_MSG[errorCode];
+ }
+ catch (ArrayIndexOutOfBoundsException e) {
+ message = ZZ_ERROR_MSG[ZZ_UNKNOWN_ERROR];
+ }
+
+ throw new Error(message);
+ }
+
+
+ /**
+ * Pushes the specified amount of characters back into the input stream.
+ *
+ * They will be read again by then next call of the scanning method
+ *
+ * @param number the number of characters to be read again.
+ * This number must not be greater than yylength()!
+ */
+ public void yypushback(int number) {
+ if ( number > yylength() )
+ zzScanError(ZZ_PUSHBACK_2BIG);
+
+ zzMarkedPos -= number;
+ }
+
+
+ /**
+ * Contains user EOF-code, which will be executed exactly once,
+ * when the end of file is reached
+ */
+ private void zzDoEOF() {
+ if (!zzEOFDone) {
+ zzEOFDone = true;
+ return;
+
+ }
+ }
+
+
+ /**
+ * Resumes scanning until the next regular expression is matched,
+ * the end of input is encountered or an I/O-Error occurs.
+ *
+ * @return the next token
+ * @exception java.io.IOException if any I/O-Error occurs
+ */
+ public IElementType advance() throws java.io.IOException {
+ int zzInput;
+ int zzAction;
+
+ // cached fields:
+ int zzCurrentPosL;
+ int zzMarkedPosL;
+ int zzEndReadL = zzEndRead;
+ CharSequence zzBufferL = zzBuffer;
+ char[] zzBufferArrayL = zzBufferArray;
+ char [] zzCMapL = ZZ_CMAP;
+
+ int [] zzTransL = ZZ_TRANS;
+ int [] zzRowMapL = ZZ_ROWMAP;
+ int [] zzAttrL = ZZ_ATTRIBUTE;
+
+ while (true) {
+ zzMarkedPosL = zzMarkedPos;
+
+ zzAction = -1;
+
+ zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL;
+
+ zzState = ZZ_LEXSTATE[zzLexicalState];
+
+
+ zzForAction: {
+ while (true) {
+
+ if (zzCurrentPosL < zzEndReadL)
+ zzInput = zzBufferL.charAt(zzCurrentPosL++);
+ else if (zzAtEOF) {
+ zzInput = YYEOF;
+ break zzForAction;
+ }
+ else {
+ // store back cached positions
+ zzCurrentPos = zzCurrentPosL;
+ zzMarkedPos = zzMarkedPosL;
+ boolean eof = zzRefill();
+ // get translated positions and possibly new buffer
+ zzCurrentPosL = zzCurrentPos;
+ zzMarkedPosL = zzMarkedPos;
+ zzBufferL = zzBuffer;
+ zzEndReadL = zzEndRead;
+ if (eof) {
+ zzInput = YYEOF;
+ break zzForAction;
+ }
+ else {
+ zzInput = zzBufferL.charAt(zzCurrentPosL++);
+ }
+ }
+ int zzNext = zzTransL[ zzRowMapL[zzState] + zzCMapL[zzInput] ];
+ if (zzNext == -1) break zzForAction;
+ zzState = zzNext;
+
+ int zzAttributes = zzAttrL[zzState];
+ if ( (zzAttributes & 1) == 1 ) {
+ zzAction = zzState;
+ zzMarkedPosL = zzCurrentPosL;
+ if ( (zzAttributes & 8) == 8 ) break zzForAction;
+ }
+
+ }
+ }
+
+ // store back cached position
+ zzMarkedPos = zzMarkedPosL;
+
+ switch (zzAction < 0 ? zzAction : ZZ_ACTION[zzAction]) {
+ case 9:
+ { return StoryTokenType.TABLE_CELL;
+ }
+ case 25: break;
+ case 14:
+ { yystatePop(); return StoryTokenType.COMMENT;
+ }
+ case 26: break;
+ case 17:
+ { yystatePopNPush(2, IN_WHEN); currentStepStart = 0; return StoryTokenType.WHEN_TYPE;
+ }
+ case 27: break;
+ case 8:
+ { return StoryTokenType.META_KEY;
+ }
+ case 28: break;
+ case 2:
+ { yystatePush(IN_STORY); yypushback(yytext().length());
+ }
+ case 29: break;
+ case 18:
+ { yystatePopNPush(2, IN_THEN); currentStepStart = 0; return StoryTokenType.THEN_TYPE;
+ }
+ case 30: break;
+ case 22:
+ { yypushback(yytext().length() - 4); currentStepStart = 0; return StoryTokenType.THEN_TYPE;
+ }
+ case 31: break;
+ case 10:
+ { yystatePop(); yypushback(1);
+ }
+ case 32: break;
+ case 5:
+ { yystatePopNPush(1, IN_TABLE); return StoryTokenType.TABLE_DELIM;
+ }
+ case 33: break;
+ case 20:
+ { yypushback(yytext().length() - 4); currentStepStart = 0; return StoryTokenType.GIVEN_TYPE;
+ }
+ case 34: break;
+ case 12:
+ { yystatePush(IN_DIRECTIVE); yypushback(yytext().length());
+ }
+ case 35: break;
+ case 21:
+ { yypushback(yytext().length() - 4); currentStepStart = 0; return StoryTokenType.WHEN_TYPE;
+ }
+ case 36: break;
+ case 7:
+ { return StoryTokenType.META_TEXT;
+ }
+ case 37: break;
+ case 11:
+ { return StoryTokenType.TABLE_DELIM;
+ }
+ case 38: break;
+ case 19:
+ { yystatePopNPush(2, IN_GIVEN); currentStepStart = 0; return StoryTokenType.GIVEN_TYPE;
+ }
+ case 39: break;
+ case 6:
+ { return StoryTokenType.SCENARIO_TEXT;
+ }
+ case 40: break;
+ case 23:
+ { yystatePopNPush(2, IN_EXAMPLES); return StoryTokenType.EXAMPLE_TYPE;
+ }
+ case 41: break;
+ case 13:
+ { retrieveMultilineText(); return StoryTokenType.STEP_TEXT;
+ }
+ case 42: break;
+ case 1:
+ { return StoryTokenType.STORY_DESCRIPTION;
+ }
+ case 43: break;
+ case 3:
+ { return StoryTokenType.BAD_CHARACTER;
+ }
+ case 44: break;
+ case 16:
+ { yystatePopNPush(2, IN_META); return StoryTokenType.META;
+ }
+ case 45: break;
+ case 24:
+ { yystatePopNPush(2, IN_SCENARIO); return StoryTokenType.SCENARIO_TYPE;
+ }
+ case 46: break;
+ case 15:
+ { setStepStart();
+ }
+ case 47: break;
+ case 4:
+ { return StoryTokenType.WHITE_SPACE;
+ }
+ case 48: break;
+ default:
+ if (zzInput == YYEOF && zzStartRead == zzCurrentPos) {
+ zzAtEOF = true;
+ zzDoEOF();
+ return null;
+ }
+ else {
+ zzScanError(ZZ_NO_MATCH);
+ }
+ }
+ }
+ }
+
+
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/generateLexer.sh b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/generateLexer.sh
new file mode 100755
index 00000000..6fc18b8c
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/highlighter/generateLexer.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+FLEX_DIR=~/Projects/intellij/tools/jflex-1.4.3/bin
+
+${FLEX_DIR}/jflex --skel "${FLEX_DIR}/../../idea-flex.skeleton" Story.flex
+
+cat _StoryLexer.java | sed 's/zzBufferL\[zzCurrentPosL++]/zzBufferL.charAt(zzCurrentPosL\+\+)/' > _StoryLexer.tmp
+
+mv _StoryLexer.tmp _StoryLexer.java
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilder.java b/src/main/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilder.java
new file mode 100644
index 00000000..9df12fbb
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilder.java
@@ -0,0 +1,198 @@
+package com.github.kumaraman21.intellijbehave.jbehave.core.steps;
+
+import static java.util.Arrays.asList;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ *
+ * Builds a set of pattern variants of given pattern input, supporting a custom
+ * directives. Depending on the directives present, one or more resulting
+ * variants are created.
+ *
+ *
+ * Currently supported directives are
+ *
+ *
+ *
+ *
+ *
Pattern
+ *
Result
+ *
+ *
+ *
+ *
..A {x|y} B..
+ *
+ *
+ *
..A x B..
+ *
..A y B..
+ *
+ *
+ *
+ *
+ *
..A {x|y|} B..
+ *
+ *
+ *
..A x B..
+ *
..A y B..
+ *
..A B..
+ *
+ *
+ *
+ *
+ *
..A {x} B..
+ *
+ *
+ *
..A x B..
+ *
+ *
+ *
+ *
+ *
+ * These directives can be used to conveniently create several variants of a
+ * step pattern, without having to repeat it as a whole as one or more aliases.
+ *
+ *
+ * Examples:
+ *
+ *
+ *
+ *
+ * @Then("the result {must |has to |}be $x") public void checkResult(int x)...
+ *
+ *
+ * Would match any of these variants from a story file:
+ *
+ *
Then the result must be 42
+ *
Then the result has to be 42
+ *
Then the result be 42
+ *
+ *
+ *
+ *
+ *
+ * @When("$A {+|plus|is added to} $B") public void add(int A, int B)...
+ *
+ *
+ * Would match any of these variants from a story file:
+ *
+ *
When 42 + 23
+ *
When 42 plus 23
+ *
When 42 is added to 23
+ *
+ *
+ *
+ *
+ *
+ * This is the modified and optimized version of {@link org.jbehave.core.steps.PatternVariantBuilder}.
+ *
+ * @author Daniel Schneller
+ */
+public class PatternVariantBuilder {
+
+ /**
+ * Regular expression that locates patterns to be evaluated in the input
+ * pattern.
+ */
+ private static final Pattern REGEX = Pattern.compile("([^\\n{]*+)(\\{(([^|}]++)(\\|)?+)*+})([^\\n]*+)");
+
+ private final Set variants;
+
+ private final String input;
+
+ /**
+ * Creates a builder and calculates all variants for given input. When there
+ * are no variants found in the input, it will itself be the only result.
+ *
+ * @param input to be evaluated
+ */
+ public PatternVariantBuilder(String input) {
+ this.input = input;
+ this.variants = variantsFor(input);
+ }
+
+ public String getInput() {
+ return input;
+ }
+
+ /**
+ *
+ * Parses the {@link #input} received at construction and generates the
+ * variants. When there are multiple patterns in the input, the method will
+ * recurse on itself to generate the variants for the tailing end after the
+ * first matched pattern.
+ *
+ *
+ * Generated variants are stored in a {@link Set}, so there will never be
+ * any duplicates, even if the input's patterns were to result in such.
+ *
+ */
+ private Set variantsFor(String input) {
+ Matcher m = REGEX.matcher(input);
+
+ if (!m.matches()) {
+ // if the regex does not find any patterns,
+ // simply add the input as is
+ return Collections.singleton(input);
+ }
+
+ // isolate the pattern itself, removing its wrapping {}
+ String patternGroup = m.group(2).replaceAll("[{}]", "");
+
+ // split the pattern into its options and add an empty
+ // string if it ends with a separator
+ List patternParts = new ArrayList<>(asList(patternGroup.split("\\|")));
+ if (patternGroup.endsWith("|")) {
+ patternParts.add("");
+ }
+
+ if (!patternParts.isEmpty()) {
+ // Store current invocation's results
+ Set variants = new HashSet<>(8);
+
+ // isolate the part before the first pattern
+ String head = m.group(1);
+
+ // isolate the remaining part of the input
+ String tail = m.group(6);
+
+ var variantsForTail = variantsFor(tail);
+
+ if (!variantsForTail.isEmpty()) {
+ // Iterate over the current pattern's
+ // variants and construct the result.
+ for (String part : patternParts) {
+ var partString = head != null ? head + part : part;
+
+ // recurse on the tail of the input
+ // to handle the next pattern
+
+ // append all variants of the tail end
+ // and add each of them to the part we have
+ // built up so far.
+ for (String tailVariant : variantsForTail) {
+ variants.add(partString + tailVariant);
+ }
+ }
+ }
+
+ return variants;
+ }
+ return Collections.emptySet();
+ }
+
+ /**
+ * Returns a new copy set of all variants with no whitespace compression.
+ *
+ * @return a {@link Set} of all variants without whitespace compression
+ */
+ public Set allVariants() {
+ return variants.size() == 1 ? Collections.singleton(variants.iterator().next()) : new HashSet<>(variants);
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/KotlinConfig.kt b/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/KotlinConfig.kt
new file mode 100644
index 00000000..aaa03aaf
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/KotlinConfig.kt
@@ -0,0 +1,11 @@
+package com.github.kumaraman21.intellijbehave.kotlin
+
+import com.intellij.ide.plugins.PluginManagerCore
+import com.intellij.openapi.extensions.PluginId
+
+/**
+ * Created by Rodrigo Quesada on 20/09/15.
+ */
+private const val kotlinPluginId = "org.jetbrains.kotlin"
+
+val pluginIsEnabled = PluginManagerCore.getPlugin(PluginId.getId(kotlinPluginId))?.isEnabled ?: false
\ No newline at end of file
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/psi/NavigableKotlinPsiAnnotation.kt b/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/psi/NavigableKotlinPsiAnnotation.kt
new file mode 100644
index 00000000..2b5bf999
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/psi/NavigableKotlinPsiAnnotation.kt
@@ -0,0 +1,25 @@
+package com.github.kumaraman21.intellijbehave.kotlin.psi
+
+import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiElement
+import org.jetbrains.kotlin.psi.KtElement
+
+/**
+ * Represents a Kotlin PSI annotation with its PSI and Kotlin element counterparts.
+ * This type also provides (overrides) some of the navigation behaviours from its parent class.
+ *
+ * Created by Rodrigo Quesada on 20/09/15.
+ */
+class NavigableKotlinPsiAnnotation(
+ private val psiAnnotation: PsiAnnotation,
+ ktElement: KtElement
+) : PsiAnnotation by psiAnnotation {
+
+ private val navigableKotlinPsiElement = NavigableKotlinPsiElement(psiAnnotation, ktElement)
+
+ override fun getTextOffset(): Int = navigableKotlinPsiElement.textOffset
+
+ override fun getParent(): PsiElement = navigableKotlinPsiElement.parent
+
+ override fun getNavigationElement(): PsiElement = navigableKotlinPsiElement.navigationElement
+}
\ No newline at end of file
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/psi/NavigableKotlinPsiElement.kt b/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/psi/NavigableKotlinPsiElement.kt
new file mode 100644
index 00000000..b84f2dd4
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/psi/NavigableKotlinPsiElement.kt
@@ -0,0 +1,44 @@
+package com.github.kumaraman21.intellijbehave.kotlin.psi
+
+import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethod
+import org.jetbrains.kotlin.psi.KtElement
+
+/**
+ * Represents a Kotlin PSI element with its PSI and Kotlin element counterparts.
+ * This type also provides (overrides) some of the navigation behaviours from its parent class.
+ *
+ * Created by Rodrigo Quesada on 20/09/15.
+ */
+open class NavigableKotlinPsiElement(
+ private val psiElement: PsiElement,
+ private val ktElement: KtElement)
+: PsiElement by psiElement {
+
+ override fun getTextOffset(): Int = ktElement.textOffset
+
+ /**
+ * Returns the parent element as one of the custom `NavigableKotlin*` element types.
+ */
+ override fun getParent(): PsiElement {
+ val psiParent = psiElement.parent
+ val ktParent = ktElement.parent
+
+ return if (ktParent !is KtElement) {
+ psiParent
+ } else {
+ when (psiParent) {
+ is PsiAnnotation -> NavigableKotlinPsiAnnotation(psiParent, ktParent)
+ is PsiMethod -> NavigableKotlinPsiMethod(psiParent, ktParent)
+ else -> {
+ NavigableKotlinPsiElement(psiParent, ktParent)
+ }
+ }
+ }
+ }
+
+ override fun getNavigationElement(): PsiElement {
+ return this
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/psi/NavigableKotlinPsiMethod.kt b/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/psi/NavigableKotlinPsiMethod.kt
new file mode 100644
index 00000000..4f0b83a7
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/psi/NavigableKotlinPsiMethod.kt
@@ -0,0 +1,25 @@
+package com.github.kumaraman21.intellijbehave.kotlin.psi
+
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethod
+import org.jetbrains.kotlin.psi.KtElement
+
+/**
+ * Represents a Kotlin PSI method with its PSI and Kotlin element counterparts.
+ * This type also provides (overrides) some of the navigation behaviours from its parent class.
+ *
+ * Created by Rodrigo Quesada on 20/09/15.
+ */
+class NavigableKotlinPsiMethod(
+ private val psiMethod: PsiMethod,
+ ktElement: KtElement
+) : PsiMethod by psiMethod {
+
+ private val navigableKotlinPsiElement = NavigableKotlinPsiElement(psiMethod, ktElement)
+
+ override fun getTextOffset(): Int = navigableKotlinPsiElement.textOffset
+
+ override fun getParent(): PsiElement = navigableKotlinPsiElement.parent
+
+ override fun getNavigationElement(): PsiElement = navigableKotlinPsiElement.navigationElement
+}
\ No newline at end of file
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/support/services/KotlinAnnotationsLoader.kt b/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/support/services/KotlinAnnotationsLoader.kt
new file mode 100644
index 00000000..66d40a52
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/support/services/KotlinAnnotationsLoader.kt
@@ -0,0 +1,43 @@
+package com.github.kumaraman21.intellijbehave.kotlin.support.services
+
+import com.github.kumaraman21.intellijbehave.kotlin.psi.NavigableKotlinPsiAnnotation
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.search.GlobalSearchScope
+import com.intellij.psi.util.QualifiedName
+import org.jetbrains.kotlin.asJava.LightClassUtil
+import org.jetbrains.kotlin.idea.stubindex.KotlinAnnotationsIndex
+import org.jetbrains.kotlin.psi.KtFunction
+
+/**
+ * Created by Rodrigo Quesada on 20/09/15.
+ */
+class KotlinAnnotationsLoader private constructor() {
+
+ companion object {
+ /**
+ * Returns all occurrences of the step annotation referenced by [qualifiedName] in Kotlin files, in the given [scope].
+ *
+ * @param qualifiedName a step annotation, e.g. `@org.jbehave.core.annotations.Given`
+ * @param project the current project
+ * @param scope the search scope where to look up the annotation usage
+ *
+ * @see com.github.kumaraman21.intellijbehave.service.JBehaveStepsIndex.getAllStepAnnotations
+ */
+ @JvmStatic
+ fun getAnnotations(qualifiedName: QualifiedName, project: Project, scope: GlobalSearchScope): Collection {
+ val name = qualifiedName.lastComponent
+ return if (name != null) {
+ KotlinAnnotationsIndex[name, project, scope]
+ .asSequence()
+ .map { ktAnnotation ->
+ val function = ktAnnotation.parent?.parent as? KtFunction
+ function?.let {
+ val psiAnnotation = LightClassUtil.getLightClassMethod(function)?.modifierList?.findAnnotation(qualifiedName.toString())
+ psiAnnotation?.let { NavigableKotlinPsiAnnotation(psiAnnotation, ktAnnotation) }
+ }
+ }.filterNotNull().toList()
+ } else emptyList()
+ }
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/support/services/KotlinPsiClassesHandler.kt b/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/support/services/KotlinPsiClassesHandler.kt
new file mode 100644
index 00000000..728cefed
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/kotlin/support/services/KotlinPsiClassesHandler.kt
@@ -0,0 +1,88 @@
+package com.github.kumaraman21.intellijbehave.kotlin.support.services
+
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiFile
+import org.jetbrains.kotlin.idea.structuralsearch.visitor.KotlinRecursiveElementVisitor
+import org.jetbrains.kotlin.idea.util.findAnnotation
+import org.jetbrains.kotlin.name.FqName
+import org.jetbrains.kotlin.psi.KtClass
+import org.jetbrains.kotlin.psi.KtFile
+import org.jetbrains.kotlin.psi.psiUtil.isPublic
+import kotlin.jvm.internal.Ref.BooleanRef
+
+/**
+ * Provides utilities for working with and processing Kotlin files and class.
+ *
+ * Created by Rodrigo Quesada on 20/09/15.
+ */
+class KotlinPsiClassesHandler private constructor() {
+
+ companion object {
+ private val GIVEN = FqName("org.jbehave.core.annotations.Given")
+ private val WHEN = FqName("org.jbehave.core.annotations.When")
+ private val THEN = FqName("org.jbehave.core.annotations.Then")
+ private val ALIAS = FqName("org.jbehave.core.annotations.Alias")
+ private val ALIASES = FqName("org.jbehave.core.annotations.Aliases")
+ private val COMPOSITE = FqName("org.jbehave.core.annotations.Composite")
+
+ /**
+ * Returns the classes in [psiFile] if it is a Kotlin file, otherwise returns null.
+ */
+ @JvmStatic
+ fun getPsiClasses(psiFile: PsiFile): Array? = if (psiFile is KtFile) {
+ psiFile.classes
+ } else null
+
+ @JvmStatic
+ fun isKotlinFile(psiFile: PsiFile): Boolean = psiFile is KtFile
+
+ /**
+ * Returns if the provided Kotlin [file] contains any step definition class,
+ * meaning at least one class that contains at least one step definition method.
+ *
+ * If the file is not a Kotlin file, it returns false.
+ */
+ @JvmStatic
+ fun visitClasses(file: PsiFile): Boolean {
+ val hasJBehaveStepDefTestClass = BooleanRef()
+ if (file is KtFile) {
+ file.accept(object : KotlinRecursiveElementVisitor() {
+ override fun visitClass(aClass: KtClass) {
+ if (isKotlinJBehaveStepDefClass(aClass)) {
+ hasJBehaveStepDefTestClass.element = true
+ return
+ }
+ super.visitClass(aClass)
+ }
+ })
+ }
+
+ return hasJBehaveStepDefTestClass.element
+ }
+
+ /**
+ * Returns if any of the functions in the provided Kotlin class is a step definition function.
+ */
+ private fun isKotlinJBehaveStepDefClass(aClass: KtClass): Boolean {
+ return try {
+ !aClass.isEnum()
+ && !aClass.isInterface()
+ && aClass.fqName != null
+ && aClass.body?.functions?.asSequence()?.filter { it.isPublic }?.any {
+ return@any try {
+ it.findAnnotation(GIVEN) != null
+ || it.findAnnotation(WHEN) != null
+ || it.findAnnotation(THEN) != null
+ || it.findAnnotation(ALIAS) != null
+ || it.findAnnotation(ALIASES) != null
+ || it.findAnnotation(COMPOSITE) != null
+ } catch (_: Exception) {
+ false
+ }
+ } == true
+ } catch (_: Exception) {
+ false
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/language/JBehaveIcons.java b/src/main/java/com/github/kumaraman21/intellijbehave/language/JBehaveIcons.java
new file mode 100644
index 00000000..1cdb3d8c
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/language/JBehaveIcons.java
@@ -0,0 +1,11 @@
+package com.github.kumaraman21.intellijbehave.language;
+
+import com.intellij.openapi.util.IconLoader;
+
+import javax.swing.*;
+
+public class JBehaveIcons {
+ //https://github.com/jbehave/jbehave-eclipse/blob/master/org.jbehave.eclipse/icons/bdd-jb-orange-red-green.png.
+ public static final Icon JB = IconLoader.getIcon("/fileTypes/bdd-jb-orange-red-green.png",
+ JBehaveIcons.class); // 16x16
+}
diff --git a/src/com/github/kumaraman21/intellijbehave/language/StoryFileType.java b/src/main/java/com/github/kumaraman21/intellijbehave/language/StoryFileType.java
similarity index 59%
rename from src/com/github/kumaraman21/intellijbehave/language/StoryFileType.java
rename to src/main/java/com/github/kumaraman21/intellijbehave/language/StoryFileType.java
index 9cd31e0d..fde20162 100644
--- a/src/com/github/kumaraman21/intellijbehave/language/StoryFileType.java
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/language/StoryFileType.java
@@ -16,7 +16,6 @@
package com.github.kumaraman21.intellijbehave.language;
import com.intellij.openapi.fileTypes.LanguageFileType;
-import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
@@ -24,37 +23,32 @@
import static com.github.kumaraman21.intellijbehave.language.StoryLanguage.STORY_LANGUAGE;
public class StoryFileType extends LanguageFileType {
- public static final StoryFileType STORY_FILE_TYPE = new StoryFileType();
-
- protected StoryFileType() {
- super(STORY_LANGUAGE);
- }
-
- @NotNull
- @Override
- public String getName() {
- return "JBehave Story";
- }
-
- @NotNull
- @Override
- public String getDescription() {
- return "JBehave story files";
- }
-
- @NotNull
- @Override
- public String getDefaultExtension() {
- return "story";
- }
-
- @Override
- public Icon getIcon() {
- return null;
- }
-
- @Override
- public String getCharset(@NotNull VirtualFile virtualFile, byte[] bytes) {
- return null;
- }
+ public static final StoryFileType STORY_FILE_TYPE = new StoryFileType();
+
+ protected StoryFileType() {
+ super(STORY_LANGUAGE);
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return "JBehave Story";
+ }
+
+ @NotNull
+ @Override
+ public String getDescription() {
+ return "JBehave story files";
+ }
+
+ @NotNull
+ @Override
+ public String getDefaultExtension() {
+ return "story";
+ }
+
+ @Override
+ public Icon getIcon() {
+ return JBehaveIcons.JB;
+ }
}
diff --git a/src/com/github/kumaraman21/intellijbehave/language/StoryLanguage.java b/src/main/java/com/github/kumaraman21/intellijbehave/language/StoryLanguage.java
similarity index 91%
rename from src/com/github/kumaraman21/intellijbehave/language/StoryLanguage.java
rename to src/main/java/com/github/kumaraman21/intellijbehave/language/StoryLanguage.java
index a578db78..4ce69891 100644
--- a/src/com/github/kumaraman21/intellijbehave/language/StoryLanguage.java
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/language/StoryLanguage.java
@@ -27,7 +27,7 @@ public class StoryLanguage extends CompositeLanguage {
private StoryLanguage() {
super("Story", "text/story");
- SyntaxHighlighterFactory.LANGUAGE_FACTORY.addExplicitExtension(this, new SingleLazyInstanceSyntaxHighlighterFactory() {
+ SyntaxHighlighterFactory.getLanguageFactory().addExplicitExtension(this, new SingleLazyInstanceSyntaxHighlighterFactory() {
@NotNull
protected SyntaxHighlighter createHighlighter() {
return new StorySyntaxHighlighter();
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/parser/JBehaveStep.java b/src/main/java/com/github/kumaraman21/intellijbehave/parser/JBehaveStep.java
new file mode 100644
index 00000000..6701d6d6
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/parser/JBehaveStep.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.parser;
+
+import com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType;
+import com.intellij.extapi.psi.ASTWrapperPsiElement;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry;
+import org.jbehave.core.steps.StepType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import static org.apache.commons.lang3.StringUtils.trim;
+
+public class JBehaveStep extends ASTWrapperPsiElement {
+ private final StepType stepType;
+
+ public JBehaveStep(@NotNull ASTNode node, StepType stepType) {
+ super(node);
+ this.stepType = stepType;
+ }
+
+ @Override
+ public PsiReference @NotNull [] getReferences() {
+ return ReadAction.compute(() -> ReferenceProvidersRegistry.getReferencesFromProviders(this));
+ }
+
+ public StepType getStepType() {
+ return stepType;
+ }
+
+ @Nullable
+ public ASTNode getKeyword() {
+ return getNode().findChildByType(StoryTokenType.STEP_TYPES);
+ }
+
+ /**
+ * Returns the step text without the step type. For example:
+ * for the step {@code Given I opened the homepage} it returns {@code I opened the homepage}.
+ */
+ public String getStepText() {
+ int offset = getStepTextOffset();
+ String text = getText();
+
+ if (offset <= 0 || offset >= text.length()) {
+ return trim(text);
+ } else {
+ return trim(text.substring(offset));
+ }
+ }
+
+ /**
+ * Returns the step type keyword, for example {@code Given} for the step
+ * {@code Given I opened the homepage}.
+ */
+ @Nullable("When the keyword is null.")
+ public String getActualStepPrefix() {
+ ASTNode keyword = getKeyword();
+ if (keyword == null) { // that's weird!
+ return null;
+ }
+ return keyword.getText();
+ }
+
+ /**
+ * Returns the off set the step text after the step type keyword. For example:
+ * for {@code Given I opened the homepage}, it would return 6.
+ *
+ * Returns 0 when the step type prefix is null.
+ */
+ public int getStepTextOffset() {
+ String stepPrefix = getActualStepPrefix();
+ return stepPrefix != null ? stepPrefix.length() + 1 : 0;
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/parser/StoryElementType.java b/src/main/java/com/github/kumaraman21/intellijbehave/parser/StoryElementType.java
new file mode 100644
index 00000000..0abce546
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/parser/StoryElementType.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.parser;
+
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.tree.IFileElementType;
+import com.intellij.psi.tree.TokenSet;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+
+import static com.github.kumaraman21.intellijbehave.language.StoryFileType.STORY_FILE_TYPE;
+
+public class StoryElementType extends IElementType {
+ public static final StoryElementType UNKNOWN_FRAGMENT = new StoryElementType("UNKNOWN_FRAGMENT");
+
+ public static final StoryElementType COMMENT = new StoryElementType("COMMENT");
+ public static final IFileElementType STORY_FILE = new IFileElementType(STORY_FILE_TYPE.getLanguage());
+ public static final StoryElementType STORY = new StoryElementType("STORY");
+ public static final StoryElementType STORY_DESCRIPTION = new StoryElementType("STORY_DESCRIPTION");
+ public static final StoryElementType SCENARIO = new StoryElementType("SCENARIO");
+ public static final StoryElementType META = new StoryElementType("META");
+ public static final StoryElementType EXAMPLES = new StoryElementType("EXAMPLES");
+ public static final StoryElementType TABLE_ROW = new StoryElementType("TABLE_ROW");
+
+ public static final StoryElementType GIVEN_STEP = new StoryElementType("GIVEN_STEP");
+ public static final StoryElementType WHEN_STEP = new StoryElementType("WHEN_STEP");
+ public static final StoryElementType THEN_STEP = new StoryElementType("THEN_STEP");
+
+ public static final TokenSet STEPS_TOKEN_SET = TokenSet.create(GIVEN_STEP, WHEN_STEP, THEN_STEP);
+
+
+ public StoryElementType(@NotNull @NonNls String debugName) {
+ super(debugName, STORY_FILE_TYPE.getLanguage());
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/parser/StoryFile.java b/src/main/java/com/github/kumaraman21/intellijbehave/parser/StoryFile.java
new file mode 100644
index 00000000..11c838ca
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/parser/StoryFile.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.parser;
+
+import com.intellij.extapi.psi.PsiFileBase;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.tree.TokenSet;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.github.kumaraman21.intellijbehave.language.StoryFileType.STORY_FILE_TYPE;
+import static com.github.kumaraman21.intellijbehave.parser.StoryElementType.*;
+
+public class StoryFile extends PsiFileBase {
+
+ public StoryFile(FileViewProvider fileViewProvider) {
+ super(fileViewProvider, STORY_FILE_TYPE.getLanguage());
+ }
+
+ @NotNull
+ @Override
+ public FileType getFileType() {
+ return STORY_FILE_TYPE;
+ }
+
+ @NotNull
+ public List getSteps() {
+
+ return getScenarios().stream()
+ .map(scenario -> scenario.getNode().getChildren(STEPS_TOKEN_SET))
+ .flatMap(Stream::of)
+ .map(astNode -> (JBehaveStep) astNode.getPsi())
+ .collect(Collectors.toList());
+ }
+
+ @NotNull
+ private List getScenarios() {
+ PsiElement story = getStory();
+ if (story == null) {
+ return new ArrayList<>();
+ }
+
+ ASTNode[] scenarioNodes = story.getNode().getChildren(TokenSet.create(SCENARIO));
+ return Stream.of(scenarioNodes).map(ASTNode::getPsi).collect(Collectors.toList());
+ }
+
+ private PsiElement getStory() {
+ ASTNode[] storyNodes = this.getNode().getChildren(TokenSet.create(STORY));
+ return storyNodes.length > 0 ? storyNodes[0].getPsi() : null;
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/parser/StoryParser.java b/src/main/java/com/github/kumaraman21/intellijbehave/parser/StoryParser.java
new file mode 100644
index 00000000..4fb7e17f
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/parser/StoryParser.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.parser;
+
+import com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.PsiBuilder;
+import com.intellij.lang.PsiParser;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.tree.TokenSet;
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+
+public class StoryParser implements PsiParser {
+
+ private static final boolean DEBUG = false;
+
+ @NotNull
+ @Override
+ public ASTNode parse(IElementType root, PsiBuilder builder) {
+ final PsiBuilder.Marker rootMarker = builder.mark();
+ builder.setDebugMode(true);
+ parseStory(builder);
+ rootMarker.done(root);
+ return builder.getTreeBuilt();
+ }
+
+ @SuppressWarnings("UnnecessaryLabelOnContinueStatement")
+ private void parseStory(PsiBuilder builder) {
+ final PsiBuilder.Marker storyMarker = builder.mark();
+
+ ParserState state = new ParserState(builder);
+
+ whileLoop:
+ while (!builder.eof()) {
+ IElementType tokenType = builder.getTokenType();
+
+ // Comment and whitespace are not returned by default
+
+ if (isComment(tokenType)) {
+ state.enterComment();
+ builder.advanceLexer();
+ continue whileLoop;
+ } else {
+ state.leaveComment();
+ }
+
+ if (isWhitespace(tokenType)) {
+ if (isCrlf(builder.getTokenText())) {
+ // this is never called unfortunately
+ state.leaveTableRow();
+ }
+
+ state.enterWhitespace();
+ builder.advanceLexer();
+ continue whileLoop;
+ } else {
+ state.leaveWhitespace();
+ }
+
+
+ if (isStoryDescription(tokenType)) {
+ state.enterStoryDescription();
+ builder.advanceLexer();
+ continue whileLoop;
+ } else {
+ state.leaveStoryDescription();
+ }
+
+ if (isScenario(tokenType)) {
+ state.enterScenario();
+ builder.advanceLexer();
+ continue whileLoop;
+ } else if (!belongsToScenario(tokenType)) {
+ state.leaveScenario();
+ }
+
+ if (isScenarioText(tokenType)) {
+ builder.advanceLexer();
+ continue whileLoop;
+ }
+
+ if (isMeta(tokenType)) {
+ state.enterMeta();
+ builder.advanceLexer();
+ continue whileLoop;
+ }
+
+ if (isStepType(tokenType)) {
+ state.enterStepType(tokenType);
+ builder.advanceLexer();
+ continue whileLoop;
+ }
+
+ if (isStepText(tokenType)) {
+ builder.advanceLexer();
+ continue whileLoop;
+ }
+
+ if (isExampleTable(tokenType)) {
+ state.enterExampleTable();
+ builder.advanceLexer();
+ continue whileLoop;
+ } else if (!belongsToTable(tokenType)) {
+ state.leaveExampleTable();
+ }
+
+ if (isTableRow(tokenType)) {
+ state.enterTableRow();
+ builder.advanceLexer();
+ continue whileLoop;
+ }
+
+ // unknown
+ PsiBuilder.Marker unknwonMark = builder.mark();
+ builder.advanceLexer();
+ unknwonMark.done(StoryElementType.UNKNOWN_FRAGMENT);
+ }
+ state.leaveRemainings();
+ storyMarker.done(StoryElementType.STORY);
+ }
+
+ private static boolean isCrlf(String text) {
+ return text.contains("\n") || text.contains("\r");
+ }
+
+ private static class MarkerData {
+ private final PsiBuilder.Marker marker;
+ private final IElementType elementType;
+
+ private MarkerData(PsiBuilder.Marker marker, IElementType elementType) {
+ this.marker = marker;
+ this.elementType = elementType;
+ }
+
+ public boolean matches(IElementType elementType) {
+ return this.elementType == elementType;
+ }
+
+ public boolean matches(TokenSet tokenSet) {
+ return tokenSet.contains(this.elementType);
+ }
+
+ public void applyMark() {
+ marker.done(elementType);
+ }
+ }
+
+ private static class ParserState {
+ private final PsiBuilder builder;
+ private final MarkerData[] markers = new MarkerData[10];
+ private int markerIndex = -1;
+ private StoryElementType previousStepElementType = null;
+
+
+ public ParserState(PsiBuilder builder) {
+ this.builder = builder;
+ }
+
+ private void matchesHeadOrPush(StoryElementType elementType) {
+ if (markerIndex >= 0 && markers[markerIndex].matches(elementType)) {
+ return;
+ }
+ markers[++markerIndex] = new MarkerData(builder.mark(), elementType);
+ if (DEBUG) {
+ System.out.println("StoryParser$ParserState: PUSH>> " + StringUtils.repeat("..", markerIndex) + elementType);
+ }
+ }
+
+ private void popUntilOnlyIfPresent(StoryElementType elementType) {
+ int newMarkerIndex = markerIndex;
+ for (int i = markerIndex; i >= 0; i--) {
+ if (markers[i].matches(elementType)) {
+ newMarkerIndex = i - 1;
+ break;
+ }
+ }
+
+ for (int i = newMarkerIndex + 1; i <= markerIndex; i++) {
+ if (DEBUG) {
+ System.out.println("StoryParser$ParserState: POP >> " + StringUtils.repeat("..", i) + markers[i].elementType);
+ }
+ markers[i].applyMark();
+ }
+
+ markerIndex = newMarkerIndex;
+ }
+
+ public void leaveRemainings() {
+ while (markerIndex >= 0) {
+ if (DEBUG) {
+ System.out.println("StoryParser$ParserState: POP >> " + StringUtils.repeat("..", markerIndex) + markers[markerIndex].elementType);
+ }
+ markers[markerIndex--].applyMark();
+ }
+ }
+
+
+ private void popUntilOnlyIfPresent(TokenSet tokenSet) {
+ int newMarkerIndex = markerIndex;
+ for (int i = markerIndex; i >= 0; i--) {
+ if (markers[i].matches(tokenSet)) {
+ newMarkerIndex = i - 1;
+ break;
+ }
+ }
+
+ for (int i = newMarkerIndex + 1; i <= markerIndex; i++) {
+ if (DEBUG) {
+ System.out.println("StoryParser$ParserState: POP >> " + StringUtils.repeat("..", i) + markers[i].elementType);
+ }
+ markers[i].applyMark();
+ }
+
+ markerIndex = newMarkerIndex;
+ }
+
+
+ public void enterComment() {
+ matchesHeadOrPush(StoryElementType.COMMENT);
+ }
+
+ private void leaveComment() {
+ popUntilOnlyIfPresent(StoryElementType.COMMENT);
+ }
+
+ public void enterWhitespace() {
+ }
+
+ public void leaveWhitespace() {
+ }
+
+ public void enterStoryDescription() {
+ leaveRemainings();
+ matchesHeadOrPush(StoryElementType.STORY_DESCRIPTION);
+ }
+
+ private void leaveStoryDescription() {
+ popUntilOnlyIfPresent(StoryElementType.STORY_DESCRIPTION);
+ }
+
+ public void enterScenario() {
+ leaveRemainings();
+ previousStepElementType = null;
+ matchesHeadOrPush(StoryElementType.SCENARIO);
+ }
+
+ private void leaveScenario() {
+ leaveRemainings();
+ previousStepElementType = null;
+ popUntilOnlyIfPresent(StoryElementType.SCENARIO);
+ }
+
+ public void enterMeta() {
+ leaveStoryDescription();
+ leaveExampleTable();
+ leaveTableRow();
+ leaveStep();
+ matchesHeadOrPush(StoryElementType.META);
+ }
+
+ private void leaveMeta() {
+ popUntilOnlyIfPresent(StoryElementType.META);
+ }
+
+ public void enterStepType(IElementType tokenType) {
+ leaveExampleTable();
+ leaveMeta();
+ leaveStep();
+ leaveStoryDescription();
+ leaveTableRow();
+
+ StoryElementType elementType = previousStepElementType;
+ if (tokenType == StoryTokenType.STEP_TYPE_GIVEN) {
+ elementType = StoryElementType.GIVEN_STEP;
+ } else if (tokenType == StoryTokenType.STEP_TYPE_WHEN) {
+ elementType = StoryElementType.WHEN_STEP;
+ } else if (tokenType == StoryTokenType.STEP_TYPE_THEN) {
+ elementType = StoryElementType.THEN_STEP;
+ }
+
+ if (elementType == null) { // yuk!
+ elementType = StoryElementType.GIVEN_STEP;
+ }
+ previousStepElementType = elementType;
+ matchesHeadOrPush(elementType);
+ }
+
+ private void leaveStep() {
+ leaveTableRow();
+ popUntilOnlyIfPresent(StoryElementType.STEPS_TOKEN_SET);
+ }
+
+ public void enterExampleTable() {
+ leaveTableRow();
+ leaveStep();
+ leaveMeta();
+ leaveExampleTable();
+ matchesHeadOrPush(StoryElementType.EXAMPLES);
+ }
+
+ public void leaveExampleTable() {
+ leaveTableRow();
+ popUntilOnlyIfPresent(StoryElementType.EXAMPLES);
+ }
+
+ public void enterTableRow() {
+ matchesHeadOrPush(StoryElementType.TABLE_ROW);
+ }
+
+ public void leaveTableRow() {
+ popUntilOnlyIfPresent(StoryElementType.TABLE_ROW);
+ }
+ }
+
+ private static boolean belongsToScenario(IElementType tokenType) {
+ return isWhitespace(tokenType)
+ || isComment(tokenType)
+ || isScenarioText(tokenType)
+ || isStoryDescription(tokenType)
+ || isStepType(tokenType)
+ || isStepText(tokenType)
+ || isExampleTable(tokenType)
+ || isTableRow(tokenType)
+ || isMeta(tokenType);
+ }
+
+ private boolean belongsToTable(IElementType tokenType) {
+ return isWhitespace(tokenType)
+ || isComment(tokenType)
+ || isTableRow(tokenType);
+ }
+
+ private static boolean isMeta(IElementType tokenType) {
+ return tokenType == StoryTokenType.META
+ || tokenType == StoryTokenType.META_KEY
+ || tokenType == StoryTokenType.META_TEXT;
+ }
+
+ private static boolean isExampleTable(IElementType tokenType) {
+ return tokenType == StoryTokenType.EXAMPLE_TYPE;
+ }
+
+ private static boolean isTableRow(IElementType tokenType) {
+ return tokenType == StoryTokenType.TABLE_CELL
+ || tokenType == StoryTokenType.TABLE_DELIM;
+ }
+
+ private static boolean isStepType(IElementType tokenType) {
+ return tokenType == StoryTokenType.STEP_TYPE_GIVEN
+ || tokenType == StoryTokenType.STEP_TYPE_WHEN
+ || tokenType == StoryTokenType.STEP_TYPE_THEN
+ || tokenType == StoryTokenType.STEP_TYPE_AND;
+ }
+
+ private static boolean isStepText(IElementType tokenType) {
+ return tokenType == StoryTokenType.STEP_TEXT;
+ }
+
+ private static boolean isScenario(IElementType tokenType) {
+ return tokenType == StoryTokenType.SCENARIO_TYPE;
+ }
+
+ private static boolean isScenarioText(IElementType tokenType) {
+ return tokenType == StoryTokenType.SCENARIO_TEXT;
+ }
+
+ private static boolean isWhitespace(IElementType tokenType) {
+ return tokenType == StoryTokenType.WHITE_SPACE;
+ }
+
+ private static boolean isComment(IElementType tokenType) {
+ return tokenType == StoryTokenType.COMMENT
+ || tokenType == StoryTokenType.COMMENT_WITH_LOCALE;
+ }
+
+ private static boolean isStoryDescription(IElementType tokenType) {
+ return tokenType == StoryTokenType.STORY_DESCRIPTION
+ || tokenType == StoryTokenType.NARRATIVE_TYPE
+ || tokenType == StoryTokenType.NARRATIVE_TEXT;
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/parser/StoryParserDefinition.java b/src/main/java/com/github/kumaraman21/intellijbehave/parser/StoryParserDefinition.java
new file mode 100644
index 00000000..05510458
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/parser/StoryParserDefinition.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.parser;
+
+import com.github.kumaraman21.intellijbehave.highlighter.StoryLexerFactory;
+import com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType;
+import com.intellij.extapi.psi.ASTWrapperPsiElement;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.ParserDefinition;
+import com.intellij.lang.PsiParser;
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.tree.IFileElementType;
+import com.intellij.psi.tree.TokenSet;
+import org.jbehave.core.steps.StepType;
+import org.jetbrains.annotations.NotNull;
+
+public class StoryParserDefinition implements ParserDefinition {
+ @NotNull
+ @Override
+ public Lexer createLexer(Project project) {
+ return new StoryLexerFactory().createLexer();
+ }
+
+ @Override
+ public PsiParser createParser(Project project) {
+ return new StoryParser();
+ }
+
+ @Override
+ public IFileElementType getFileNodeType() {
+ return StoryElementType.STORY_FILE;
+ }
+
+ @NotNull
+ @Override
+ public TokenSet getWhitespaceTokens() {
+ return TokenSet.create(StoryTokenType.WHITE_SPACE);
+ }
+
+ @NotNull
+ @Override
+ public TokenSet getCommentTokens() {
+ return TokenSet.create(StoryTokenType.COMMENT, StoryTokenType.COMMENT_WITH_LOCALE);
+ }
+
+ @NotNull
+ @Override
+ public TokenSet getStringLiteralElements() {
+ return TokenSet.EMPTY;
+ }
+
+ @NotNull
+ @Override
+ public PsiElement createElement(ASTNode node) {
+ final IElementType type = node.getElementType();
+ if (type == StoryElementType.GIVEN_STEP) {
+ return new JBehaveStep(node, StepType.GIVEN);
+ } else if (type == StoryElementType.WHEN_STEP) {
+ return new JBehaveStep(node, StepType.WHEN);
+ } else if (type == StoryElementType.THEN_STEP) {
+ return new JBehaveStep(node, StepType.THEN);
+ }
+
+ return new ASTWrapperPsiElement(node);
+ }
+
+ @Override
+ public PsiFile createFile(FileViewProvider fileViewProvider) {
+ return new StoryFile(fileViewProvider);
+ }
+
+ @Override
+ public SpaceRequirements spaceExistenceTypeBetweenTokens(ASTNode left, ASTNode right) {
+ return SpaceRequirements.MAY;
+ }
+}
diff --git a/src/com/github/kumaraman21/intellijbehave/codeInspector/UnusedStepsInspectionProvider.java b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotation.java
similarity index 66%
rename from src/com/github/kumaraman21/intellijbehave/codeInspector/UnusedStepsInspectionProvider.java
rename to src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotation.java
index 04b4ae6f..6f44427d 100644
--- a/src/com/github/kumaraman21/intellijbehave/codeInspector/UnusedStepsInspectionProvider.java
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotation.java
@@ -13,12 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.github.kumaraman21.intellijbehave.codeInspector;
+package com.github.kumaraman21.intellijbehave.resolver;
-import com.intellij.codeInspection.InspectionToolProvider;
+import com.intellij.psi.PsiAnnotation;
+import org.jbehave.core.steps.StepType;
-public class UnusedStepsInspectionProvider implements InspectionToolProvider {
- public Class[] getInspectionClasses() {
- return new Class[] { UnusedStepsInspection.class };
- }
+public record StepDefinitionAnnotation(StepType stepType, String annotationText, PsiAnnotation annotation) {
}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotationConverter.java b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotationConverter.java
new file mode 100644
index 00000000..5654fa6f
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotationConverter.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.resolver;
+
+import static com.github.kumaraman21.intellijbehave.utility.StepTypeMappings.ANNOTATION_TO_STEP_TYPE_MAPPING;
+import static com.intellij.openapi.application.ReadAction.compute;
+import static org.apache.commons.lang3.StringUtils.remove;
+import static org.apache.commons.lang3.StringUtils.removeEnd;
+import static org.apache.commons.lang3.StringUtils.removeStart;
+
+import com.github.kumaraman21.intellijbehave.jbehave.core.steps.PatternVariantBuilder;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiAnnotation;
+import com.intellij.psi.PsiAnnotationMemberValue;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiLiteral;
+import org.jbehave.core.annotations.Alias;
+import org.jbehave.core.annotations.Aliases;
+import org.jbehave.core.steps.StepType;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public final class StepDefinitionAnnotationConverter {
+
+ /**
+ * Converts each of the input annotations to a {@link StepDefinitionAnnotation} by extracting
+ * information (the step type and annotation text) from them.
+ *
+ *
Limitations
+ * Currently, there are a couple of cases that are either not handled or handled incorrectly:
+ *
+ *
When converting an @Alias or @Aliases annotation, the order of the input annotations matters.
+ * Having the step annotation first, i.e. {@code @When -> @Alias}, the {@code @Alias} annotation
+ * is created with the proper WHEN step type. If the order is switched ({@code @Alias -> @When}),
+ * the step type becomes null, and that step candidate is not code completed by
+ * {@link com.github.kumaraman21.intellijbehave.completion.StoryCompletionContributor}.
+ *
The case when multiple of the {@code @Given/@When/@Then/@Alias/@Aliases} annotations
+ * are added to the same step definition method is not handled.
+ *
+ *
+ * @param annotations the annotations on a given step definition method
+ */
+ public static Set convertFrom(PsiAnnotation[] annotations) {
+ //Refs and AnnotationsHolders are not created in this case
+ if (annotations.length == 0) return Collections.emptySet();
+
+ //Variable init
+
+ final var result = new AnnotationsHolder();
+ StepType stepType = null;
+ var annotationQualifiedName = new Ref();
+
+ //Annotation processing
+
+ for (PsiAnnotation annotation : annotations) {
+ var attributes = compute(() -> {
+ annotationQualifiedName.set(annotation.getQualifiedName());
+ return annotation.getParameterList().getAttributes();
+ });
+
+ // When there are no attributes for the annotation, don't process this annotation
+ if (attributes.length == 0) continue;
+
+ //When the processed annotation is @Given, @When or @Then
+ if (ANNOTATION_TO_STEP_TYPE_MAPPING.containsKey(annotationQualifiedName.get())) {
+ stepType = ANNOTATION_TO_STEP_TYPE_MAPPING.get(annotationQualifiedName.get());
+ String annotationText = getTextFromValue(() -> attributes[0].getValue());
+ result.add(stepType, annotationText, annotation);
+ } else if (!annotationQualifiedName.isNull()) {
+
+ //When the processed annotation is @Alias
+ if (annotationQualifiedName.get().equals(Alias.class.getName())) {
+ String annotationText = getTextFromValue(() -> attributes[0].getValue());
+ result.add(stepType, annotationText, annotation);
+ }
+
+ //When the processed annotation is @Aliases
+ else if (annotationQualifiedName.get().equals(Aliases.class.getName())) {
+ PsiAnnotationMemberValue attributeValue = compute(() -> attributes[0].getValue());
+ if (attributeValue != null) {
+ PsiElement[] values = attributeValue.getChildren();
+ //Processes all specified alias values in the annotation attribute
+ for (PsiElement value : values) {
+ if (value instanceof PsiLiteral) {
+ String annotationText = getTextFromValue(() -> value);
+ result.add(stepType, annotationText, annotation);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return result.getAnnotations();
+ }
+
+ private static Set getPatternVariants(final StepType stepType, String annotationText, final PsiAnnotation annotation) {
+ return new PatternVariantBuilder(annotationText)
+ .allVariants()
+ .stream()
+ .map(variant -> new StepDefinitionAnnotation(stepType, variant, annotation))
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * By using a Supplier here, {@code compute()} calls can be deduplicated with call sites of this method.
+ */
+ private static String getTextFromValue(Supplier value) {
+ return remove(removeStart(removeEnd(compute(() -> value.get().getText()), "\""), "\""), "\\");
+ }
+
+ private static final class AnnotationsHolder {
+ @Nullable
+ private Set annotations;
+
+ private Set getAnnotations() {
+ return annotations == null ? Collections.emptySet() : annotations;
+ }
+
+ private void add(StepType stepType, String annotationText, PsiAnnotation annotation) {
+ if (annotations == null) annotations = new HashSet<>();
+ annotations.addAll(getPatternVariants(stepType, annotationText, annotation));
+ }
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionIterator.java b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionIterator.java
new file mode 100644
index 00000000..c8754163
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionIterator.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.resolver;
+
+import static com.intellij.openapi.application.ReadAction.compute;
+
+import com.github.kumaraman21.intellijbehave.kotlin.KotlinConfigKt;
+import com.github.kumaraman21.intellijbehave.kotlin.support.services.KotlinPsiClassesHandler;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ContentIterator;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiAnnotation;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassOwner;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.PsiMethod;
+import org.jbehave.core.steps.StepType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Processes VirtualFiles to see if step annotations match the pre-configured step type.
+ */
+public abstract class StepDefinitionIterator implements ContentIterator {
+ private final StepType stepType;
+ private final Project project;
+
+ public StepDefinitionIterator(@Nullable StepType stepType, Project project) {
+ this.stepType = stepType;
+ this.project = project;
+ }
+
+ public StepType getStepType() {
+ return stepType;
+ }
+
+ @Override
+ public boolean processFile(@NotNull VirtualFile virtualFile) {
+ if (virtualFile.isDirectory()) return true;
+
+ var psiClasses = compute(() -> getPsiClasses(PsiManager.getInstance(project).findFile(virtualFile)));
+
+ for (PsiClass psiClass : psiClasses) {
+ for (PsiMethod method : compute(psiClass::getMethods)) {
+ PsiAnnotation[] annotations = compute(() -> method.getModifierList().getApplicableAnnotations());
+
+ for (StepDefinitionAnnotation stepDefinitionAnnotation : StepDefinitionAnnotationConverter.convertFrom(annotations)) {
+ if ((stepType == null || Objects.equals(stepType, stepDefinitionAnnotation.stepType()))
+ && !processStepDefinition(stepDefinitionAnnotation)) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the PSI classes from the provided file.
+ *
+ * @param psiFile the Java or Kotlin step definitions file
+ * @return the classes contained by the file
+ */
+ private static PsiClass[] getPsiClasses(PsiFile psiFile) {
+ if (!(psiFile instanceof PsiClassOwner psiClassOwner)) return PsiClass.EMPTY_ARRAY;
+
+ PsiClass[] psiClasses = null;
+ if (KotlinConfigKt.getPluginIsEnabled()) {
+ psiClasses = KotlinPsiClassesHandler.getPsiClasses(psiFile);
+ }
+
+ return psiClasses != null ? psiClasses : psiClassOwner.getClasses();
+ }
+
+ public abstract boolean processStepDefinition(StepDefinitionAnnotation stepDefinitionAnnotation);
+
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReference.java b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReference.java
new file mode 100644
index 00000000..0a7645c1
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReference.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.resolver;
+
+import static com.intellij.openapi.application.ReadAction.compute;
+
+import com.github.kumaraman21.intellijbehave.parser.JBehaveStep;
+import com.github.kumaraman21.intellijbehave.service.JBehaveStepsIndex;
+import com.github.kumaraman21.intellijbehave.service.JavaStepDefinition;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiPolyVariantReference;
+import com.intellij.psi.ResolveResult;
+import com.intellij.psi.impl.PsiManagerEx;
+import com.intellij.psi.impl.source.resolve.ResolveCache;
+import com.intellij.psi.impl.source.resolve.reference.impl.CachingReference;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * This reference extends {@link CachingReference}, so that resolved results are cached.
+ *
+ * This prevents other parts of the IntelliJ Platform, that constantly call {@code resolve()},
+ * to call {@link #resolveToDefinitions()} and in turn {@link JBehaveStepsIndex#findStepDefinitions(JBehaveStep)}
+ * over and over again.
+ */
+public class StepPsiReference extends CachingReference implements PsiPolyVariantReference {
+ private final JBehaveStep myStep;
+ private final TextRange myRange;
+
+ public StepPsiReference(JBehaveStep element, TextRange range) {
+ myStep = element;
+ myRange = range;
+ }
+
+ //Getters and handlers
+
+ @Override
+ public @NotNull JBehaveStep getElement() {
+ return myStep;
+ }
+
+ @Override
+ public @NotNull TextRange getRangeInElement() {
+ return myRange;
+ }
+
+ @NotNull
+ @Override
+ public String getCanonicalText() {
+ return myStep.getText();
+ }
+
+ @Override
+ public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException {
+ return myStep;
+ }
+
+ @Override
+ public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException {
+ return myStep;
+ }
+
+ @Override
+ public boolean isSoft() {
+ return false;
+ }
+
+ //Resolution
+
+ @Override
+ public boolean isReferenceTo(@NotNull PsiElement element) {
+ PsiManagerEx manager = null;
+
+ //This part is a simplified version of 'multiResolve()', and using it instead of 'multiResolve()',
+ // so that unnecessary calls to 'getAnnotatedMethod()' and instantiation of ResolveResults can be avoided.
+ var resolvedElements = new ArrayList(4);
+
+ for (var resolvedJavaStepDefinition : resolveToDefinitions()) {
+ final PsiMethod method = resolvedJavaStepDefinition.getAnnotatedMethod();
+ if (method != null && !resolvedElements.contains(method)) {
+ if (manager == null) manager = compute(() -> getElement().getManager());
+ if (manager.areElementsEquivalent(method, element)) {
+ return true;
+ }
+ resolvedElements.add(method);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the first Java step definition found for this reference.
+ */
+ @Nullable("When no definition could be resolved.")
+ public JavaStepDefinition resolveToDefinition() {
+ Collection definitions = resolveToDefinitions();
+ return definitions.isEmpty() ? null : definitions.iterator().next();
+ }
+
+ @NotNull
+ private Collection resolveToDefinitions() {
+ return JBehaveStepsIndex.getInstance(compute(myStep::getProject)).findStepDefinitions(myStep);
+ }
+
+ @Override
+ public @Nullable PsiElement resolveInner() {
+ var resolveResults = ResolveCache.getInstance(myStep.getProject())
+ .resolveWithCaching(this, DelegatingResolver.INSTANCE, false, false, myStep.getContainingFile());
+ return resolveResults.length > 0
+ ? resolveResults[0].getElement()
+ : null;
+ }
+
+ @Override
+ public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) {
+ var result = new ArrayList(4);
+ var resolvedElements = new ArrayList(4);
+
+ for (var resolvedJavaStepDefinition : resolveToDefinitions()) {
+ final PsiMethod method = resolvedJavaStepDefinition.getAnnotatedMethod();
+ if (method != null && !resolvedElements.contains(method)) {
+ resolvedElements.add(method);
+ result.add(new ResolveResult() {
+ public PsiElement getElement() {
+ return method;
+ }
+
+ public boolean isValidResult() {
+ return true;
+ }
+ });
+ }
+ }
+
+ return result.toArray(ResolveResult.EMPTY_ARRAY);
+ }
+
+ /**
+ * {@link ResolveCache#resolveWithCaching} calls this to resolve and cache the resolve results.
+ *
+ * This resolver delegates to {@link #multiResolve(boolean)}.
+ *
+ * Currently, it doesn't utilize the passed in {@link PsiFile} instance.
+ */
+ private static final class DelegatingResolver implements ResolveCache.PolyVariantContextResolver {
+ private static final DelegatingResolver INSTANCE = new DelegatingResolver();
+
+ @Override
+ public ResolveResult @NotNull [] resolve(@NotNull StepPsiReference ref, @NotNull PsiFile containingFile, boolean incompleteCode) {
+ return ref.multiResolve(false);
+ }
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReferenceContributor.java b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReferenceContributor.java
new file mode 100644
index 00000000..807edd1b
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReferenceContributor.java
@@ -0,0 +1,35 @@
+package com.github.kumaraman21.intellijbehave.resolver;
+
+import com.github.kumaraman21.intellijbehave.parser.JBehaveStep;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.PsiReferenceContributor;
+import com.intellij.psi.PsiReferenceProvider;
+import com.intellij.psi.PsiReferenceRegistrar;
+import com.intellij.util.ProcessingContext;
+import org.jetbrains.annotations.NotNull;
+
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+/**
+ * Provides references for JBehave steps in Story files.
+ *
+ * @see StepPsiReference
+ */
+public final class StepPsiReferenceContributor extends PsiReferenceContributor {
+
+ private static final PsiReferenceProvider REFERENCE_PROVIDER = new PsiReferenceProvider() {
+ @Override
+ public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
+ return element instanceof JBehaveStep step
+ ? new PsiReference[]{new StepPsiReference(step, TextRange.from(0, element.getTextLength()))}
+ : PsiReference.EMPTY_ARRAY;
+ }
+ };
+
+ @Override
+ public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
+ registrar.registerReferenceProvider(psiElement(JBehaveStep.class), REFERENCE_PROVIDER);
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/runner/RunStoryAction.java b/src/main/java/com/github/kumaraman21/intellijbehave/runner/RunStoryAction.java
new file mode 100644
index 00000000..5b8b6a94
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/runner/RunStoryAction.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.runner;
+
+import com.github.kumaraman21.intellijbehave.settings.JBehaveSettings;
+import com.intellij.execution.*;
+import com.intellij.execution.application.ApplicationConfiguration;
+import com.intellij.execution.executors.DefaultRunExecutor;
+import com.intellij.execution.impl.RunManagerImpl;
+import com.intellij.execution.impl.RunnerAndConfigurationSettingsImpl;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.execution.runners.ProgramRunner;
+import com.intellij.execution.util.ExecutionErrorDialog;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.PlatformDataKeys;
+import com.intellij.openapi.application.Application;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ProjectRootManager;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.search.GlobalSearchScope;
+import org.jetbrains.annotations.NotNull;
+
+import static com.github.kumaraman21.intellijbehave.runner.StoryRunnerConfigurationType.JBEHAVE_STORY_RUNNER;
+import static com.intellij.openapi.ui.Messages.getErrorIcon;
+import static com.intellij.openapi.ui.Messages.showMessageDialog;
+import static org.apache.commons.lang3.StringUtils.isBlank;
+
+public class RunStoryAction extends AnAction {
+
+ public void actionPerformed(@NotNull AnActionEvent e) {
+ Application application = ApplicationManager.getApplication();
+ JBehaveSettings jBehaveSettings = application.getService(JBehaveSettings.class);
+
+ String storyRunnerName = jBehaveSettings.getStoryRunner();
+ if (isBlank(storyRunnerName)) {
+ showMessageDialog("In order to run a story file you need to first set a main class in the JBehave settings.",
+ "No Main Class Found to Run the Story",
+ getErrorIcon());
+ return;
+ }
+
+ VirtualFile file = e.getData(PlatformDataKeys.VIRTUAL_FILE);
+ if (file == null) {
+ showMessageDialog("Select a file or focus on the story file in the editor to run it.",
+ "Story File not Selected",
+ getErrorIcon());
+ return;
+ }
+
+ Project project = e.getData(PlatformDataKeys.PROJECT);
+ final PsiClass storyRunnerClass = JavaPsiFacade.getInstance(project).findClass(storyRunnerName,
+ GlobalSearchScope.allScope(project));
+ if (storyRunnerClass == null) {
+ showMessageDialog("Could not find the specified main class ''" + storyRunnerName + "'.",
+ "Main Class not Found",
+ getErrorIcon());
+ return;
+ }
+
+ Module module = ProjectRootManager.getInstance(project).getFileIndex()
+ .getModuleForFile(storyRunnerClass.getContainingFile().getVirtualFile());
+ if (module == null) {
+ showMessageDialog("Could not find the module in which main class to run stories was defined.'" +
+ "/n Resetting the main class in the JBehave settings might fix this issue.",
+ "Module not Found For Main Class",
+ getErrorIcon()
+ );
+ return;
+ }
+
+ RunManagerImpl runManager = (RunManagerImpl) RunManager.getInstance(project);
+ RunnerAndConfigurationSettingsImpl runnerAndConfigurationSettings = findConfigurationByName(JBEHAVE_STORY_RUNNER, runManager);
+ ApplicationConfiguration conf = null;
+
+ if (runnerAndConfigurationSettings != null) {
+ conf = (ApplicationConfiguration) runnerAndConfigurationSettings.getConfiguration();
+ updateConfiguration(storyRunnerName, file, module, conf);
+ } else {
+ StoryRunnerConfigurationType type = application.getService(StoryRunnerConfigurationType.class);
+ runnerAndConfigurationSettings =
+ (RunnerAndConfigurationSettingsImpl) runManager.createConfiguration(JBEHAVE_STORY_RUNNER, type.getConfigurationFactories()[0]);
+ conf = (ApplicationConfiguration) runnerAndConfigurationSettings.getConfiguration();
+ updateConfiguration(storyRunnerName, file, module, conf);
+
+ runnerAndConfigurationSettings.storeInDotIdeaFolder();
+ runManager.addConfiguration(runnerAndConfigurationSettings);
+ }
+
+ runManager.setSelectedConfiguration(runnerAndConfigurationSettings);
+
+ Executor executor = DefaultRunExecutor.getRunExecutorInstance();
+ ProgramRunner runner = ProgramRunner.getRunner(executor.getId(), conf);
+ ExecutionEnvironment environment = new ExecutionEnvironment(executor, runner, runnerAndConfigurationSettings, project);
+
+ try {
+ runner.execute(environment);
+ } catch (ExecutionException e1) {
+ ExecutionErrorDialog.show(e1, "Error", project);
+ }
+ }
+
+ private RunnerAndConfigurationSettingsImpl findConfigurationByName(String name, RunManagerImpl runManager) {
+ for (RunnerAndConfigurationSettings settings : runManager.getAllSettings()) {
+ if (settings.getName().equals(name)) return (RunnerAndConfigurationSettingsImpl) settings;
+ }
+ return null;
+ }
+
+ private void updateConfiguration(String mainClassName, VirtualFile file, Module module, ApplicationConfiguration conf) {
+ conf.setMainClassName(mainClassName);
+ conf.setProgramParameters(file.getPath());
+ conf.setModule(module);
+ }
+}
diff --git a/src/com/github/kumaraman21/intellijbehave/runner/StoryRunnerConfigurationType.java b/src/main/java/com/github/kumaraman21/intellijbehave/runner/StoryRunnerConfigurationType.java
similarity index 59%
rename from src/com/github/kumaraman21/intellijbehave/runner/StoryRunnerConfigurationType.java
rename to src/main/java/com/github/kumaraman21/intellijbehave/runner/StoryRunnerConfigurationType.java
index a390b905..4af572fd 100644
--- a/src/com/github/kumaraman21/intellijbehave/runner/StoryRunnerConfigurationType.java
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/runner/StoryRunnerConfigurationType.java
@@ -15,26 +15,35 @@
*/
package com.github.kumaraman21.intellijbehave.runner;
+import com.github.kumaraman21.intellijbehave.language.JBehaveIcons;
import com.intellij.execution.application.ApplicationConfigurationType;
import org.jetbrains.annotations.NotNull;
+import javax.swing.Icon;
+
public class StoryRunnerConfigurationType extends ApplicationConfigurationType {
- public static final String JBEHAVE_STORY_RUNNER = "JBehave Story Runner";
+ public static final String JBEHAVE_STORY_RUNNER = "JBehave Story Runner";
+
+ @NotNull
+ @Override
+ public String getDisplayName() {
+ return JBEHAVE_STORY_RUNNER;
+ }
- @Override
- public String getDisplayName() {
- return JBEHAVE_STORY_RUNNER;
- }
+ @Override
+ public String getConfigurationTypeDescription() {
+ return "Runs a JBehave story file";
+ }
- @Override
- public String getConfigurationTypeDescription() {
- return "Runs a JBehave story file";
- }
+ @NotNull
+ @Override
+ public String getId() {
+ return "intellijbehave.storyrunner";
+ }
- @NotNull
- @Override
- public String getId() {
- return "intellijbehave.storyrunner";
- }
+ @Override
+ public Icon getIcon() {
+ return JBehaveIcons.JB;
+ }
}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaMethodUsageSearcher.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaMethodUsageSearcher.java
new file mode 100644
index 00000000..29c82bd1
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaMethodUsageSearcher.java
@@ -0,0 +1,39 @@
+package com.github.kumaraman21.intellijbehave.service;
+
+import static com.github.kumaraman21.intellijbehave.service.JBehaveUtil.getAnnotationTexts;
+import static com.github.kumaraman21.intellijbehave.service.JBehaveUtil.getTheBiggestWordToSearchByIndex;
+import static com.intellij.openapi.util.text.StringUtil.isNotEmpty;
+
+import com.github.kumaraman21.intellijbehave.language.StoryFileType;
+import com.intellij.openapi.application.QueryExecutorBase;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.searches.MethodReferencesSearch.SearchParameters;
+import com.intellij.psi.search.searches.ReferencesSearch;
+import com.intellij.util.Processor;
+import com.intellij.util.Query;
+import org.jetbrains.annotations.NotNull;
+
+public class JBehaveJavaMethodUsageSearcher extends QueryExecutorBase {
+
+ @Override
+ public void processQuery(@NotNull SearchParameters queryParameters, @NotNull Processor super PsiReference> consumer) {
+ if (queryParameters.getScopeDeterminedByUser() instanceof GlobalSearchScope scopeByUser) {
+ final PsiMethod method = queryParameters.getMethod();
+
+ boolean hasNonEmptyBiggestWord = ReadAction.compute(() -> getAnnotationTexts(method))
+ .stream()
+ .anyMatch(stepText -> isNotEmpty(getTheBiggestWordToSearchByIndex(stepText)));
+
+ //Originally, this was executed for each non-empty biggest word, but since it performed the same reference search query, it became deduplicated.
+ if (hasNonEmptyBiggestWord) {
+ var restrictedScope = GlobalSearchScope.getScopeRestrictedByFileTypes(scopeByUser, StoryFileType.STORY_FILE_TYPE);
+ Query query = ReferencesSearch.search(new ReferencesSearch.SearchParameters(method, restrictedScope, false, queryParameters.getOptimizer()));
+ //This, behind the scenes, will also call into JBehaveJavaStepDefinitionSearch#execute
+ query.forEach(consumer);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaStepDefinitionSearch.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaStepDefinitionSearch.java
new file mode 100644
index 00000000..f246862f
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaStepDefinitionSearch.java
@@ -0,0 +1,45 @@
+package com.github.kumaraman21.intellijbehave.service;
+
+import static com.github.kumaraman21.intellijbehave.service.JBehaveUtil.findJBehaveReferencesToElement;
+import static com.github.kumaraman21.intellijbehave.service.JBehaveUtil.getAnnotationTexts;
+import static com.github.kumaraman21.intellijbehave.service.JBehaveUtil.isStepDefinition;
+import static com.intellij.openapi.application.ReadAction.compute;
+
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.search.SearchScope;
+import com.intellij.psi.search.searches.ReferencesSearch.SearchParameters;
+import com.intellij.util.Processor;
+import com.intellij.util.QueryExecutor;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Provides Story step references for JBehave Java step definition methods.
+ */
+public class JBehaveJavaStepDefinitionSearch implements QueryExecutor {
+
+ @Override
+ public boolean execute(@NotNull SearchParameters queryParameters, @NotNull Processor super PsiReference> consumer) {
+ if (!(queryParameters.getElementToSearch() instanceof PsiMethod method) || !isStepDefinition(method)) {
+ return true;
+ }
+
+ SearchScope searchScope = null;
+ boolean result = true;
+
+ for (String stepText : compute(() -> getAnnotationTexts(method))) {
+ if (stepText == null) {
+ return true;
+ }
+
+ //Lazy-initializing the search scope in case the first step text is null
+ if (searchScope == null) {
+ searchScope = JBehaveUtil.restrictScopeToJBehaveFiles(compute(queryParameters::getEffectiveSearchScope));
+ }
+
+ result &= findJBehaveReferencesToElement(method, stepText, consumer, searchScope);
+ }
+
+ return result;
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java
new file mode 100644
index 00000000..dc1c23ef
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java
@@ -0,0 +1,140 @@
+package com.github.kumaraman21.intellijbehave.service;
+
+import static com.intellij.openapi.application.ReadAction.compute;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+
+import com.github.kumaraman21.intellijbehave.JBehaveStepDefClassesModificationTracker;
+import com.github.kumaraman21.intellijbehave.kotlin.KotlinConfigKt;
+import com.github.kumaraman21.intellijbehave.kotlin.support.services.KotlinAnnotationsLoader;
+import com.github.kumaraman21.intellijbehave.parser.JBehaveStep;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.components.Service;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleUtilCore;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ProjectRootModificationTracker;
+import com.intellij.openapi.util.Key;
+import com.intellij.psi.PsiAnnotation;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.impl.java.stubs.index.JavaAnnotationIndex;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.util.CachedValueProvider;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.psi.util.QualifiedName;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Project service that provides Java step definitions for JBehave Story steps.
+ */
+@Service(Service.Level.PROJECT)
+public final class JBehaveStepsIndex implements Disposable {
+
+ //Argument is necessary for project-level service creation
+ public JBehaveStepsIndex(Project project) {
+ }
+
+ public static JBehaveStepsIndex getInstance(Project project) {
+ return project.getService(JBehaveStepsIndex.class);
+ }
+
+ @NotNull
+ public Collection findStepDefinitions(@NotNull JBehaveStep step) {
+ return ReadAction.compute(() -> CachedValuesManager.getCachedValue(step, Key.create(step.getStepText()), (CachedValueProvider extends Collection>) () -> {
+ Module module = ModuleUtilCore.findModuleForPsiElement(step);
+
+ if (module == null) {
+ return new CachedValueProvider.Result<>(
+ emptyList(),
+ JBehaveStepDefClassesModificationTracker.getInstance(step.getProject()),
+ ProjectRootModificationTracker.getInstance(step.getProject()));
+ }
+
+ var definitionsByClass = new HashMap(2);
+ String stepText = step.getStepText();
+
+ for (var javaStepDefinition : loadStepsFor(module)) {
+ if (javaStepDefinition.supportsStepAndMatches(step, stepText)) {
+ Integer currentHighestPriority = getPriorityByDefinition(definitionsByClass.get(javaStepDefinition.getClass()));
+ Integer newPriority = getPriorityByDefinition(javaStepDefinition);
+
+ if (newPriority > currentHighestPriority) {
+ definitionsByClass.put(javaStepDefinition.getClass(), javaStepDefinition);
+ }
+ }
+ }
+
+ return new CachedValueProvider.Result<>(definitionsByClass.values(),
+ JBehaveStepDefClassesModificationTracker.getInstance(step.getProject()),
+ ProjectRootModificationTracker.getInstance(step.getProject()));
+ }));
+ }
+
+ @NotNull
+ private List loadStepsFor(@NotNull Module module) {
+ GlobalSearchScope dependenciesScope = module.getModuleWithDependenciesAndLibrariesScope(true);
+
+ var stepAnnotations = StepAnnotationsCache.getInstance(module.getProject()).cacheStepAnnotations(module, dependenciesScope);
+ if (stepAnnotations.isAnyAnnotationMissing())
+ return emptyList();
+
+ var javaStepDefs = new ArrayList();
+
+ for (PsiClass stepAnnotation : asList(stepAnnotations.given(), stepAnnotations.when(), stepAnnotations.then())) {
+ for (PsiAnnotation stepDefAnnotation : getAllStepAnnotations(stepAnnotation, dependenciesScope)) {
+ javaStepDefs.add(new JavaStepDefinition(stepDefAnnotation));
+ }
+ }
+
+ return javaStepDefs;
+ }
+
+ @NotNull
+ private static Integer getPriorityByDefinition(@Nullable JavaStepDefinition definition) {
+ return definition != null ? definition.getAnnotationPriority() : -1;
+ }
+
+ /**
+ * Collects all {@link PsiAnnotation}s in the given {@code scope} that reference the {@code annClass}.
+ *
+ * @param annClass the PsiClass representing the {@code @Given}, {@code @When} or {@code @Then} step annotations.
+ * Since the annotation classes doesn't change much, unless e.g. updating the library version,
+ * the same PsiClass instance will be available for the same annotations throughout the IDE session,
+ * thus it should be safe to use it as the cache location
+ */
+ @NotNull
+ @VisibleForTesting
+ static Collection getAllStepAnnotations(@NotNull final PsiClass annClass, @NotNull final GlobalSearchScope scope) {
+ return CachedValuesManager.getCachedValue(annClass, (CachedValueProvider extends Collection>) () -> {
+ Collection annotations = compute(() -> {
+ Project project = annClass.getProject();
+ Collection psiAnnotations = new ArrayList<>();
+ if (KotlinConfigKt.getPluginIsEnabled()) {
+ String annotationFqn = annClass.getQualifiedName();
+ if (annotationFqn != null) {
+ psiAnnotations.addAll(KotlinAnnotationsLoader.getAnnotations(QualifiedName.fromDottedString(annotationFqn), project, scope));
+ }
+ }
+ psiAnnotations.addAll(JavaAnnotationIndex.getInstance().getAnnotations(annClass.getName(), project, scope));
+ return psiAnnotations;
+ });
+
+ return new CachedValueProvider.Result<>(annotations,
+ JBehaveStepDefClassesModificationTracker.getInstance(annClass.getProject()),
+ ProjectRootModificationTracker.getInstance(annClass.getProject()));
+ });
+ }
+
+ @Override
+ public void dispose() {
+ //No-op. Used for ProjectStartupActivity as parent Disposable
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java
new file mode 100644
index 00000000..15941434
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java
@@ -0,0 +1,304 @@
+package com.github.kumaraman21.intellijbehave.service;
+
+import static com.intellij.openapi.application.ReadAction.compute;
+import static com.intellij.openapi.util.text.StringUtil.isEmptyOrSpaces;
+
+import com.github.kumaraman21.intellijbehave.jbehave.core.steps.PatternVariantBuilder;
+import com.github.kumaraman21.intellijbehave.language.StoryFileType;
+import com.intellij.codeInsight.AnnotationUtil;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiAnnotation;
+import com.intellij.psi.PsiAnnotationMemberValue;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.PsiSearchHelper;
+import com.intellij.psi.search.SearchScope;
+import com.intellij.psi.search.TextOccurenceProcessor;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.Processor;
+import org.jbehave.core.annotations.Alias;
+import org.jbehave.core.annotations.Aliases;
+import org.jbehave.core.annotations.Given;
+import org.jbehave.core.annotations.Then;
+import org.jbehave.core.annotations.When;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.annotation.Annotation;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public final class JBehaveUtil {
+
+ private static final Set JBEHAVE_ANNOTATIONS_SET = Set.of(Given.class.getName(), When.class.getName(),
+ Then.class.getName());
+
+ /**
+ * Returns if the provided annotation is one of {@link #JBEHAVE_ANNOTATIONS_SET}.
+ */
+ public static boolean isJBehaveStepAnnotation(@NotNull PsiAnnotation annotation) {
+ String annotationName = compute(annotation::getQualifiedName);
+
+ return annotationName != null && JBEHAVE_ANNOTATIONS_SET.contains(annotationName);
+ }
+
+ /**
+ * Returns if the provided annotation is of type {@code annotationClass}.
+ *
+ * For example, if the {@code @org.jbehave.core.annotations.Given} annotation is of type {@link Given}.
+ */
+ public static boolean isAnnotationOfClass(@NotNull PsiAnnotation annotation,
+ @NotNull Class extends Annotation> annotationClass) {
+ String annotationName = compute(annotation::getQualifiedName);
+
+ return annotationName != null && annotationName.equals(annotationClass.getName());
+ }
+
+ /**
+ * Returns all {@code @Given}, {@code @When} and {@code @Then} step annotations on the provided {@code method}.
+ */
+ @NotNull
+ private static List getJBehaveStepAnnotations(@NotNull PsiMethod method) {
+ var annotations = compute(() -> method.getModifierList().getAnnotations());
+
+ //Optimizations to avoid creating unnecessary Streams
+ if (annotations.length == 0) return Collections.emptyList();
+ if (annotations.length == 1 && JBehaveUtil.isJBehaveStepAnnotation(annotations[0])) return Collections.singletonList(annotations[0]);
+
+ return Stream.of(annotations)
+ .filter(JBehaveUtil::isJBehaveStepAnnotation)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Returns if the provided method is step definition, meaning has at least one annotation that
+ *
+ *
is annotated with one of {@link #JBEHAVE_ANNOTATIONS_SET},
+ *
and its {@code value} attribute is not null.
+ *
+ */
+ public static boolean isStepDefinition(@NotNull PsiMethod method) {
+ return ReadAction.compute(() -> {
+ var jBehaveStepAnnotations = getJBehaveStepAnnotations(method);
+
+ //Optimizations to avoid creating unnecessary Streams
+ if (jBehaveStepAnnotations.isEmpty()) return false;
+ if (jBehaveStepAnnotations.size() == 1) {
+ var attributeValue = compute(() -> jBehaveStepAnnotations.getFirst().findAttributeValue("value"));
+ return attributeValue != null;
+ } else {
+ return jBehaveStepAnnotations.stream()
+ .map(stepAnnotation -> compute(() -> stepAnnotation.findAttributeValue("value")))
+ .anyMatch(Objects::nonNull);
+ }
+ });
+ }
+
+ /**
+ * Collects all possible step annotation pattern variants for {@code stepAnnotation},
+ * as well as the {@link Alias} and {@link Aliases} annotations on the parent
+ * step definition method.
+ *
+ * @param stepAnnotation annotation for one of {@link #JBEHAVE_ANNOTATIONS_SET}
+ * @return the collection of step patterns
+ */
+ @NotNull
+ public static Set getAnnotationTexts(@NotNull PsiAnnotation stepAnnotation, @Nullable PsiMethod parentMethod) {
+ var annotationTexts = new HashSet(4);
+ getAnnotationText(stepAnnotation).ifPresent(annotationTexts::add);
+
+ //If the parent method is available, e.g. from JBehaveJavaStepDefinitionSearch, then use that, otherwise compute it
+ PsiMethod method = parentMethod != null ? parentMethod : compute(() -> PsiTreeUtil.getParentOfType(stepAnnotation, PsiMethod.class));
+ if (method != null) {
+ for (PsiAnnotation annotation : compute(() -> method.getModifierList().getAnnotations())) {
+ if (isAnnotationOfClass(annotation, Alias.class)) {
+ getAnnotationText(annotation).ifPresent(annotationTexts::add);
+ } else if (isAnnotationOfClass(annotation, Aliases.class)) {
+ annotationTexts.addAll(getAliasesAnnotationTexts(annotation));
+ }
+ }
+ }
+
+ return annotationTexts.stream()
+ .map(PatternVariantBuilder::new)
+ .flatMap(builder -> builder.allVariants().stream())
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Returns the value of {@code annotation}'s {@code value} attribute, the step pattern.
+ *
+ * @param annotation a JBehave annotation: Given, When, Then, Alias or Aliases
+ */
+ private static Optional getAnnotationText(@NotNull PsiAnnotation annotation) {
+ return Optional.ofNullable(compute(() -> AnnotationUtil.getStringAttributeValue(annotation, "value")));
+ }
+
+ @NotNull
+ private static Set getAliasesAnnotationTexts(@NotNull PsiAnnotation aliasAnnotation) {
+ return compute(() -> AnnotationUtil.arrayAttributeValues(aliasAnnotation.findAttributeValue("values")))
+ .stream()
+ .map(attr -> compute(() -> AnnotationUtil.getStringAttributeValue(attr)))
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Collects all possible step annotation pattern variants for the annotations on {@code method}.
+ *
+ * @param method the step definition method
+ * @return the list of step patterns
+ */
+ @NotNull
+ public static List getAnnotationTexts(@NotNull PsiMethod method) {
+ var annotations = getJBehaveStepAnnotations(method);
+
+ //Optimization to avoid creating unnecessary Streams
+ if (annotations.isEmpty()) return Collections.emptyList();
+
+ return annotations
+ .stream()
+ .map(annotation -> JBehaveUtil.getAnnotationTexts(annotation, method))
+ .flatMap(Set::stream)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Returns the value of {@code stepAnnotation}'s {@code priority} attribute, or -1 if the
+ * priority is not an integer.
+ */
+ @NotNull
+ public static Integer getAnnotationPriority(@NotNull PsiAnnotation stepAnnotation) {
+ PsiAnnotationMemberValue attrValue = compute(() -> stepAnnotation.findAttributeValue("priority"));
+ // TODO test change doesn't break other languages; this change works as a quick fix for Kotlin
+ //Object constValue = JavaPsiFacade.getInstance(stepAnnotation.getProject()).getConstantEvaluationHelper().computeConstantExpression(attrValue);
+ Object constValue = compute(() -> JavaPsiFacade.getInstance(compute(stepAnnotation::getProject))
+ .getConstantEvaluationHelper()
+ .computeConstantExpression(attrValue.getOriginalElement()));
+ Integer priority = constValue instanceof Integer ? (Integer) constValue : null;
+
+ return priority != null ? priority : -1;
+ }
+
+ /**
+ * Performs word based reference search for {@code stepDefinitionElement}.
+ *
+ * @param stepDefinitionElement a step definition method
+ * @param stepText the step pattern value of the step annotation
+ * @param consumer
+ * @param searchScope the search scope to find references in. Already restricted to JBehave Story files.
+ * See {@link JBehaveJavaStepDefinitionSearch}.
+ * @return true if the corresponding query execution in {@link JBehaveJavaStepDefinitionSearch} should continue,
+ * false if it should stop
+ */
+ public static boolean findJBehaveReferencesToElement(@NotNull PsiElement stepDefinitionElement,
+ @NotNull String stepText,
+ @NotNull Processor super PsiReference> consumer,
+ @NotNull final SearchScope searchScope) {
+ String word = getTheBiggestWordToSearchByIndex(stepText);
+
+ return isEmptyOrSpaces(word)
+ || PsiSearchHelper.getInstance(compute(stepDefinitionElement::getProject))
+ .processElementsWithWord(new MyReferenceCheckingProcessor(stepDefinitionElement, consumer), searchScope, word, (short) 5, true);
+ }
+
+ /**
+ * Returns a search scope that is based on the {@code originalScopeComputation} but that is restricted to JBehave Story file types.
+ */
+ public static SearchScope restrictScopeToJBehaveFiles(final SearchScope originalScope) {
+ return compute(() ->
+ originalScope instanceof GlobalSearchScope globalSearchScope
+ ? GlobalSearchScope.getScopeRestrictedByFileTypes(globalSearchScope, StoryFileType.STORY_FILE_TYPE)
+ : originalScope);
+ }
+
+ /**
+ * Returns the longest non-placeholder (e.g. {@code $price}) word from {@code stepText}.
+ *
+ * @param stepText the step pattern value from a step annotation
+ */
+ public static String getTheBiggestWordToSearchByIndex(@NotNull String stepText) {
+ //This helps avoid the creation of the variables below in case of empty and blank step text
+ if (stepText.isBlank()) return "";
+
+ String result = "";
+
+ int par = 0;
+ StringBuilder sb = new StringBuilder();
+
+ for (int i = 0; i < stepText.length(); ++i) {
+ char c = stepText.charAt(i);
+
+ if (c == '$') {
+ ++par;
+ }
+
+ if (c == ' ' && par > 0) {
+ --par;
+ }
+
+ if (par > 0) {
+ if (par == 1) {
+ sb = new StringBuilder();
+ }
+ } else if (Character.isLetterOrDigit(c)) {
+ sb.append(c);
+ if (sb.length() > 0 && sb.toString().length() > result.length()) {
+ result = sb.toString();
+ }
+ } else {
+ sb = new StringBuilder();
+ }
+ }
+
+ if (sb.length() > 0 && sb.toString().length() > result.length()) {
+ result = sb.toString();
+ }
+
+ return result;
+ }
+
+ private static final class MyReferenceCheckingProcessor implements TextOccurenceProcessor {
+ @NotNull
+ private final PsiElement myElementToFind;
+ @NotNull
+ private final Processor super PsiReference> myConsumer;
+
+ private MyReferenceCheckingProcessor(@NotNull PsiElement elementToFind, @NotNull Processor super PsiReference> consumer) {
+ myElementToFind = elementToFind;
+ myConsumer = consumer;
+ }
+
+ @Override
+ public boolean execute(@NotNull PsiElement element, int offsetInElement) {
+ boolean result = executeInternal(element);
+ //This avoids calculating the parent element when result is false
+ if (result) {
+ PsiElement parent = element.getParent();
+ return parent == null || executeInternal(parent);
+ }
+ return false;
+ }
+
+ private boolean executeInternal(@NotNull PsiElement referenceOwner) {
+ for (PsiReference ref : referenceOwner.getReferences()) {
+ if (ref != null && ref.isReferenceTo(myElementToFind) && !myConsumer.process(ref)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ private JBehaveUtil() {
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinition.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinition.java
new file mode 100644
index 00000000..5e8c83ca
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinition.java
@@ -0,0 +1,138 @@
+package com.github.kumaraman21.intellijbehave.service;
+
+import static com.github.kumaraman21.intellijbehave.utility.StepTypeMappings.ANNOTATION_TO_STEP_TYPE_MAPPING;
+import static com.intellij.openapi.application.ReadAction.compute;
+
+import com.github.kumaraman21.intellijbehave.parser.JBehaveStep;
+import com.intellij.psi.PsiAnnotation;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.SmartPointerManager;
+import com.intellij.psi.SmartPsiElementPointer;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jbehave.core.parsers.RegexPrefixCapturingPatternParser;
+import org.jbehave.core.parsers.StepPatternParser;
+import org.jbehave.core.steps.StepType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Represents a Java step definition as a step annotation.
+ */
+public final class JavaStepDefinition {
+ private static final StepPatternParser STEP_PATTERN_PARSER = new RegexPrefixCapturingPatternParser();
+ private final SmartPsiElementPointer annotationPointer;
+
+ public JavaStepDefinition(PsiAnnotation annotation) {
+ annotationPointer = compute(() -> SmartPointerManager.getInstance(annotation.getProject()).createSmartPsiElementPointer(annotation));
+ }
+
+ /**
+ * Returns if the step type of the provided Story step is the same as the step
+ * type of the current annotation, and if any of the step annotation patterns match
+ * the provided Story step text.
+ *
+ * @param step a step from a Story file
+ * @param stepText the text of a step in a Story file
+ */
+ public boolean supportsStepAndMatches(@NotNull JBehaveStep step, String stepText) {
+ StepType annotationType = getAnnotationType();
+ if (!Objects.equals(step.getStepType(), annotationType)) return false;
+
+ for (String annotationText : getAnnotationTexts()) {
+ if (new OptimizedStepMatcher(STEP_PATTERN_PARSER.parseStep(annotationType, annotationText)).matches(stepText))
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the step pattern from the current annotation if it matches the Story {@code stepText}.
+ *
+ * If there is only one step text without any text variant, it returns that pattern.
+ * Otherwise, it returns the first annotation text that matches {@code stepText}.
+ *
+ * @param stepText the text of a step in a Story file
+ */
+ @Nullable("When there is no annotation text.")
+ public String getAnnotationTextFor(String stepText) {
+ Set annotationTexts = getAnnotationTexts();
+
+ if (annotationTexts.size() == 1) {//small optimization: it doesn't create matchers if no step variants found
+ return annotationTexts.iterator().next();
+ }
+
+ final StepType annotationType = getAnnotationType();
+ for (String annotationText : annotationTexts) {
+ OptimizedStepMatcher stepMatcher = new OptimizedStepMatcher(STEP_PATTERN_PARSER.parseStep(annotationType, annotationText));
+ if (stepMatcher.matches(stepText)) {
+ return stepMatcher.pattern().annotated();
+ }
+ }
+
+ return null;
+ }
+
+ @Nullable
+ private PsiAnnotation getAnnotation() {
+ return annotationPointer.getElement();
+ }
+
+ /**
+ * Returns the method that the current annotation is placed on.
+ */
+ @Nullable
+ public PsiMethod getAnnotatedMethod() {
+ return compute(() -> PsiTreeUtil.getParentOfType(getAnnotation(), PsiMethod.class));
+ }
+
+ @NotNull
+ private Set getAnnotationTexts() {
+ PsiAnnotation annotation = getAnnotation();
+
+ return annotation == null ? Collections.emptySet() : JBehaveUtil.getAnnotationTexts(annotation, null);
+ }
+
+ /**
+ * Returns the {@link StepType} mapped to the current annotation if it is one of {@code @Given},
+ * {@code @When} or {@code @Then}.
+ */
+ @Nullable("When there is no step annotation to work with, or the annotation has no StepType mapped.")
+ public StepType getAnnotationType() {
+ final PsiAnnotation annotation = getAnnotation();
+ return annotation == null
+ ? null
+ : ANNOTATION_TO_STEP_TYPE_MAPPING.get(compute(annotation::getQualifiedName));
+ }
+
+ /**
+ * Returns the value of the {@code priority} attribute of the current annotation.
+ */
+ @NotNull
+ public Integer getAnnotationPriority() {
+ PsiAnnotation annotation = getAnnotation();
+
+ return annotation != null ? JBehaveUtil.getAnnotationPriority(annotation) : -1;
+ }
+
+ //Equals and hashcode
+
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ } else if (o != null && getClass() == o.getClass()) {
+ JavaStepDefinition that = (JavaStepDefinition) o;
+ return annotationPointer.equals(that.annotationPointer);
+ } else {
+ return false;
+ }
+ }
+
+ public int hashCode() {
+ return annotationPointer.hashCode();
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/OptimizedStepMatcher.kt b/src/main/java/com/github/kumaraman21/intellijbehave/service/OptimizedStepMatcher.kt
new file mode 100644
index 00000000..614d9da9
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/OptimizedStepMatcher.kt
@@ -0,0 +1,63 @@
+@file:Suppress("NOTHING_TO_INLINE")
+
+package com.github.kumaraman21.intellijbehave.service
+
+import org.jbehave.core.parsers.StepMatcher
+
+/**
+ * Created by Rodrigo Quesada on 04/10/16.
+ */
+private object StepPatterns {
+ object Plain {
+ const val VARIABLE = "(.*)"
+ }
+
+ object Regex {
+ val SPACE = Regex("\\s+")
+ }
+}
+
+internal class OptimizedStepMatcher(stepMatcher: StepMatcher) : StepMatcher by stepMatcher {
+
+ private val matchesRegexes: List
+
+ init {
+ val patterns = stepMatcher.pattern().resolved().split(StepPatterns.Plain.VARIABLE)
+ fun List.toRegexes() = asSequence()
+ //Replaces sequences of spaces with single \s symbols. This is in line with the trimming
+ // of space in the 'matches()' function below.
+ .map { it.replace(StepPatterns.Regex.SPACE, "\\s") }
+ .map(::Regex)
+ .toList()
+
+ matchesRegexes = patterns.toRegexes().mapIndexed { i, regex ->
+ when (i) {
+ 0 -> Regex("^${regex.pattern}")
+ patterns.lastIndex -> Regex("${regex.pattern}$")
+ else -> regex
+ }
+ }
+ }
+
+ //TODO implement catching optimization strategy
+
+ fun matches(text: String): Boolean = text.trimSpaces().find()
+
+ private fun String.find(textIndex: Int = 0, regexIndex: Int = 0): Boolean {
+ return if (regexIndex == matchesRegexes.size) true
+ else if (textIndex < length) doFind(textIndex, regexIndex)
+ // This helps to solve the case when the parameter is right at the end of the step pattern,
+ // e.g. '@When("a user$thing")' and the parameter is passed an empty value.
+ // In this case the remaining regex pattern would be a single $ sign which has to be matched against an empty string.
+ else textIndex == length
+ }
+
+ private fun String.doFind(textIndex: Int, regexIndex: Int): Boolean {
+ val matchResult = matchesRegexes[regexIndex].find(this, textIndex)
+ return matchResult != null && find(matchResult.range.last + 1, regexIndex + 1)
+ }
+}
+
+//region Utils
+private inline fun String.trimSpaces() = replace(StepPatterns.Regex.SPACE, " ")
+//endregion
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/StepAnnotationsCache.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/StepAnnotationsCache.java
new file mode 100644
index 00000000..d624a89d
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/StepAnnotationsCache.java
@@ -0,0 +1,75 @@
+package com.github.kumaraman21.intellijbehave.service;
+
+import com.intellij.openapi.components.Service;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.impl.java.stubs.index.JavaFullClassNameIndex;
+import com.intellij.psi.search.GlobalSearchScope;
+import org.jbehave.core.annotations.Given;
+import org.jbehave.core.annotations.Then;
+import org.jbehave.core.annotations.When;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Caches the {@link PsiClass} instances of the {@link Given}, {@link When} and {@link Then} classes
+ * for each module in the project.
+ */
+@Service(Service.Level.PROJECT)
+final class StepAnnotationsCache {
+
+ //TODO: clean up stale module entries upon project root changes or module root additions/removals
+ private final Map stepAnnotationClasses = new ConcurrentHashMap<>(4);
+ private final Project project;
+
+ public StepAnnotationsCache(Project project) {
+ this.project = project;
+ }
+
+ /**
+ * Caches the Given-When-Then annotations' PsiClass instances for the provided module if they haven't been.
+ *
+ * @return the cached PsiClass instances wrapped in a {@link StepAnnotations}.
+ */
+ public StepAnnotations cacheStepAnnotations(Module module, GlobalSearchScope dependenciesScope) {
+ return stepAnnotationClasses.computeIfAbsent(module, __ -> new StepAnnotations(
+ findStepAnnotation(Given.class.getName(), dependenciesScope),
+ findStepAnnotation(When.class.getName(), dependenciesScope),
+ findStepAnnotation(Then.class.getName(), dependenciesScope)
+ ));
+ }
+
+ /**
+ * Finds the {@code PsiClass} instance of the {@code stepAnnotationClassFqn}.
+ *
+ * @param stepAnnotationClassFqn the fully qualified name of the step annotation to find
+ * @param dependenciesScope the dependencies scope within the current module. See {@link JBehaveStepsIndex#loadStepsFor(Module)}.
+ * @return the PsiClass instance for the step annotation, or null if not found
+ */
+ @Nullable("When there is no annotation class found.")
+ private PsiClass findStepAnnotation(String stepAnnotationClassFqn, GlobalSearchScope dependenciesScope) {
+ var stepDefAnnotationCandidates = JavaFullClassNameIndex.getInstance().getClasses(stepAnnotationClassFqn, project, dependenciesScope);
+ for (PsiClass stepDefAnnotations : stepDefAnnotationCandidates) {
+ if (stepAnnotationClassFqn.equals(stepDefAnnotations.getQualifiedName())) {
+ return stepDefAnnotations;
+ }
+ }
+ return null;
+ }
+
+ public static StepAnnotationsCache getInstance(Project project) {
+ return project.getService(StepAnnotationsCache.class);
+ }
+
+ /**
+ * Wrapper class for the Given-When-Then annotation classes for easier caching and handling.
+ */
+ record StepAnnotations(@Nullable PsiClass given, @Nullable PsiClass when, @Nullable PsiClass then) {
+ boolean isAnyAnnotationMissing() {
+ return given == null || when == null || then == null;
+ }
+ }
+}
diff --git a/src/com/github/kumaraman21/intellijbehave/settings/JBehaveConfigurable.java b/src/main/java/com/github/kumaraman21/intellijbehave/settings/JBehaveConfigurable.java
similarity index 97%
rename from src/com/github/kumaraman21/intellijbehave/settings/JBehaveConfigurable.java
rename to src/main/java/com/github/kumaraman21/intellijbehave/settings/JBehaveConfigurable.java
index 9bef0c3d..88a3f7f2 100644
--- a/src/com/github/kumaraman21/intellijbehave/settings/JBehaveConfigurable.java
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/settings/JBehaveConfigurable.java
@@ -42,11 +42,6 @@ public String getDisplayName() {
return "JBehave";
}
- @Override
- public Icon getIcon() {
- return null;
- }
-
@Override
public String getHelpTopic() {
return "reference.settingsdialog.project.jbehave";
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/settings/JBehaveSettings.java b/src/main/java/com/github/kumaraman21/intellijbehave/settings/JBehaveSettings.java
new file mode 100644
index 00000000..2ad1cb34
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/settings/JBehaveSettings.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.settings;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.components.*;
+import com.intellij.util.xmlb.XmlSerializerUtil;
+import org.jetbrains.annotations.NotNull;
+
+@State(
+ name = "JBehaveSettings",
+ storages = @Storage("$APP_CONFIG$/jbehave_settings.xml")
+)
+public class JBehaveSettings implements PersistentStateComponent {
+
+ private String storyRunner;
+ private boolean storyAutoCompletion;
+
+ public static JBehaveSettings getInstance() {
+ return ApplicationManager.getApplication().getService(JBehaveSettings.class);
+ }
+
+ @Override
+ public JBehaveSettings getState() {
+ return this;
+ }
+
+ @Override
+ public void loadState(@NotNull JBehaveSettings jBehaveSettings) {
+ XmlSerializerUtil.copyBean(jBehaveSettings, this);
+ }
+
+ public String getStoryRunner() {
+ return storyRunner;
+ }
+
+ public void setStoryRunner(String storyRunner) {
+ this.storyRunner = storyRunner;
+ }
+
+ public boolean isStoryAutoCompletion() {
+ return storyAutoCompletion;
+ }
+
+ public void setStoryAutoCompletion(boolean storyAutoCompletion) {
+ this.storyAutoCompletion = storyAutoCompletion;
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/settings/JBehaveSettingsPanel.java b/src/main/java/com/github/kumaraman21/intellijbehave/settings/JBehaveSettingsPanel.java
new file mode 100644
index 00000000..93ff9ead
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/settings/JBehaveSettingsPanel.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.settings;
+
+import com.intellij.execution.configurations.ConfigurationUtil;
+import com.intellij.ide.DataManager;
+import com.intellij.ide.util.ClassFilter;
+import com.intellij.ide.util.TreeJavaClassChooserDialog;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.ui.TextFieldWithBrowseButton;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.util.PsiMethodUtil;
+import com.intellij.util.ui.FormBuilder;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.Objects;
+
+public final class JBehaveSettingsPanel {
+ private final JPanel contentPane;
+ private final JLabel storyRunnerLabel;
+ private final TextFieldWithBrowseButton storyRunnerField;
+ private final JCheckBox enableCodeCompletionCheckBox;
+
+ private final JBehaveSettings jBehaveSettings;
+
+ public JBehaveSettingsPanel() {
+ jBehaveSettings = JBehaveSettings.getInstance();
+
+ storyRunnerLabel = new JLabel();
+ storyRunnerLabel.setText("Main class for running stories:");
+ storyRunnerLabel.setToolTipText("Class with a main function to receive the story file to run as a parameter");
+ storyRunnerField = new TextFieldWithBrowseButton();
+ storyRunnerField.addActionListener(new BrowseMainClassListener(storyRunnerField));
+
+ enableCodeCompletionCheckBox = new JCheckBox("Enable code completion in *.story files");
+
+ contentPane = buildSettingsPanel();
+ }
+
+ public void apply() {
+ jBehaveSettings.setStoryRunner(storyRunnerField.getText());
+ jBehaveSettings.setStoryAutoCompletion(enableCodeCompletionCheckBox.isSelected());
+ }
+
+ public void reset() {
+ storyRunnerField.setText(jBehaveSettings.getStoryRunner());
+ enableCodeCompletionCheckBox.setSelected(jBehaveSettings.isStoryAutoCompletion());
+ }
+
+ public boolean isModified() {
+ boolean storyRunnerChanged = !storyRunnerField.getText().equals(jBehaveSettings.getStoryRunner());
+ boolean storyCodeCompletionChanged = !Objects.equals(enableCodeCompletionCheckBox.isSelected(), jBehaveSettings.isStoryAutoCompletion());
+ return storyRunnerChanged || storyCodeCompletionChanged;
+ }
+
+ public JPanel getContentPane() {
+ return this.contentPane;
+ }
+
+ private JPanel buildSettingsPanel() {
+ return FormBuilder.createFormBuilder()
+ .addLabeledComponent(storyRunnerLabel, storyRunnerField)
+ .addVerticalGap(5)
+ .addComponent(enableCodeCompletionCheckBox)
+ .addComponentFillVertically(new JPanel(), 0)
+ .getPanel();
+ }
+
+ /**
+ * Shows a dialog in which the user can browse and select a class as the main class.
+ *
+ * The qualified name of the class is then populated into the class text field on the settings panel.
+ */
+ private class BrowseMainClassListener implements ActionListener {
+ private final TextFieldWithBrowseButton textField;
+
+ public BrowseMainClassListener(TextFieldWithBrowseButton textField) {
+ this.textField = textField;
+ }
+
+ public void actionPerformed(ActionEvent e) {
+ DataContext dataContext = DataManager.getInstance().getDataContext(JBehaveSettingsPanel.this.contentPane);
+ Project project = CommonDataKeys.PROJECT.getData(dataContext);
+
+ // TODO: display error message if project is null
+
+ var dialog = new TreeJavaClassChooserDialog("Main Class for Running Stories",
+ project,
+ GlobalSearchScope.allScope(project),
+ MainClassFilter.INSTANCE,
+ null);
+
+ final PsiClass currentClass = JavaPsiFacade.getInstance(project)
+ .findClass(textField.getText(), GlobalSearchScope.allScope(project));
+
+ //TODO: this is not working for some reason
+ if (currentClass != null) {
+ dialog.select(currentClass);
+ }
+
+ dialog.show();
+
+ if (dialog.getExitCode() != DialogWrapper.CANCEL_EXIT_CODE) {
+ textField.setText(dialog.getSelected().getQualifiedName());
+ }
+ }
+ }
+
+ private static class MainClassFilter implements ClassFilter {
+ public static final ClassFilter INSTANCE = new MainClassFilter();
+
+ @Override
+ public boolean isAccepted(final PsiClass aClass) {
+ return ConfigurationUtil.MAIN_CLASS.value(aClass) && PsiMethodUtil.findMainMethod(aClass) != null;
+ }
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/spellchecker/JBehaveSpellcheckerStrategy.java b/src/main/java/com/github/kumaraman21/intellijbehave/spellchecker/JBehaveSpellcheckerStrategy.java
new file mode 100644
index 00000000..1d52f58d
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/spellchecker/JBehaveSpellcheckerStrategy.java
@@ -0,0 +1,39 @@
+package com.github.kumaraman21.intellijbehave.spellchecker;
+
+import java.util.List;
+
+import com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType;
+import com.intellij.codeInspection.LocalQuickFix;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.impl.source.tree.LeafElement;
+import com.intellij.spellchecker.quickfixes.AcceptWordAsCorrect;
+import com.intellij.spellchecker.quickfixes.ChangeTo;
+import com.intellij.spellchecker.tokenizer.SpellcheckingStrategy;
+import com.intellij.spellchecker.tokenizer.Tokenizer;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Provides spellchecking support for .story files.
+ */
+public class JBehaveSpellcheckerStrategy extends SpellcheckingStrategy {
+ @NotNull
+ public Tokenizer getTokenizer(PsiElement element) {
+ if (element instanceof LeafElement) {
+ ASTNode node = element.getNode();
+ if (node != null && node.getElementType() instanceof StoryTokenType) {
+ return TEXT_TOKENIZER;
+ }
+ }
+
+ return super.getTokenizer(element);
+ }
+
+ @Override
+ public LocalQuickFix[] getRegularFixes(PsiElement element, @NotNull TextRange textRange, boolean useRename, String wordWithTypo) {
+ List fixes = new ChangeTo(wordWithTypo, element, textRange).getAllAsFixes();
+ fixes.add(new AcceptWordAsCorrect(wordWithTypo));
+ return fixes.toArray(LocalQuickFix[]::new);
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/startup/ProjectStartupActivity.java b/src/main/java/com/github/kumaraman21/intellijbehave/startup/ProjectStartupActivity.java
new file mode 100644
index 00000000..3354195e
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/startup/ProjectStartupActivity.java
@@ -0,0 +1,24 @@
+package com.github.kumaraman21.intellijbehave.startup;
+
+import com.github.kumaraman21.intellijbehave.JBehaveStepDefClassPsiChangeListener;
+import com.github.kumaraman21.intellijbehave.service.JBehaveStepsIndex;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.startup.ProjectActivity;
+import com.intellij.psi.PsiManager;
+import kotlin.Unit;
+import kotlin.coroutines.Continuation;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Performs actions on project startup.
+ */
+public class ProjectStartupActivity implements ProjectActivity {
+
+ @Nullable
+ @Override
+ public Object execute(@NotNull Project project, @NotNull Continuation super Unit> continuation) {
+ PsiManager.getInstance(project).addPsiTreeChangeListener(new JBehaveStepDefClassPsiChangeListener(project), JBehaveStepsIndex.getInstance(project));
+ return Unit.INSTANCE;
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/structure/JBehaveStructureViewElement.java b/src/main/java/com/github/kumaraman21/intellijbehave/structure/JBehaveStructureViewElement.java
new file mode 100644
index 00000000..fb8e1a6a
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/structure/JBehaveStructureViewElement.java
@@ -0,0 +1,159 @@
+package com.github.kumaraman21.intellijbehave.structure;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Pattern;
+import javax.swing.*;
+
+import com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType;
+import com.github.kumaraman21.intellijbehave.language.JBehaveIcons;
+import com.github.kumaraman21.intellijbehave.parser.JBehaveStep;
+import com.github.kumaraman21.intellijbehave.parser.StoryElementType;
+import com.github.kumaraman21.intellijbehave.parser.StoryFile;
+import com.intellij.extapi.psi.ASTWrapperPsiElement;
+import com.intellij.icons.AllIcons;
+import com.intellij.ide.structureView.StructureViewTreeElement;
+import com.intellij.ide.structureView.impl.common.PsiTreeElementBase;
+import com.intellij.openapi.util.NlsSafe;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.SmartList;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Represents an item in the structure view for .story files created by {@link StoryStructureViewFactory}.
+ *
+ * Supported language elements: Story file, Scenario, step, Examples table.
+ *
+ * Lifecycle elements and steps specified within lifecycle scope are not supported for now due to lack of support on the Story language side.
+ */
+public class JBehaveStructureViewElement extends PsiTreeElementBase {
+ private static final Pattern NON_SCENARIO_DESCRIPTIONS = Pattern.compile("^(Narrative|Composite|Lifecycle):.*");
+
+ public JBehaveStructureViewElement(PsiElement root) {
+ super(root);
+ }
+
+ @Override
+ public @NotNull Collection getChildrenBase() {
+ List result = new ArrayList<>();
+ for (PsiElement element : getElement().getChildren()) {
+ if (element instanceof StoryFile
+ //Lifecycle steps are not recognized properly by the Story language, thus excluding them for now
+ || (element instanceof JBehaveStep && !isASTWrapperWithType(element.getParent(), StoryElementType.STORY))
+ //Composite and Lifecycle Examples tables are excluded because such steps are also excluded
+ || (isASTWrapperWithType(element, StoryElementType.EXAMPLES) && !isASTWrapperWithType(element.getParent(), StoryElementType.STORY))
+ || isASTWrapperWithAnyType(element, StoryElementType.STORY, StoryElementType.SCENARIO)
+ || (isASTWrapperWithAnyType(element, StoryElementType.STORY_DESCRIPTION) && NON_SCENARIO_DESCRIPTIONS.matcher(element.getText()).matches())) {
+ result.add(new JBehaveStructureViewElement(element));
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public Icon getIcon(boolean open) {
+ final PsiElement element = getElement();
+ if (element instanceof StoryFile || isASTWrapperWithType(element, StoryElementType.STORY))
+ return JBehaveIcons.JB;
+ if (isASTWrapperWithType(element, StoryElementType.STORY_DESCRIPTION)) {
+ if (element.getText().startsWith("Narrative:"))
+ return AllIcons.General.User;
+ if (element.getText().startsWith("Composite:"))
+ return AllIcons.Actions.ListFiles;
+ if (element.getText().startsWith("Lifecycle:"))
+ return AllIcons.Actions.Refresh;
+ }
+ if (isASTWrapperWithType(element, StoryElementType.SCENARIO))
+ return AllIcons.Nodes.LogFolder;
+ if (element instanceof JBehaveStep)
+ return AllIcons.Nodes.Static;
+
+ return null;
+ }
+
+ @Override
+ public @NlsSafe @Nullable String getPresentableText() {
+ PsiElement element = getElement();
+ if (element instanceof StoryFile) return ((StoryFile) element).getName();
+
+ if (element instanceof ASTWrapperPsiElement) {
+ IElementType elementType = elementTypeOf(element);
+ if (elementType == StoryElementType.STORY) return "Story";
+ if (elementType == StoryElementType.STORY_DESCRIPTION) return getStoryDescriptionText(element);
+ if (elementType == StoryElementType.SCENARIO) return getScenarioText(element);
+ if (elementType == StoryElementType.EXAMPLES) return "Examples";
+ }
+
+ if (element instanceof JBehaveStep) return getStepText((JBehaveStep) element);
+
+ return null;
+ }
+
+ //Text retrieval
+
+ /**
+ * Returns the text for the Story/Narrative element.
+ *
+ * Lifecycle elements have to be filtered since they are too recognized as story description.
+ */
+ private String getStoryDescriptionText(PsiElement element) {
+ if (element.getText().startsWith("Narrative:"))
+ return "Narrative";
+ if (element.getText().startsWith("Composite:"))
+ return element.getText();
+ if (element.getText().startsWith("Lifecycle:"))
+ return "Lifecycle";
+
+ return "";
+ }
+
+ /**
+ * Returns the keyword and title of the scenario without any meta information.
+ */
+ private String getScenarioText(PsiElement element) {
+ var scenarioTexts = new SmartList();
+ PsiElement sibling = element.getFirstChild();
+ while ((sibling = PsiTreeUtil.findSiblingForward(sibling, StoryTokenType.SCENARIO_TEXT, null)) != null) {
+ scenarioTexts.add(sibling.getText());
+ }
+ return !scenarioTexts.isEmpty() ? truncate("Scenario:" + String.join(" ", scenarioTexts), 50) : "Scenario";
+ }
+
+ /**
+ * Returns the text for the step including the step type (Given, When, Then) as well.
+ */
+ @NotNull
+ private String getStepText(JBehaveStep step) {
+ return step.getActualStepPrefix() + ": " + step.getStepText();
+ }
+
+ /**
+ * Returns the provided text if fits within the provided length, or truncates it at the provided length, appended with the ... postfix.
+ */
+ private String truncate(String text, int length) {
+ return text.length() <= length ? text : text.substring(0, length) + "...";
+ }
+
+ //Elements
+
+ private boolean isASTWrapperWithAnyType(PsiElement element, StoryElementType... types) {
+ if (element instanceof ASTWrapperPsiElement) {
+ IElementType elementType = elementTypeOf(element);
+ return Arrays.stream(types).anyMatch(type -> elementType == type);
+ }
+ return false;
+ }
+
+ private boolean isASTWrapperWithType(PsiElement element, StoryElementType type) {
+ return element instanceof ASTWrapperPsiElement && elementTypeOf(element) == type;
+ }
+
+ private IElementType elementTypeOf(PsiElement element) {
+ return ((ASTWrapperPsiElement) element).getNode().getElementType();
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/structure/StoryStructureViewFactory.java b/src/main/java/com/github/kumaraman21/intellijbehave/structure/StoryStructureViewFactory.java
new file mode 100644
index 00000000..42ad8d61
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/structure/StoryStructureViewFactory.java
@@ -0,0 +1,37 @@
+package com.github.kumaraman21.intellijbehave.structure;
+
+import com.github.kumaraman21.intellijbehave.parser.JBehaveStep;
+import com.github.kumaraman21.intellijbehave.parser.StoryFile;
+import com.intellij.extapi.psi.ASTWrapperPsiElement;
+import com.intellij.ide.structureView.StructureViewBuilder;
+import com.intellij.ide.structureView.StructureViewModel;
+import com.intellij.ide.structureView.StructureViewModelBase;
+import com.intellij.ide.structureView.TreeBasedStructureViewBuilder;
+import com.intellij.lang.PsiStructureViewFactory;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Creates a structure view for .story files.
+ */
+public class StoryStructureViewFactory implements PsiStructureViewFactory {
+ @Override
+ public @Nullable StructureViewBuilder getStructureViewBuilder(@NotNull PsiFile psiFile) {
+ return new TreeBasedStructureViewBuilder() {
+ @NotNull
+ @Override
+ public StructureViewModel createStructureViewModel(@Nullable Editor editor) {
+ PsiElement root = PsiTreeUtil.getChildOfType(psiFile, StoryFile.class);
+ if (root == null) {
+ root = psiFile;
+ }
+ return new StructureViewModelBase(psiFile, editor, new JBehaveStructureViewElement(root))
+ .withSuitableClasses(StoryFile.class, JBehaveStep.class, ASTWrapperPsiElement.class);
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/utility/CharTree.java b/src/main/java/com/github/kumaraman21/intellijbehave/utility/CharTree.java
new file mode 100644
index 00000000..48210f50
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/utility/CharTree.java
@@ -0,0 +1,141 @@
+package com.github.kumaraman21.intellijbehave.utility;
+
+import org.jetbrains.annotations.Nullable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @author @aloyer
+ */
+public class CharTree {
+ private final Map> children = new HashMap<>();
+ private final int key;
+ private T value;
+
+ public CharTree(int key) {
+ this(key, null);
+ }
+
+ public CharTree(int key, @Nullable T value) {
+ super();
+ this.key = key;
+ this.value = value;
+ }
+
+ public T lookupValue(CharSequence seq) {
+ return lookupValue(seq, 0);
+ }
+
+ public T lookupValue(CharSequence seq, int offset) {
+ return lookup(seq, offset).value;
+ }
+
+ public Entry lookup(CharSequence seq, int offset) {
+ CharTree ct = this;
+ T found = this.value;
+
+ int i = offset;
+ for (int n = seq.length(); i < n; i++) {
+ ct = ct.get(seq.charAt(i));
+ if (ct == null) {
+ break;
+ }
+ if (ct.value != null) {
+ found = ct.value;
+ }
+ }
+ return new Entry<>(found, i - offset);
+ }
+
+ public void print(int level) {
+ System.out.print(" | ".repeat(Math.max(0, level)));
+ System.out.print("[");
+ System.out.print((char) key);
+ System.out.print("] ");
+ System.out.println(value == null ? "n/a" : value);
+
+ List keys = new ArrayList<>(children.keySet());
+ Collections.sort(keys);
+ for (Integer aKey : keys) {
+ children.get(aKey).print(level + 1);
+ }
+ }
+
+ public void push(CharSequence seq, T value) {
+ push(seq, 0, value);
+ }
+
+ private void push(CharSequence seq, int pos, T value) {
+ if (pos < seq.length()) {
+ int c = seq.charAt(pos);
+ CharTree child = getOrCreate(c);
+ child.push(seq, pos + 1, value);
+ }
+ else {
+ this.value = value;
+ }
+ }
+
+ private CharTree get(int c) {
+ return children.get(c);
+ }
+
+ private CharTree getOrCreate(int c) {
+ CharTree cn = children.get(c);
+ if (cn == null) {
+ cn = new CharTree<>(c);
+ children.put(c, cn);
+ }
+ return cn;
+ }
+
+ public boolean isLeaf() {
+ return children.isEmpty();
+ }
+
+ public static class Entry {
+ public final T value;
+ public final int length;
+
+ public Entry(T value, int length) {
+ this.value = value;
+ this.length = length;
+ }
+
+ public boolean hasValue() {
+ return value != null;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Entry entry = (Entry) o;
+ return length == entry.length && Objects.equals(value, entry.value);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = value != null ? value.hashCode() : 0;
+ result = 31 * result + length;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Entry{" +
+ "value=" + value +
+ ", length=" + length +
+ '}';
+ }
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/utility/JBKeyword.java b/src/main/java/com/github/kumaraman21/intellijbehave/utility/JBKeyword.java
new file mode 100644
index 00000000..feb9ba04
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/utility/JBKeyword.java
@@ -0,0 +1,126 @@
+package com.github.kumaraman21.intellijbehave.utility;
+
+import org.jbehave.core.configuration.Keywords;
+
+/**
+ * @author @aloyer
+ */
+
+public enum JBKeyword {
+ Meta {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.meta();
+ }
+ },
+ MetaProperty {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.metaProperty();
+ }
+ },
+ Narrative {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.narrative();
+ }
+ },
+ InOrderTo {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.inOrderTo();
+ }
+ },
+ AsA {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.asA();
+ }
+ },
+ IWantTo {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.iWantTo();
+ }
+ },
+ Scenario {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.scenario();
+ }
+ },
+ GivenStories {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.givenStories();
+ }
+ },
+ ExamplesTable {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.examplesTable();
+ }
+ },
+ ExamplesTableRow {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.examplesTableRow();
+ }
+ },
+ ExamplesTableHeaderSeparator {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.examplesTableHeaderSeparator();
+ }
+ },
+ ExamplesTableValueSeparator {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.examplesTableValueSeparator();
+ }
+ },
+ ExamplesTableIgnorableSeparator {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.examplesTableIgnorableSeparator();
+ }
+ },
+ Given {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.given();
+ }
+ },
+ When {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.when();
+ }
+ },
+ Then {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.then();
+ }
+ },
+ And {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.and();
+ }
+ },
+ Ignorable {
+ @Override
+ public String asString(Keywords keywords) {
+ return keywords.ignorable();
+ }
+ };
+
+ public String asString(Keywords keywords) {
+ throw new AbstractMethodError();
+ }
+
+ public boolean isComment() {
+ return this == Ignorable;
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/utility/LocalizedStorySupport.java b/src/main/java/com/github/kumaraman21/intellijbehave/utility/LocalizedStorySupport.java
new file mode 100644
index 00000000..ece52fe2
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/utility/LocalizedStorySupport.java
@@ -0,0 +1,53 @@
+package com.github.kumaraman21.intellijbehave.utility;
+
+import org.apache.commons.lang3.LocaleUtils;
+import org.jbehave.core.i18n.LocalizedKeywords;
+import org.jetbrains.annotations.NotNull;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author @aloyer
+ */
+public class LocalizedStorySupport {
+
+ private static final Pattern LANGUAGE_PATTERN = Pattern.compile("language:[ ]*([a-z]{2}(_[A-Z]{2}(_[^ ]+)?)?)");
+
+ /**
+ * Returns the language directive if it exists.
+ *
+ * The methods checks if the {@link CharSequence} contains a language directive,
+ * if so it returns the corresponding locale as a {@link String} otherwise returns
+ * null.
+ *
+ * @param charSeq the sequence where the language directive is searched
+ * @return the defined locale or null
+ */
+ public static String checkForLanguageDefinition(CharSequence charSeq) {
+ Matcher matcher = LANGUAGE_PATTERN.matcher(charSeq);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link LocalizedKeywords} that corresponds to the given locale. If
+ * the locale provided in invalid, {@link Locale#ENGLISH} is used.
+ *
+ * @param localeAsString the locale for which the {@link LocalizedKeywords} must
+ * be returned
+ */
+ @NotNull
+ public LocalizedKeywords getKeywords(String localeAsString) {
+ Locale locale;
+ try {
+ locale = LocaleUtils.toLocale(localeAsString);
+ }
+ catch (Exception e) {
+ locale = Locale.ENGLISH;
+ }
+ return new LocalizedKeywords(locale);
+ }
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/utility/ParametrizedString.java b/src/main/java/com/github/kumaraman21/intellijbehave/utility/ParametrizedString.java
new file mode 100644
index 00000000..7c7a427b
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/utility/ParametrizedString.java
@@ -0,0 +1,346 @@
+package com.github.kumaraman21.intellijbehave.utility;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author @aloyer
+ */
+public class ParametrizedString {
+
+ private static Pattern compileParameterPattern(String parameterPrefix) {
+ return Pattern.compile("(\\" + parameterPrefix + "\\w*)(\\W|\\Z)", Pattern.DOTALL);
+ }
+
+ private List tokens = new ArrayList<>();
+ private final String content;
+ private final String parameterPrefix;
+
+
+ public ParametrizedString(String content) {
+ this(content, "$");
+ }
+
+ public ParametrizedString(String content, String parameterPrefix) {
+ if (content == null) {
+ throw new IllegalArgumentException("Content cannot be null");
+ }
+ this.content = content;
+ this.parameterPrefix = parameterPrefix;
+ parse(compileParameterPattern(parameterPrefix));
+ }
+
+ @Override
+ public int hashCode() {
+ return content.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return (obj instanceof ParametrizedString) && isSameAs((ParametrizedString) obj);
+ }
+
+ public boolean isSameAs(ParametrizedString other) {
+ return other.content.equals(content);
+ }
+
+ private void parse(final Pattern parameterPattern) {
+ final Matcher matcher = parameterPattern.matcher(content);
+
+ int prev = 0;
+ while (matcher.find()) {
+ int start = matcher.start();
+ int end = matcher.end();
+ if (start > prev) {
+ add(new Token(prev, start - prev, false));
+ }
+ end -= matcher.group(2).length();
+ start += parameterPrefix.length(); // remove prefix from the identifier
+ add(new Token(start, end - start, true));
+ prev = end;
+ }
+ if (prev < content.length()) {
+ add(new Token(prev, content.length() - prev, false));
+ }
+ }
+
+ private void add(Token token) {
+ tokens.add(token);
+ }
+
+ public class Token {
+ private final int offset;
+ private final int length;
+ private final boolean isIdentifier;
+
+ public Token(int offset, int length, boolean isIdentifier) {
+ this.offset = offset;
+ this.length = length;
+ this.isIdentifier = isIdentifier;
+ }
+
+ public String value() {
+ return content.substring(getOffset(), getOffset() + getLength());
+ }
+
+ @Override
+ public String toString() {
+ return "<<" + (isIdentifier() ? "$" : "") + value() + ">>";
+ }
+
+ public boolean regionMatches(int toffset, String other, int ooffset, int len) {
+ try {
+ return normalize(content, getOffset() + toffset, len)
+ .equalsIgnoreCase(normalize(other, ooffset, len));
+ } catch (final java.lang.StringIndexOutOfBoundsException e) {
+ return false;
+ }
+ }
+
+ private String normalize(final String input, final int offset, final int len) {
+ return input.substring(offset, offset + len)
+ .replaceAll("\\s+", "");
+ }
+
+ public int getOffset() {
+ return offset;
+ }
+
+ public int getLength() {
+ return length;
+ }
+
+ public boolean isIdentifier() {
+ return isIdentifier;
+ }
+ }
+
+ public Token getToken(int index) {
+ return tokens.get(index);
+ }
+
+ public int getTokenCount() {
+ return tokens.size();
+ }
+
+ public WeightChain calculateWeightChain(String input) {
+ WeightChain chain = acceptsBeginning(0, input, 0);
+ chain.input = input;
+ chain.collectWeights();
+ return chain;
+ }
+
+ public static class StringToken {
+ private final String value;
+ private final boolean identifier;
+
+ public StringToken(String value, boolean identifier) {
+ this.value = value;
+ this.identifier = identifier;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public boolean isIdentifier() {
+ return identifier;
+ }
+ }
+
+ public List tokenize(String stepInput) {
+
+ List stringTokens = new ArrayList<>();
+
+ WeightChain chain = calculateWeightChain(stepInput);
+ List inputTokens = chain.tokenize();
+ while (chain != null) {
+ if (!chain.isZero()) {
+ Token token = tokens.get(chain.getTokenIndex());
+ String value = inputTokens.get(chain.getTokenIndex());
+ stringTokens.add(new StringToken(value, token.isIdentifier()));
+ }
+ chain = chain.getNext();
+ }
+ return stringTokens;
+ }
+
+ private WeightChain acceptsBeginning(int inputIndex, String input, int tokenIndexStart) {
+ WeightChain pair = new WeightChain();
+ pair.inputIndex = inputIndex;
+
+ WeightChain current = pair;
+
+ List tokenList = this.tokens;
+ for (int tokenIndex = tokenIndexStart, n = tokenList.size(); tokenIndex < n; tokenIndex++) {
+ boolean isLastToken = (tokenIndex == n - 1);
+ Token token = tokenList.get(tokenIndex);
+ if (!token.isIdentifier()) {
+ int remaining = input.length() - inputIndex;
+ if (remaining > token.getLength() && isLastToken) {
+ // more data than the token itself
+ return WeightChain.zero();
+ }
+
+ int overlaping = Math.min(token.getLength(), remaining);
+ if (overlaping > 0) {
+ if (token.regionMatches(0, input, inputIndex, overlaping)) {
+ current.tokenIndex = tokenIndex;
+ current.weight++;
+ if (overlaping == token.getLength()) // full token match
+ {
+ current.weight++;
+ if ((inputIndex + overlaping) == input.length())
+ // no more data, break the loop now
+ {
+ return pair;
+ }
+ } // break looop
+ else {
+ return pair;
+ }
+
+ inputIndex += overlaping;
+ WeightChain next = new WeightChain();
+ next.inputIndex = inputIndex;
+ current.next = next;
+ current = next;
+ } else {
+ // no match
+ return WeightChain.zero();
+ }
+ } else {
+ // not enough data, returns what has been collected
+ return pair;
+ }
+ } else {
+ current.tokenIndex = tokenIndex;
+ current.weight++;
+
+ // not the most efficient part, but no other solution right now
+ WeightChain next = WeightChain.zero();
+ for (int j = inputIndex + 1; j < input.length(); j++) {
+ WeightChain sub = acceptsBeginning(j, input, tokenIndex + 1);
+ if (sub.hasMoreWeightThan(next)) {
+ next = sub;
+ }
+ }
+ current.next = next;
+ return pair;
+ }
+ }
+ return pair;
+ }
+
+ public static class WeightChain {
+ public static WeightChain zero() {
+ return new WeightChain();
+ }
+
+ private String input;
+ private int inputIndex;
+ private int weight;
+ private int tokenIndex = -1;
+ private WeightChain next;
+
+ public WeightChain last() {
+ WeightChain last = this;
+ WeightChain iter = this;
+ while (iter != null) {
+ if (!iter.isZero()) {
+ last = iter;
+ }
+ iter = iter.next;
+ }
+ return last;
+ }
+
+ public boolean isZero() {
+ return weight == 0 && tokenIndex == -1;
+ }
+
+ public WeightChain getNext() {
+ return next;
+ }
+
+ public int getTokenIndex() {
+ return tokenIndex;
+ }
+
+ public boolean hasMoreWeightThan(WeightChain pair) {
+ if (weight > pair.weight) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "WeightChain [inputIndex=" + inputIndex + ", weight=" + weight + ", tokenIndex=" + tokenIndex + "]";
+ }
+
+ public void collectWeights() {
+ int w = weight;
+ WeightChain n = next;
+ while (n != null) {
+ if (!n.isZero()) {
+ w += n.weight;
+ }
+ n = n.next;
+ }
+
+ this.weight = w;
+ }
+
+ public List tokenize() {
+ List parts = new ArrayList<>();
+ if (isZero()) {
+ return parts;
+ }
+
+ int indexBeg = inputIndex;
+ WeightChain n = next;
+ while (n != null) {
+ if (!n.isZero()) {
+ parts.add(input.substring(indexBeg, n.inputIndex));
+ indexBeg = n.inputIndex;
+ }
+ n = n.next;
+ }
+ parts.add(input.substring(indexBeg));
+
+ return parts;
+ }
+ }
+
+ public String complete(String input) {
+ WeightChain chain = calculateWeightChain(input);
+ WeightChain last = chain.last();
+ if (last.isZero()) {
+ return "";
+ }
+ int inputIndex = last.inputIndex;
+ int tokenIndex = last.tokenIndex;
+
+ StringBuilder builder = new StringBuilder();
+
+ Token token = getToken(tokenIndex);
+ if (!token.isIdentifier()) {
+ int consumed = input.length() - inputIndex;
+ builder.append(getToken(tokenIndex).value().substring(consumed));
+ }
+ tokenIndex++;
+ for (int i = tokenIndex; i < getTokenCount(); i++) {
+ token = getToken(i);
+ if (token.isIdentifier()) {
+ builder.append(parameterPrefix);
+ }
+ builder.append(token.value());
+ }
+ return builder.toString();
+ }
+
+}
diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/utility/ScanUtils.java b/src/main/java/com/github/kumaraman21/intellijbehave/utility/ScanUtils.java
new file mode 100644
index 00000000..ba761d11
--- /dev/null
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/utility/ScanUtils.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2011-12 Aman Kumar
+ *
+ * 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 com.github.kumaraman21.intellijbehave.utility;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleUtil;
+import com.intellij.openapi.roots.ContentIterator;
+import com.intellij.openapi.roots.ModuleRootManager;
+import com.intellij.psi.PsiElement;
+
+import java.util.HashSet;
+
+public final class ScanUtils {
+
+ public static boolean iterateInContextOf(PsiElement storyRef, ContentIterator iterator) {
+ Module module = ModuleUtil.findModuleForPsiElement(storyRef);
+
+ boolean shouldContinue = (module != null) && ModuleRootManager.getInstance(module).getFileIndex().iterateContent(iterator);
+
+ if (shouldContinue) {
+ HashSet dependencies = new HashSet<>();
+ ModuleUtil.getDependencies(module, dependencies);
+
+ for (Module dependency : dependencies) {
+ shouldContinue = ModuleRootManager.getInstance(dependency).getFileIndex().iterateContent(iterator);
+ if (!shouldContinue) {
+ break;
+ }
+ }
+ }
+
+ return shouldContinue;
+ }
+
+ private ScanUtils() {
+ }
+}
diff --git a/src/com/github/kumaraman21/intellijbehave/utility/ProjectUtils.java b/src/main/java/com/github/kumaraman21/intellijbehave/utility/StepTypeMappings.java
similarity index 53%
rename from src/com/github/kumaraman21/intellijbehave/utility/ProjectUtils.java
rename to src/main/java/com/github/kumaraman21/intellijbehave/utility/StepTypeMappings.java
index 21c26601..c2f3467e 100644
--- a/src/com/github/kumaraman21/intellijbehave/utility/ProjectUtils.java
+++ b/src/main/java/com/github/kumaraman21/intellijbehave/utility/StepTypeMappings.java
@@ -15,21 +15,24 @@
*/
package com.github.kumaraman21.intellijbehave.utility;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.project.ProjectManager;
-import com.intellij.openapi.roots.ProjectFileIndex;
-import com.intellij.openapi.roots.ProjectRootManager;
-import org.jetbrains.annotations.NotNull;
+import org.jbehave.core.annotations.Given;
+import org.jbehave.core.annotations.Then;
+import org.jbehave.core.annotations.When;
+import org.jbehave.core.steps.StepType;
-public class ProjectUtils {
+import java.util.Map;
- @NotNull
- public static Project getCurrentProject() {
- return ProjectManager.getInstance().getOpenProjects()[0];
- }
+/**
+ * Utility to provide {@link StepType} and step annotation mappings.
+ */
+public final class StepTypeMappings {
+
+ public static final Map ANNOTATION_TO_STEP_TYPE_MAPPING = Map.of(
+ Given.class.getName(), StepType.GIVEN,
+ When.class.getName(), StepType.WHEN,
+ Then.class.getName(), StepType.THEN
+ );
- @NotNull
- public static ProjectFileIndex getProjectFileIndex() {
- return ProjectRootManager.getInstance(getCurrentProject()).getFileIndex();
- }
+ private StepTypeMappings() {
+ }
}
diff --git a/src/main/kotlin/com/github/kumaraman21/intellijbehave/JBehaveStepDefClassPsiChangeListener.kt b/src/main/kotlin/com/github/kumaraman21/intellijbehave/JBehaveStepDefClassPsiChangeListener.kt
new file mode 100644
index 00000000..57a31e98
--- /dev/null
+++ b/src/main/kotlin/com/github/kumaraman21/intellijbehave/JBehaveStepDefClassPsiChangeListener.kt
@@ -0,0 +1,108 @@
+package com.github.kumaraman21.intellijbehave
+
+import com.github.kumaraman21.intellijbehave.kotlin.pluginIsEnabled
+import com.github.kumaraman21.intellijbehave.kotlin.support.services.KotlinPsiClassesHandler
+import com.intellij.openapi.project.DumbService
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.Ref
+import com.intellij.psi.JavaRecursiveElementVisitor
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiFile
+import com.intellij.psi.PsiJavaFile
+import com.intellij.psi.PsiModifier
+import com.intellij.psi.PsiTreeChangeAdapter
+import com.intellij.psi.PsiTreeChangeEvent
+import com.intellij.util.ui.update.MergingUpdateQueue
+import com.intellij.util.ui.update.Update
+
+/**
+ * Reacts to changes in JBehave Java step definition files.
+ *
+ * TODO: this should probably have a counterpart for Kotlin step def classes
+ */
+class JBehaveStepDefClassPsiChangeListener(val project: Project) : PsiTreeChangeAdapter() {
+
+ private val updateQueue = MergingUpdateQueue(
+ "JBehaveStepDefClassPsiChangeListener",
+ 300, // ms delay
+ true, // activate
+ null,
+ JBehaveStepDefClassesModificationTracker.getInstance(project) // disposable parent
+ )
+
+ override fun childrenChanged(event: PsiTreeChangeEvent) = updateJBehaveTestClassModificationTracker(event)
+ override fun childAdded(event: PsiTreeChangeEvent) = updateJBehaveTestClassModificationTracker(event)
+ override fun childMoved(event: PsiTreeChangeEvent) = updateJBehaveTestClassModificationTracker(event)
+ override fun childRemoved(event: PsiTreeChangeEvent) = updateJBehaveTestClassModificationTracker(event)
+ override fun childReplaced(event: PsiTreeChangeEvent) = updateJBehaveTestClassModificationTracker(event)
+ override fun propertyChanged(event: PsiTreeChangeEvent) = updateJBehaveTestClassModificationTracker(event)
+
+ private fun updateJBehaveTestClassModificationTracker(event: PsiTreeChangeEvent) {
+ val file = event.file
+ if (file != null && file.isValid) {
+ updateQueue.queue(Update.create(project) { updateModificationTrackerIfFileContainsJBehaveStepDefClass(file) })
+ }
+ //file is null when the file has just been deleted
+ else {
+ val child = event.child
+ if (child is PsiJavaFile && child.isValid) {
+ updateQueue.queue(Update.create(project) { updateModificationTrackerIfFileContainsJBehaveStepDefClass(child) })
+ }
+ }
+ }
+
+ /**
+ * Updates the [JBehaveStepDefClassesModificationTracker] if the provided file is a JBehave step definitions file.
+ *
+ * @param file the PsiFile being examined
+ */
+ private fun updateModificationTrackerIfFileContainsJBehaveStepDefClass(file: PsiFile) {
+ if (!DumbService.isDumb(project)) {
+ val hasJBehaveStepDefTestClass = Ref()
+ if (file is PsiJavaFile) {
+ //If it finds a JBehave step def class in the file, then modification tracker will be eligible for update
+ file.accept(object : JavaRecursiveElementVisitor() {
+ override fun visitClass(aClass: PsiClass) {
+ if (isJavaJBehaveStepDefClass(aClass)) {
+ hasJBehaveStepDefTestClass.set(true)
+ return
+ }
+ super.visitClass(aClass)
+ }
+ })
+ } else if (pluginIsEnabled && KotlinPsiClassesHandler.isKotlinFile(file)) {
+ if (KotlinPsiClassesHandler.visitClasses(file))
+ hasJBehaveStepDefTestClass.set(true)
+ }
+
+ if (!hasJBehaveStepDefTestClass.isNull && hasJBehaveStepDefTestClass.get()) {
+ JBehaveStepDefClassesModificationTracker.getInstance(project).increaseModificationCount()
+ }
+ }
+ }
+
+ private fun isJavaJBehaveStepDefClass(aClass: PsiClass): Boolean {
+ return try {
+ !aClass.isEnum
+ && !aClass.isRecord
+ && !aClass.isInterface
+ && aClass.qualifiedName != null
+ //Treat only the public methods of this class as step defs as the visitor also goes into nested and
+ // super classes, so listing methods from no need
+ && aClass.methods.asSequence().filter { it.hasModifierProperty(PsiModifier.PUBLIC) }.any {
+ return@any try {
+ it.hasAnnotation("org.jbehave.core.annotations.Given")
+ || it.hasAnnotation("org.jbehave.core.annotations.When")
+ || it.hasAnnotation("org.jbehave.core.annotations.Then")
+ || it.hasAnnotation("org.jbehave.core.annotations.Alias")
+ || it.hasAnnotation("org.jbehave.core.annotations.Aliases")
+ || it.hasAnnotation("org.jbehave.core.annotations.Composite")
+ } catch (_: Exception) {
+ false
+ }
+ }
+ } catch (_: Exception) {
+ false
+ }
+ }
+}
diff --git a/src/main/kotlin/com/github/kumaraman21/intellijbehave/jbehaveModificationTrackers.kt b/src/main/kotlin/com/github/kumaraman21/intellijbehave/jbehaveModificationTrackers.kt
new file mode 100644
index 00000000..b6c8d3b5
--- /dev/null
+++ b/src/main/kotlin/com/github/kumaraman21/intellijbehave/jbehaveModificationTrackers.kt
@@ -0,0 +1,27 @@
+package com.github.kumaraman21.intellijbehave
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.ModificationTracker
+import com.intellij.openapi.util.SimpleModificationTracker
+
+/**
+ * Tracks the modification count of JBehave step definition files in the given project.
+ */
+@Service(Service.Level.PROJECT)
+class JBehaveStepDefClassesModificationTracker(val project: Project) : ModificationTrackerBase(), Disposable.Default {
+ companion object {
+ @JvmStatic
+ fun getInstance(project: Project) = project.service()
+ }
+}
+
+abstract class ModificationTrackerBase : ModificationTracker {
+ private val modificationTracker = SimpleModificationTracker()
+
+ override fun getModificationCount() = modificationTracker.modificationCount
+
+ fun increaseModificationCount() = modificationTracker.incModificationCount()
+}
\ No newline at end of file
diff --git a/src/main/resources/META-INF/kotlin-config.xml b/src/main/resources/META-INF/kotlin-config.xml
new file mode 100644
index 00000000..6fb76a66
--- /dev/null
+++ b/src/main/resources/META-INF/kotlin-config.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
new file mode 100644
index 00000000..b6bb8cb5
--- /dev/null
+++ b/src/main/resources/META-INF/plugin.xml
@@ -0,0 +1,113 @@
+
+
+ JBehave Support
+ jbehave-support-plugin
+
+ The plugin provides the following features:
+
+
+
Basic syntax highlighting for JBehave story files
+
Jump to step definition in Java or Groovy
+
Error Highlighting in story if step was not defined
+
Create new story files from a configurable story template
+
Comment/uncomment lines in story files
+
Code inspections to report unused steps definitions and undefined step usages
+
Run *.story files
+
Finding usages of steps methods
+
+
+
+ Known limitations:
+
+
+
Searches complete module classpath, no configuration available to limit scope
+
Does not take into account any custom JBehave configuration