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
  • -
  • Run *.story files
  • -
- ]]>
- 1.2 - Aman Kumar - - - - - com.github.kumaraman21.intellijbehave.template.JBehaveTemplateLoaderComponent - - - com.github.kumaraman21.intellijbehave.settings.JBehaveSettings - - - com.github.kumaraman21.intellijbehave.runner.StoryRunnerConfigurationType - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
\ No newline at end of file diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 00000000..b9c2aa40 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,7 @@ +This product includes software developed in the JBehave project. (https://github.com/jbehave/jbehave-core). + +The classes 'com.github.kumaraman21.intellijbehave.jbehave.core.steps.PatternVariantBuilder' and +'com.github.kumaraman21.intellijbehave.jbehave.core.steps.PatternVariantBuilderTest' are the simplified +and modified versions of 'org.jbehave.core.steps.PatternVariantBuilder' and 'org.jbehave.core.steps.PatternVariantBuilderTest'. + +Those classes are licensed under the BSD-3-Clause license. diff --git a/README.md b/README.md new file mode 100644 index 00000000..091735c2 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +JBehave Support for IntelliJ IDEA +================================= + +[![JBehave Support](https://img.shields.io/jetbrains/plugin/v/7268-jbehave-support)](https://plugins.jetbrains.com/plugin/7268-jbehave-support) + +IntelliJ IDEA Plugin for JBehave + + +This plugin provides some support for JBehave. It is a fork of IntelliJBehave, originally created by Aman Kumar. + +## Features + +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 +* Spellchecking in .story files +* Structure view for .story files (via Ctrl+12 or similar shortcut) + +## Known limitations + +* Searches complete module classpath, no configuration available to limit scope +* Does not take into account any custom JBehave configuration +* Lifecycles are currently not supported in the .story file structure view + + +## Contribute + +* Setup: +``` +git clone +./gradlew idea +``` + +In IntelliJ just 'Open' that folder and everything should be ready. + +* Run: +``` +./gradlew runIde +``` +You can do this from within IntelliJ if you enable gradle on the project. + +## Contributions + +See original documentation at https://github.com/kumaraman21/IntelliJBehave/wiki. + +Most of the original code has been retained, but several improvements have been incorporated by various contributors: +* https://github.com/jarosite +* https://github.com/Arnauld +* https://github.com/harley84 +* https://github.com/RodrigoQuesadaDev +* https://github.com/JenoDK \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..c2fccdf6 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,140 @@ +import org.jetbrains.changelog.Changelog +import org.jetbrains.changelog.markdownToHTML +import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType +import org.jetbrains.intellij.platform.gradle.TestFrameworkType + +plugins { + id("java") // Java support + alias(libs.plugins.kotlin) // Kotlin support + alias(libs.plugins.intelliJPlatform) // IntelliJ Platform Gradle Plugin + alias(libs.plugins.changelog) // Gradle Changelog Plugin +} + +group = providers.gradleProperty("pluginGroup").get() +version = providers.gradleProperty("pluginVersion").get() + +// Set the JVM language level used to build the project. +kotlin { + jvmToolchain(21) +} + +// Configure project's dependencies +repositories { + mavenCentral() + + // IntelliJ Platform Gradle Plugin Repositories Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-repositories-extension.html + intellijPlatform { + defaultRepositories() + } +} + +// Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog +dependencies { + // IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html + + intellijPlatform { + create(providers.gradleProperty("platformType"), providers.gradleProperty("platformVersion")) + + // Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins. + bundledPlugins(providers.gradleProperty("platformBundledPlugins").map { it.split(',') }) + + // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file for plugin from JetBrains Marketplace. + plugins(providers.gradleProperty("platformPlugins").map { it.split(',') }) + + pluginVerifier() + zipSigner() + testFramework(TestFrameworkType.Platform) + //Required for 'LightJavaCodeInsightFixtureTestCase5' + testFramework(TestFrameworkType.Plugin.Java) + //Required for the 'com.intellij.testFramework.junit5' package + testFramework(TestFrameworkType.JUnit5) + + //Required for 'junit.framework.TestCase' referenced in 'com.intellij.testFramework.UsefulTestCase' + testImplementation(libs.junit) + testImplementation(libs.opentest4j) + testImplementation("org.assertj:assertj-core:3.27.0") + testImplementation("org.junit.jupiter:junit-jupiter-params:5.11.3") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.3") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.3") + } + + implementation("org.jbehave:jbehave-core:5.2.0") { + exclude(group = "org.junit.jupiter", module = "junit-jupiter") + exclude(group = "org.junit.jupiter", module = "junit-jupiter-api") + exclude(group = "org.junit.jupiter", module = "junit-jupiter-engine") + exclude(group = "org.junit.jupiter", module = "junit-jupiter-params") + exclude(group = "org.junit.platform", module = "junit-platform-commons") + exclude(group = "org.junit.platform", module = "junit-platform-engine") + exclude(group = "org.junit.platform", module = "junit-platform-launcher") + exclude(group = "org.junit.vintage", module = "junit-vintage-engine") + } + implementation("org.apache.commons:commons-text:1.13.0") +} + +// Configure IntelliJ Platform Gradle Plugin - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-extension.html +intellijPlatform { + pluginConfiguration { + name = providers.gradleProperty("pluginName") + version = providers.gradleProperty("pluginVersion") + + // Extract the section from README.md and provide for the plugin's manifest + description = providers.fileContents(layout.projectDirectory.file("README.md")).asText.map { + val start = "" + val end = "" + + with(it.lines()) { + if (!containsAll(listOf(start, end))) { + throw GradleException("Plugin description section not found in README.md:\n$start ... $end") + } + subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML) + } + } + + val changelog = project.changelog // local variable for configuration cache compatibility + // Get the latest available change notes from the changelog file + changeNotes = providers.gradleProperty("pluginVersion").map { pluginVersion -> + with(changelog) { + renderItem( + (getOrNull(pluginVersion) ?: getUnreleased()) + .withHeader(false) + .withEmptySections(false), + Changelog.OutputType.HTML, + ) + } + } + + ideaVersion { + sinceBuild = providers.gradleProperty("pluginSinceBuild") +// untilBuild = providers.gradleProperty("pluginUntilBuild") + } + } + + pluginVerification { + ides { + recommended() + } + } +} + +//Uncomment this to start the IDE with the K2 Kotlin compiler enabled +//tasks.named("runIde") { +// jvmArgumentProviders += CommandLineArgumentProvider { +// listOf("-Didea.kotlin.plugin.use.k2=true") +// } +//} + +// Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin +changelog { + groups.empty() + repositoryUrl = providers.gradleProperty("pluginRepositoryUrl") +} + +tasks { + wrapper { + gradleVersion = providers.gradleProperty("gradleVersion").get() + } + + test { + useJUnitPlatform() + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..a97582f9 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,33 @@ +# IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html + +pluginGroup = com.github.kumaraman21.intellijbehave +pluginName = JBehave Support +pluginRepositoryUrl = https://github.com/witspirit/IntelliJBehave +# SemVer format -> https://semver.org +pluginVersion = 1.69.2 + +# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html +pluginSinceBuild = 251 +#pluginUntilBuild = 251.* + +# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension +platformType = IC +platformVersion = 2025.1 + +# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html +# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 +platformPlugins = +# Example: platformBundledPlugins = com.intellij.java +platformBundledPlugins = com.intellij.java,org.jetbrains.kotlin + +# Gradle Releases -> https://github.com/gradle/gradle/releases +gradleVersion = 8.13 + +# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib +kotlin.stdlib.default.dependency = false + +# Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html +org.gradle.configuration-cache = true + +# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html +org.gradle.caching = true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..5f52879a --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,18 @@ +[versions] +# libraries +junit = "4.13.2" +opentest4j = "1.3.0" + +# plugins +kotlin = "2.1.20" +changelog = "2.2.1" +intelliJPlatform = "2.11.0" + +[libraries] +junit = { group = "junit", name = "junit", version.ref = "junit" } +opentest4j = { group = "org.opentest4j", name = "opentest4j", version.ref = "opentest4j" } + +[plugins] +changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } +intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" } +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..a4b76b95 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..37f853b1 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..f5feea6d --- /dev/null +++ b/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..9b42019c --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lib/hamcrest-all-1.1.jar b/lib/hamcrest-all-1.1.jar deleted file mode 100644 index 06686f23..00000000 Binary files a/lib/hamcrest-all-1.1.jar and /dev/null differ diff --git a/lib/jbehave-core-3.5.4.jar b/lib/jbehave-core-3.5.4.jar deleted file mode 100644 index e9bb0498..00000000 Binary files a/lib/jbehave-core-3.5.4.jar and /dev/null differ diff --git a/notes.md b/notes.md new file mode 100644 index 00000000..5a555c81 --- /dev/null +++ b/notes.md @@ -0,0 +1,144 @@ +http://confluence.jetbrains.net/display/IDEADEV/Developing+Custom+Language+Plugins+for+IntelliJ+IDEA#DevelopingCustomLanguagePluginsforIntelliJIDEA-ImplementingaParserandPSI + +Blue Forest theme: + + + + + + + + + + + + + + + \ 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 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 + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
PatternResult
..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 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 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>) () -> { + 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>) () -> { + 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 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 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 myConsumer; + + private MyReferenceCheckingProcessor(@NotNull PsiElement elementToFind, @NotNull Processor 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 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
  • +
+ +

See the github for more info

+ ]]> + Bert Van Vlerken, Victor Rosenberg + + com.intellij.modules.java + org.jetbrains.kotlin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/fileTemplates/JBehave Story.story.ft b/src/main/resources/fileTemplates/JBehave Story.story.ft similarity index 100% rename from resources/fileTemplates/JBehave Story.story.ft rename to src/main/resources/fileTemplates/JBehave Story.story.ft diff --git a/src/main/resources/fileTypes/bdd-jb-orange-red-green.png b/src/main/resources/fileTypes/bdd-jb-orange-red-green.png new file mode 100644 index 00000000..e150524c Binary files /dev/null and b/src/main/resources/fileTypes/bdd-jb-orange-red-green.png differ diff --git a/src/main/resources/inspectionDescriptions/UndefinedStep.html b/src/main/resources/inspectionDescriptions/UndefinedStep.html new file mode 100644 index 00000000..eda2b5a4 --- /dev/null +++ b/src/main/resources/inspectionDescriptions/UndefinedStep.html @@ -0,0 +1,5 @@ + + +This inspection reports when a JBehave step has not been defined. + + diff --git a/src/main/resources/inspectionDescriptions/UnusedStepDeclaration.html b/src/main/resources/inspectionDescriptions/UnusedStepDeclaration.html new file mode 100644 index 00000000..b4dcfc74 --- /dev/null +++ b/src/main/resources/inspectionDescriptions/UnusedStepDeclaration.html @@ -0,0 +1,6 @@ + + +This inspection reports when a JBehave step declaration is +not used in any of the story files. Note: May be CPU intensive for projects with a lot of story files. + + diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/ContentEntryProjectDescriptor.java b/src/test/java/com/github/kumaraman21/intellijbehave/ContentEntryProjectDescriptor.java new file mode 100644 index 00000000..63b2652e --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/ContentEntryProjectDescriptor.java @@ -0,0 +1,35 @@ +package com.github.kumaraman21.intellijbehave; + +import com.intellij.openapi.module.Module; +import com.intellij.openapi.projectRoots.JavaSdk; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.roots.ContentEntry; +import com.intellij.openapi.roots.ModifiableRootModel; +import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.jps.model.java.JavaResourceRootType; +import org.jetbrains.jps.model.java.JavaSourceRootType; + +/** + * Project descriptor that recognizes different project content entries. + */ +public final class ContentEntryProjectDescriptor extends DefaultLightProjectDescriptor { + + @Override + public Sdk getSdk() { + return JavaSdk.getInstance().createJdk("Real JDK", System.getenv("JAVA_HOME"), false); + } + + @Override + public void configureModule(@NotNull Module module, @NotNull ModifiableRootModel model, @NotNull ContentEntry contentEntry) { + super.configureModule(module, model, contentEntry); + contentEntry.clearSourceFolders(); + + String contentEntryUrl = contentEntry.getUrl(); + contentEntry.addSourceFolder(contentEntryUrl + "/main/java", JavaSourceRootType.SOURCE); + contentEntry.addSourceFolder(contentEntryUrl + "/main/kotlin", JavaSourceRootType.SOURCE); + contentEntry.addSourceFolder(contentEntryUrl + "/main/resources", JavaResourceRootType.RESOURCE); + contentEntry.addSourceFolder(contentEntryUrl + "/test/java", JavaSourceRootType.TEST_SOURCE); + contentEntry.addSourceFolder(contentEntryUrl + "/test/resources", JavaResourceRootType.TEST_RESOURCE); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/ContentEntryTestBase.java b/src/test/java/com/github/kumaraman21/intellijbehave/ContentEntryTestBase.java new file mode 100644 index 00000000..4886f4aa --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/ContentEntryTestBase.java @@ -0,0 +1,28 @@ +package com.github.kumaraman21.intellijbehave; + +import static com.intellij.openapi.application.ApplicationManager.getApplication; + +import com.intellij.openapi.application.ModalityState; +import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor; + +/** + * Base class for tests using content entries. + */ +public abstract class ContentEntryTestBase extends JBehaveSupportTestBase { + + public ContentEntryTestBase() { + super(new ContentEntryProjectDescriptor().withRepositoryLibrary("org.jbehave:jbehave-core:5.2.0")); + } + + public ContentEntryTestBase(DefaultLightProjectDescriptor projectDescriptor) { + super(projectDescriptor); + } + + /** + * The directory has to be copied in each related test method, instead of in a before hooks, + * so that it is called properly on EDT in write-safe context. + */ + protected void copySrcDirectoryToProject() { + getApplication().invokeAndWait(() -> getFixture().copyDirectoryToProject("src", ""), ModalityState.nonModal()); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/JBehaveSupportTestBase.java b/src/test/java/com/github/kumaraman21/intellijbehave/JBehaveSupportTestBase.java new file mode 100644 index 00000000..78933c59 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/JBehaveSupportTestBase.java @@ -0,0 +1,32 @@ +package com.github.kumaraman21.intellijbehave; + +import static com.intellij.openapi.application.ReadAction.compute; + +import com.intellij.openapi.projectRoots.JavaSdk; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor; +import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase5; + +/** + * Base test class for this plugin. + */ +public abstract class JBehaveSupportTestBase extends LightJavaCodeInsightFixtureTestCase5 { + + protected JBehaveSupportTestBase() { + super(new DefaultLightProjectDescriptor(() -> JavaSdk.getInstance().createJdk("Real JDK", System.getenv("JAVA_HOME"), false)) + .withRepositoryLibrary("org.jbehave:jbehave-core:5.2.0")); + } + + protected JBehaveSupportTestBase(DefaultLightProjectDescriptor projectDescriptor) { + super(projectDescriptor); + } + + protected int getCaretOffset() { + return compute(() -> getFixture().getCaretOffset()); + } + + protected PsiElement getParentOfElementAtCaretIn(PsiFile psiFile) { + return compute(() -> psiFile.findElementAt(getFixture().getCaretOffset()).getParent()); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/Samples.java b/src/test/java/com/github/kumaraman21/intellijbehave/Samples.java new file mode 100644 index 00000000..6078250f --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/Samples.java @@ -0,0 +1,178 @@ +package com.github.kumaraman21.intellijbehave; + +/** + * @author @aloyer + */ +public class Samples { + + public static final String SIMPLE_SAMPLE = + "Scenario: An unknown user cannot be logged\n" + // + "\n" + // + "Meta:\n" + // + "@skip\n" + // + "\n" + // + "Given i am the user with nickname: \"weird\"\n" + // + "When i try to login using the password \"soweird\"\n" + // + "Then i get an error message of type \"Wrong Credentials\"\n"; + + public static final String MULTILINE_SAMPLE = + "Scenario: An unknown user cannot be logged\n" + // + "\n" + // + "Meta:\n" + // + "@skip\n" + // + "\n" + // + "Given i am the user with\n nickname: \"weird\"\n" + // + "When i try to login using \nthe password\n \"soweird\"\n" + // + "Then i get an error\n message\n of type \"Wrong Credentials\"\n"; + + public static final String SIMPLE_FR = + "!-- language:fr\n" + + "Scénario: une simple sortie\n" + + "Etant donné que nous allons promener notre chienne\n" + + "!-- un commentaire qui n'a rien à voir\n" + + "Quand on sera dehors\n" + + "Alors elle pourra se soulager!\n" + + "Et elle sera super contente\n"; + + public static final String LONG_SAMPLE = + "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" + // + "@skip\n" + // + "\n" + // + "Given i am the user with nickname: \"weird\"\n" + // + "When i try to login using the password \"soweird\"\n" + // + "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\"\n" + // + "\n" + // + "\n" + // + "Scenario: A known user can be logged using the right 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 \"PacMan\"\n" + // + "Then i get logged\n" + // + "And a welcome message is displayed\n" + // + "\n"; + + public static final String MULTILINE_LONG_SAMPLE = + "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" + // + "@skip\n" + // + "\n" + // + "Given i am the user\n with nickname: \"weird\"\n" + // + "When i try to login using the password \"soweird\"\n" + // + "Then i get an error\n message of type \"Wrong Credentials\"\n" + // + "\n" + // + "\n" + // + "Scenario: A known user cannot be logged using a wrong password\n" + // + "\n" + // + "Given the following\n existing users:\n" + // + "| nickname | password |\n" + // + "| Travis | PacMan |\n" + // + "Given i am the user\n with nickname: \"Travis\"\n" + // + "And he is the user\n with nickname: \"Bomo\"\n" + // + "When i try to login\n using the password \"McCallum\"\n" + // + "And he tries to login\n using the password \"Bimo\"\n" + // + "Then i get an error\n message of type \"Wrong Credentials\"\n" + // + "\n" + // + "\n" + // + "Scenario: A known user can be logged using the right password\n" + // + "\n" + // + "Given the following\n existing users:\n" + // + "| nickname | password |\n" + // + "| Travis | PacMan |\n" + // + "Given i am the user with\n nickname: \"Travis\"\n" + // + "When i try to login using the\n password \"PacMan\"\n" + // + "Then i get logged\n" + // + "And a\n welcome message is displayed\n" + // + "When i try again to login using the\n password \"PacMan\"\n" + // + "Then i \nget logged\n" + // + "\n"; + + public static final String META_SAMPLE = + "Scenario: An unknown user cannot be logged\n" + // + "\n" + // + "Meta:\n" + // + "@author carmen\n" + // + "@skip\n" + // + "\n" + // + "Given i am the user with nickname: \"weird\"\n"; + + public static final String EXAMPLES_SAMPLE = + "Scenario: An unknown user cannot be logged\n" + // + "\n" + // + "Given i am the user with nickname: \"\"\n" + // + "When i try to login using the password \"soweird\"\n" + // + "Then i get an error message of type \"Wrong Credentials\"\n" + // + "\n" +// + "Examples: \n" + // + "| login | password |\n" + // + "| Travis | Pacman |\n" + // + "| Vlad | Thundercat |\n" + // + "\n" +// + "Scenario: A known user can be logged using the right password\n" + // + "\n" + // + "Given the following existing users:\n" + // + "| nickname | password |\n" + // + "| Travis | PacMan |\n" + // + "Given i am the user with nickname: \"Travis\"\n" // + ; + + public static final String COMPLEX_SAMPLE = + "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 turtle\n" + // + "!-- This scenario should be skipped until fixed\n" + // + "@skip\n" + // + "\n" + // + "Given i am the user with nickname: \"\"\n" + // + "!-- some weird and hard coded password\n" + // + "When i try to login using the password \"soweird\"\n" + // + "Then i get an error message of type \"Wrong Credentials\"\n" + // + "\n" +// + "Examples: \n" + // + "| login | password |\n" + // + "!-------------------------|\n" + // + "| Travis | Pacman |\n" + // + "| Vlad | Thundercat |\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\"\n" + // + "\n"; +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/codeInspector/ContentEntryInspectionTestBase.java b/src/test/java/com/github/kumaraman21/intellijbehave/codeInspector/ContentEntryInspectionTestBase.java new file mode 100644 index 00000000..07dc429b --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/codeInspector/ContentEntryInspectionTestBase.java @@ -0,0 +1,43 @@ +package com.github.kumaraman21.intellijbehave.codeInspector; + +import static com.intellij.openapi.application.ApplicationManager.getApplication; + +import com.github.kumaraman21.intellijbehave.ContentEntryProjectDescriptor; +import com.github.kumaraman21.intellijbehave.JBehaveSupportTestBase; +import com.intellij.codeInspection.InspectionProfileEntry; +import com.intellij.openapi.application.ModalityState; +import com.intellij.testFramework.TestDataFile; + +/** + * Base test class for inspections using content entries. + */ +abstract class ContentEntryInspectionTestBase extends JBehaveSupportTestBase { + + public ContentEntryInspectionTestBase() { + super(new ContentEntryProjectDescriptor().withRepositoryLibrary("org.jbehave:jbehave-core:5.2.0")); + } + + /** + * Configures the inspection to be tested. + */ + protected abstract InspectionProfileEntry getInspection(); + + /** + * The directory has to be copied in each related test method, instead of in a before hooks, + * so that it is called properly on EDT in write-safe context. + */ + protected void copySrcDirectoryToProject() { + getApplication().invokeAndWait(() -> getFixture().copyDirectoryToProject("src", ""), ModalityState.nonModal()); + } + + /** + * Tests inspection highlighting in the file located at the specified path. + * + * @param filePath the path to the file relative to the path returned by {@link #getTestDataPath()} + */ + protected void doTest(@TestDataFile String filePath) { + getFixture().configureByFile(filePath); + getFixture().enableInspections(getInspection()); + getFixture().testHighlighting(true, false, true); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/codeInspector/UndefinedStepInspectionTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/codeInspector/UndefinedStepInspectionTest.java new file mode 100644 index 00000000..55734d86 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/codeInspector/UndefinedStepInspectionTest.java @@ -0,0 +1,30 @@ +package com.github.kumaraman21.intellijbehave.codeInspector; + +import com.intellij.codeInspection.InspectionProfileEntry; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; + +/** + * Integration test for {@link UndefinedStepInspection}. + */ +class UndefinedStepInspectionTest extends ContentEntryInspectionTestBase { + + @Nullable + @Override + protected String getTestDataPath() { + return "src/test/testData/codeinspector/undefinedstep"; + } + + @Override + protected InspectionProfileEntry getInspection() { + return new UndefinedStepInspection(); + } + + @Test + void highlightingUndefinedSteps() { + copySrcDirectoryToProject(); + getFixture().copyFileToProject("main/java/OtherStepDefs.java"); + getFixture().copyFileToProject("main/java/StepDefs.java"); + doTest("src/test/resources/undefined_steps.story"); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/codeInspector/UnusedStepDeclarationInspectionTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/codeInspector/UnusedStepDeclarationInspectionTest.java new file mode 100644 index 00000000..98334462 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/codeInspector/UnusedStepDeclarationInspectionTest.java @@ -0,0 +1,30 @@ +package com.github.kumaraman21.intellijbehave.codeInspector; + +import com.intellij.codeInspection.InspectionProfileEntry; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; + +/** + * Integration test for {@link UnusedStepDeclarationInspection}. + */ +class UnusedStepDeclarationInspectionTest extends ContentEntryInspectionTestBase { + + @Nullable + @Override + protected String getTestDataPath() { + return "src/test/testData/codeinspector/unusedstepdeclarations"; + } + + @Override + protected InspectionProfileEntry getInspection() { + return new UnusedStepDeclarationInspection(); + } + + @Test + void highlightingUnusedStepDeclaration() { + copySrcDirectoryToProject(); + getFixture().copyFileToProject("main/java/OtherStepDefs.java"); + getFixture().copyFileToProject("test/resources/unused_step_declarations.story"); + doTest("main/java/StepDefs.java"); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLexerTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLexerTest.java new file mode 100644 index 00000000..78919f05 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLexerTest.java @@ -0,0 +1,378 @@ +package com.github.kumaraman21.intellijbehave.highlighter; + +import static com.github.kumaraman21.intellijbehave.Samples.COMPLEX_SAMPLE; +import static com.github.kumaraman21.intellijbehave.Samples.EXAMPLES_SAMPLE; +import static com.github.kumaraman21.intellijbehave.Samples.LONG_SAMPLE; +import static com.github.kumaraman21.intellijbehave.Samples.META_SAMPLE; +import static com.github.kumaraman21.intellijbehave.Samples.MULTILINE_LONG_SAMPLE; +import static com.github.kumaraman21.intellijbehave.Samples.MULTILINE_SAMPLE; +import static com.github.kumaraman21.intellijbehave.Samples.SIMPLE_SAMPLE; +import static com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType.EXAMPLE_TYPE; +import static com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType.GIVEN_TYPE; +import static com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType.META; +import static com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType.META_KEY; +import static com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType.META_TEXT; +import static com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType.SCENARIO_TEXT; +import static com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType.SCENARIO_TYPE; +import static com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType.STEP_TEXT; +import static com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType.STORY_DESCRIPTION; +import static com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType.TABLE_CELL; +import static com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType.TABLE_DELIM; +import static com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType.THEN_TYPE; +import static com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType.WHEN_TYPE; +import static com.github.kumaraman21.intellijbehave.highlighter.StoryTokenType.WHITE_SPACE; +import static org.assertj.core.api.Assertions.assertThat; + +import com.intellij.psi.tree.IElementType; +import com.intellij.testFramework.fixtures.BasePlatformTestCase; + +/** + * @author @aloyer + */ +public class StoryLexerTest extends BasePlatformTestCase { + + private StoryLexer storyLexer; + + public void _testTraceAll() { + //traceAll(SIMPLE_SAMPLE); + //traceAll(LONG_SAMPLE); + //traceAll(META_SAMPLE); + //traceAll(EXAMPLES_SAMPLE); + traceAll(COMPLEX_SAMPLE); + } + + public void testParseSimpleSample() { + storyLexer = new StoryLexer(); + storyLexer.start(SIMPLE_SAMPLE); + + assertToken(SCENARIO_TYPE, "Scenario: "); + advanceAndAssert(SCENARIO_TEXT, "An unknown user cannot be logged"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(META, "Meta:"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(META_KEY, "@skip"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(GIVEN_TYPE, "Given "); + advanceAndAssert(STEP_TEXT, "i am the user with nickname: \"weird\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHEN_TYPE, "When "); + advanceAndAssert(STEP_TEXT, "i try to login using the password \"soweird\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(THEN_TYPE, "Then "); + advanceAndAssert(STEP_TEXT, "i get an error message of type \"Wrong Credentials\""); + advanceAndAssert(WHITE_SPACE); + } + + public void testParseMultilineSample() { + storyLexer = new StoryLexer(); + storyLexer.start(MULTILINE_SAMPLE); + + assertToken(SCENARIO_TYPE, "Scenario: "); + advanceAndAssert(SCENARIO_TEXT, "An unknown user cannot be logged"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(META, "Meta:"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(META_KEY, "@skip"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(GIVEN_TYPE, "Given "); + advanceAndAssert(STEP_TEXT, "i am the user with nickname: \"weird\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHEN_TYPE, "When "); + advanceAndAssert(STEP_TEXT, "i try to login using the password \"soweird\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(THEN_TYPE, "Then "); + advanceAndAssert(STEP_TEXT, "i get an error message of type \"Wrong Credentials\""); + advanceAndAssert(WHITE_SPACE); + } + + public void testParseMultilineLong() { + storyLexer = new StoryLexer(); + storyLexer.start(MULTILINE_LONG_SAMPLE); + + assertToken(STORY_DESCRIPTION, "Narrative: "); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(STORY_DESCRIPTION, "In order to play a game"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(STORY_DESCRIPTION, "As a player"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(STORY_DESCRIPTION, "I want to be able to create and manage my account"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(SCENARIO_TYPE, "Scenario: "); + advanceAndAssert(SCENARIO_TEXT, "An unknown user cannot be logged"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(META, "Meta:"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(META_KEY, "@skip"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(GIVEN_TYPE, "Given "); + advanceAndAssert(STEP_TEXT, "i am the user with nickname: \"weird\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHEN_TYPE, "When "); + advanceAndAssert(STEP_TEXT, "i try to login using the password \"soweird\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(THEN_TYPE, "Then "); + advanceAndAssert(STEP_TEXT, "i get an error message of type \"Wrong Credentials\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(SCENARIO_TYPE, "Scenario: "); + advanceAndAssert(SCENARIO_TEXT, "A known user cannot be logged using a wrong password"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(GIVEN_TYPE, "Given "); + advanceAndAssert(STEP_TEXT, "the following existing users:"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " nickname "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " password "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " Travis "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " PacMan "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(GIVEN_TYPE, "Given "); + advanceAndAssert(STEP_TEXT, "i am the user with nickname: \"Travis\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(GIVEN_TYPE, "And "); + advanceAndAssert(STEP_TEXT, "he is the user with nickname: \"Bomo\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHEN_TYPE, "When "); + advanceAndAssert(STEP_TEXT, "i try to login using the password \"McCallum\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHEN_TYPE, "And "); + advanceAndAssert(STEP_TEXT, "he tries to login using the password \"Bimo\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(THEN_TYPE, "Then "); + advanceAndAssert(STEP_TEXT, "i get an error message of type \"Wrong Credentials\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(SCENARIO_TYPE, "Scenario: "); + advanceAndAssert(SCENARIO_TEXT, "A known user can be logged using the right password"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(GIVEN_TYPE, "Given "); + advanceAndAssert(STEP_TEXT, "the following existing users:"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " nickname "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " password "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " Travis "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " PacMan "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(GIVEN_TYPE, "Given "); + advanceAndAssert(STEP_TEXT, "i am the user with nickname: \"Travis\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHEN_TYPE, "When "); + advanceAndAssert(STEP_TEXT, "i try to login using the password \"PacMan\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(THEN_TYPE, "Then "); + advanceAndAssert(STEP_TEXT, "i get logged"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(THEN_TYPE, "And "); + advanceAndAssert(STEP_TEXT, "a welcome message is displayed"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHEN_TYPE, "When "); + advanceAndAssert(STEP_TEXT, "i try again to login using the password \"PacMan\""); + advanceAndAssert(WHITE_SPACE); + } + + public void testParseMetaSample() { + storyLexer = new StoryLexer(); + storyLexer.start(META_SAMPLE); + + assertToken(SCENARIO_TYPE, "Scenario: "); + advanceAndAssert(SCENARIO_TEXT, "An unknown user cannot be logged"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(META, "Meta:"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(META_KEY, "@author"); + advanceAndAssert(META_TEXT, " carmen"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(META_KEY, "@skip"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(GIVEN_TYPE, "Given "); + advanceAndAssert(STEP_TEXT, "i am the user with nickname: \"weird\""); + advanceAndAssert(WHITE_SPACE); + } + + public void testParseLongSample() { + storyLexer = new StoryLexer(); + storyLexer.start(LONG_SAMPLE); + + assertToken(STORY_DESCRIPTION, "Narrative: "); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(STORY_DESCRIPTION, "In order to play a game"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(STORY_DESCRIPTION, "As a player"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(STORY_DESCRIPTION, "I want to be able to create and manage my account"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(SCENARIO_TYPE, "Scenario: "); + advanceAndAssert(SCENARIO_TEXT, "An unknown user cannot be logged"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(META, "Meta:"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(META_KEY, "@skip"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(GIVEN_TYPE, "Given "); + advanceAndAssert(STEP_TEXT, "i am the user with nickname: \"weird\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHEN_TYPE, "When "); + advanceAndAssert(STEP_TEXT, "i try to login using the password \"soweird\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(THEN_TYPE, "Then "); + advanceAndAssert(STEP_TEXT, "i get an error message of type \"Wrong Credentials\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(SCENARIO_TYPE, "Scenario: "); + advanceAndAssert(SCENARIO_TEXT, "A known user cannot be logged using a wrong password"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(GIVEN_TYPE, "Given "); + advanceAndAssert(STEP_TEXT, "the following existing users:"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " nickname "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " password "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " Travis "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " PacMan "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(GIVEN_TYPE, "Given "); + advanceAndAssert(STEP_TEXT, "i am the user with nickname: \"Travis\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHEN_TYPE, "When "); + advanceAndAssert(STEP_TEXT, "i try to login using the password \"McCallum\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(THEN_TYPE, "Then "); + advanceAndAssert(STEP_TEXT, "i get an error message of type \"Wrong Credentials\""); + advanceAndAssert(WHITE_SPACE); + + // ... + } + + public void testParseExamples() { + storyLexer = new StoryLexer(); + storyLexer.start(EXAMPLES_SAMPLE); + + assertToken(SCENARIO_TYPE, "Scenario: "); + advanceAndAssert(SCENARIO_TEXT, "An unknown user cannot be logged"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(GIVEN_TYPE, "Given "); + advanceAndAssert(STEP_TEXT, "i am the user with nickname: \"\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHEN_TYPE, "When "); + advanceAndAssert(STEP_TEXT, "i try to login using the password \"soweird\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(THEN_TYPE, "Then "); + advanceAndAssert(STEP_TEXT, "i get an error message of type \"Wrong Credentials\""); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(EXAMPLE_TYPE, "Examples:"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " login "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " password "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " Travis "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " Pacman "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " Vlad "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(TABLE_CELL, " Thundercat "); + advanceAndAssert(TABLE_DELIM); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(SCENARIO_TYPE, "Scenario: "); + advanceAndAssert(SCENARIO_TEXT, "A known user can be logged using the right password"); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(WHITE_SPACE); + advanceAndAssert(GIVEN_TYPE, "Given "); + advanceAndAssert(STEP_TEXT, "the following existing users:"); + advanceAndAssert(WHITE_SPACE); + + // ... + } + + private void advanceAndAssert(final IElementType storyTokenType) { + storyLexer.advance(); + assertThat(storyLexer.getTokenType()).isEqualTo(storyTokenType); + } + + private void advanceAndAssert(final IElementType storyTokenType, final String content) { + storyLexer.advance(); + assertToken(storyTokenType, content); + } + + private void assertToken(final IElementType storyTokenType, final String content) { + assertThat(storyLexer.getTokenType()).isEqualTo(storyTokenType); + assertThat(storyLexer.getTokenText().replaceAll("\\n", "")).isEqualTo(content); + } + + private void traceAll(final String content) { + final StoryLexer storyLexer = new StoryLexer(); + storyLexer.start(content); + + IElementType tokenType; + do { + tokenType = storyLexer.getTokenType(); + System.out.println("[" + + rightPad(tokenType, "STORY_DESCRIPTION".length()) + "]" + + rightPad(storyLexer.lexerState(), "IN_DIRECTIVE".length()) + + ": >>" + escape(storyLexer.getTokenSequence()) + "<<"); + + storyLexer.advance(); + tokenType = storyLexer.getTokenType(); + } + while (tokenType != null); + } + + private String rightPad(final Object object, final int length) { + final StringBuilder builder = new StringBuilder(object.toString()); + while (builder.length() < length) { + builder.append(" "); + } + return builder.toString(); + } + + private String escape(final CharSequence tokenSequence) { + return tokenSequence.toString().replace("\n", "\\n").replace("\r", "\\r"); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLocalizedLexer_FrenchTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLocalizedLexer_FrenchTest.java new file mode 100644 index 00000000..4e911b68 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLocalizedLexer_FrenchTest.java @@ -0,0 +1,79 @@ +package com.github.kumaraman21.intellijbehave.highlighter; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.kumaraman21.intellijbehave.utility.LocalizedStorySupport; +import com.intellij.psi.tree.IElementType; +import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import org.jetbrains.annotations.Nullable; + +/** + * @author @aloyer + */ +public class StoryLocalizedLexer_FrenchTest extends BasePlatformTestCase { + + private StoryLocalizedLexer storyLexer; + + public void test_parse_basicScenario() { + storyLexer = new StoryLocalizedLexer(new LocalizedStorySupport()); + storyLexer.changeLocale("fr"); + storyLexer.start("Scénario: une simple sortie\n" + + "Etant donné que nous allons promener notre chienne\n" + + "Quand on sera dehors\n" + + "Alors elle pourra se soulager!"); + + assertToken(StoryTokenType.SCENARIO_TYPE, "Scénario:"); + advanceAndAssert(StoryTokenType.SCENARIO_TEXT, " une simple sortie"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_GIVEN, "Etant donné que"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " nous allons promener notre chienne"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_WHEN, "Quand"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " on sera dehors"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_THEN, "Alors"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " elle pourra se soulager!"); + advanceAndAssert(null); + } + + public void test_parse_commentAllowsToSwitchLanguage() { + storyLexer = new StoryLocalizedLexer(new LocalizedStorySupport()); + // make sure one is not in fr by default + storyLexer.changeLocale("en"); + storyLexer.start("!-- language:fr\n" + + "Scénario: une simple sortie\n" + + "Etant donné que nous allons promener notre chienne\n" + + "Quand on sera dehors\n" + + "Alors elle pourra se soulager!"); + + assertToken(StoryTokenType.COMMENT_WITH_LOCALE, "!-- language:fr"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.SCENARIO_TYPE, "Scénario:"); + advanceAndAssert(StoryTokenType.SCENARIO_TEXT, " une simple sortie"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_GIVEN, "Etant donné que"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " nous allons promener notre chienne"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_WHEN, "Quand"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " on sera dehors"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_THEN, "Alors"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " elle pourra se soulager!"); + advanceAndAssert(null); + } + + private void advanceAndAssert(@Nullable IElementType storyTokenType) { + storyLexer.advance(); + assertThat(storyLexer.getTokenType()).isEqualTo(storyTokenType); + } + + private void advanceAndAssert(IElementType storyTokenType, String content) { + storyLexer.advance(); + assertToken(storyTokenType, content); + } + + private void assertToken(IElementType storyTokenType, String content) { + assertThat(storyLexer.getTokenSequence()).isEqualTo(content); + assertThat(storyLexer.getTokenType()).isEqualTo(storyTokenType); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLocalizedLexer_MalformedTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLocalizedLexer_MalformedTest.java new file mode 100644 index 00000000..3c00cda2 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLocalizedLexer_MalformedTest.java @@ -0,0 +1,115 @@ +package com.github.kumaraman21.intellijbehave.highlighter; + +import com.github.kumaraman21.intellijbehave.utility.LocalizedStorySupport; +import com.intellij.psi.tree.IElementType; +import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import org.jetbrains.annotations.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author @aloyer + */ +public class StoryLocalizedLexer_MalformedTest extends BasePlatformTestCase { + + private StoryLocalizedLexer storyLexer; + + public void testParse_tableWithoutPreamble() { + storyLexer = new StoryLocalizedLexer(new LocalizedStorySupport()); + storyLexer.start("| Travis | Pacman |"); + + assertToken(StoryTokenType.TABLE_DELIM, "|"); + advanceAndAssert(StoryTokenType.TABLE_CELL, " Travis "); + advanceAndAssert(StoryTokenType.TABLE_DELIM, "|"); + advanceAndAssert(StoryTokenType.TABLE_CELL, " Pacman "); + advanceAndAssert(StoryTokenType.TABLE_DELIM, "|"); + advanceAndAssert(null); + } + + public void testParse_scenarioWithoutText() { + storyLexer = new StoryLocalizedLexer(new LocalizedStorySupport()); + storyLexer.start("Scenario:\n" + + "Given\n" + + "When\n" + + "Then\n" + + "And\n"); + + assertToken(StoryTokenType.SCENARIO_TYPE, "Scenario:"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_GIVEN, "Given"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_WHEN, "When"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_THEN, "Then"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_AND, "And"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(null); + } + + public void testParse_scenarioWithStepAsText() { + storyLexer = new StoryLocalizedLexer(new LocalizedStorySupport()); + storyLexer.start("Scenario: Given a nice\n" + + "Given\n" + + "When\n" + + "Then\n"); + + assertToken(StoryTokenType.SCENARIO_TYPE, "Scenario:"); + advanceAndAssert(StoryTokenType.SCENARIO_TEXT, " Given a nice"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_GIVEN, "Given"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_WHEN, "When"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_THEN, "Then"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(null); + } + + private void advanceAndAssert(@Nullable IElementType storyTokenType) { + storyLexer.advance(); + assertThat(storyLexer.getTokenType()).isEqualTo(storyTokenType); + } + + private void advanceAndAssert(IElementType storyTokenType, String content) { + storyLexer.advance(); + assertToken(storyTokenType, content); + } + + private void assertToken(IElementType storyTokenType, String content) { + assertThat(storyLexer.getTokenSequence()).isEqualTo(content); + assertThat(storyLexer.getTokenType()).isEqualTo(storyTokenType); + } + + private static void traceAll(String content, StoryLocalizedLexer storyLexer) { + storyLexer.start(content); + + IElementType tokenType; + do { + tokenType = storyLexer.getTokenType(); + System.out.println( + rightPad("" + storyLexer.getPosition(), 3) + " " + + "[" + rightPad(tokenType, "STORY_DESCRIPTION".length()) + "]" + + rightPad(storyLexer.lexerState(), "IN_DIRECTIVE".length()) + + ": >>" + escape(storyLexer.getTokenSequence()) + "<<"); + + storyLexer.advance(); + tokenType = storyLexer.getTokenType(); + } + while (tokenType != null); + } + + private static String rightPad(Object object, int length) { + StringBuilder builder = new StringBuilder(object.toString()); + while (builder.length() < length) { + builder.append(" "); + } + return builder.toString(); + } + + private static String escape(CharSequence tokenSequence) { + return tokenSequence.toString().replace("\n", "\\n").replace("\r", "\\r"); + } + + +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLocalizedLexer_SamplesTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLocalizedLexer_SamplesTest.java new file mode 100644 index 00000000..8560d08e --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/highlighter/StoryLocalizedLexer_SamplesTest.java @@ -0,0 +1,226 @@ +package com.github.kumaraman21.intellijbehave.highlighter; + +import com.github.kumaraman21.intellijbehave.utility.LocalizedStorySupport; +import com.intellij.psi.tree.IElementType; +import com.intellij.testFramework.fixtures.BasePlatformTestCase; + +import static com.github.kumaraman21.intellijbehave.Samples.EXAMPLES_SAMPLE; +import static com.github.kumaraman21.intellijbehave.Samples.LONG_SAMPLE; +import static com.github.kumaraman21.intellijbehave.Samples.META_SAMPLE; +import static com.github.kumaraman21.intellijbehave.Samples.SIMPLE_SAMPLE; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author @aloyer + */ +public class StoryLocalizedLexer_SamplesTest extends BasePlatformTestCase { + + private StoryLocalizedLexer storyLexer; + + public void _testTraceAll() { + //traceAll(SIMPLE_SAMPLE); + traceAll(LONG_SAMPLE); + //traceAll(META_SAMPLE); + //traceAll(EXAMPLES_SAMPLE); + //traceAll(COMPLEX_SAMPLE); + } + + public void testParseSimpleSample() { + storyLexer = new StoryLocalizedLexer(new LocalizedStorySupport()); + storyLexer.start(SIMPLE_SAMPLE); + + assertToken(StoryTokenType.SCENARIO_TYPE, "Scenario:"); + advanceAndAssert(StoryTokenType.SCENARIO_TEXT, " An unknown user cannot be logged"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.META, "Meta:"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.META_KEY, "@skip"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_GIVEN, "Given"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " i am the user with nickname: \"weird\""); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_WHEN, "When"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " i try to login using the password \"soweird\""); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_THEN, "Then"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " i get an error message of type \"Wrong Credentials\""); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + } + + public void testParseMetaSample() { + storyLexer = new StoryLocalizedLexer(new LocalizedStorySupport()); + storyLexer.start(META_SAMPLE); + + assertToken(StoryTokenType.SCENARIO_TYPE, "Scenario:"); + advanceAndAssert(StoryTokenType.SCENARIO_TEXT, " An unknown user cannot be logged"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.META, "Meta:"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.META_KEY, "@author"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.META_TEXT, "carmen"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.META_KEY, "@skip"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_GIVEN, "Given"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " i am the user with nickname: \"weird\""); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + } + + public void testParseLongSample() { + storyLexer = new StoryLocalizedLexer(new LocalizedStorySupport()); + storyLexer.start(LONG_SAMPLE); + + assertToken(StoryTokenType.NARRATIVE_TYPE, "Narrative:"); + advanceAndAssert(StoryTokenType.STORY_DESCRIPTION, " "); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.NARRATIVE_TYPE, "In order to"); + advanceAndAssert(StoryTokenType.STORY_DESCRIPTION, " play a game"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.NARRATIVE_TYPE, "As a"); + advanceAndAssert(StoryTokenType.STORY_DESCRIPTION, " player"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.NARRATIVE_TYPE, "I want to"); + advanceAndAssert(StoryTokenType.STORY_DESCRIPTION, " be able to create and manage my account"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.SCENARIO_TYPE, "Scenario:"); + advanceAndAssert(StoryTokenType.SCENARIO_TEXT, " An unknown user cannot be logged"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.META, "Meta:"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.META_KEY, "@skip"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_GIVEN, "Given"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " i am the user with nickname: \"weird\""); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_WHEN, "When"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " i try to login using the password \"soweird\""); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_THEN, "Then"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " i get an error message of type \"Wrong Credentials\""); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.SCENARIO_TYPE, "Scenario:"); + advanceAndAssert(StoryTokenType.SCENARIO_TEXT, " A known user cannot be logged using a wrong password"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_GIVEN, "Given"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " the following existing users:"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.TABLE_DELIM); + advanceAndAssert(StoryTokenType.TABLE_CELL, " nickname "); + advanceAndAssert(StoryTokenType.TABLE_DELIM); + advanceAndAssert(StoryTokenType.TABLE_CELL, " password "); + advanceAndAssert(StoryTokenType.TABLE_DELIM); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.TABLE_DELIM); + advanceAndAssert(StoryTokenType.TABLE_CELL, " Travis "); + advanceAndAssert(StoryTokenType.TABLE_DELIM); + advanceAndAssert(StoryTokenType.TABLE_CELL, " PacMan "); + advanceAndAssert(StoryTokenType.TABLE_DELIM); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_GIVEN, "Given"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " i am the user with nickname: \"Travis\""); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_WHEN, "When"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " i try to login using the password \"McCallum\""); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_THEN, "Then"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " i get an error message of type \"Wrong Credentials\""); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + + // ... + } + + public void testParseExamples() { + storyLexer = new StoryLocalizedLexer(new LocalizedStorySupport()); + storyLexer.start(EXAMPLES_SAMPLE); + + assertToken(StoryTokenType.SCENARIO_TYPE, "Scenario:"); + advanceAndAssert(StoryTokenType.SCENARIO_TEXT, " An unknown user cannot be logged"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_GIVEN, "Given"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " i am the user with nickname: \"\""); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_WHEN, "When"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " i try to login using the password \"soweird\""); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_THEN, "Then"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " i get an error message of type \"Wrong Credentials\""); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.EXAMPLE_TYPE, "Examples:"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.TABLE_DELIM); + advanceAndAssert(StoryTokenType.TABLE_CELL, " login "); + advanceAndAssert(StoryTokenType.TABLE_DELIM); + advanceAndAssert(StoryTokenType.TABLE_CELL, " password "); + advanceAndAssert(StoryTokenType.TABLE_DELIM); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.TABLE_DELIM); + advanceAndAssert(StoryTokenType.TABLE_CELL, " Travis "); + advanceAndAssert(StoryTokenType.TABLE_DELIM); + advanceAndAssert(StoryTokenType.TABLE_CELL, " Pacman "); + advanceAndAssert(StoryTokenType.TABLE_DELIM); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.TABLE_DELIM); + advanceAndAssert(StoryTokenType.TABLE_CELL, " Vlad "); + advanceAndAssert(StoryTokenType.TABLE_DELIM); + advanceAndAssert(StoryTokenType.TABLE_CELL, " Thundercat "); + advanceAndAssert(StoryTokenType.TABLE_DELIM); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.SCENARIO_TYPE, "Scenario:"); + advanceAndAssert(StoryTokenType.SCENARIO_TEXT, " A known user can be logged using the right password"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + advanceAndAssert(StoryTokenType.STEP_TYPE_GIVEN, "Given"); + advanceAndAssert(StoryTokenType.STEP_TEXT, " the following existing users:"); + advanceAndAssert(StoryTokenType.WHITE_SPACE); + + // ... + } + + private void advanceAndAssert(IElementType storyTokenType) { + storyLexer.advance(); + assertThat(storyLexer.getTokenType()).isEqualTo(storyTokenType); + } + + private void advanceAndAssert(IElementType storyTokenType, String content) { + storyLexer.advance(); + assertToken(storyTokenType, content); + } + + private void assertToken(IElementType storyTokenType, String content) { + assertThat(storyLexer.getTokenSequence()).isEqualTo(content); + assertThat(storyLexer.getTokenType()).isEqualTo(storyTokenType); + } + + private void traceAll(String content) { + storyLexer = new StoryLocalizedLexer(new LocalizedStorySupport()); + storyLexer.start(content); + + IElementType tokenType; + do { + tokenType = storyLexer.getTokenType(); + System.out.println( + rightPad("" + storyLexer.getPosition(), 3) + " " + + "[" + rightPad(tokenType, "STORY_DESCRIPTION".length()) + "]" + + rightPad(storyLexer.lexerState(), "IN_DIRECTIVE".length()) + + ": >>" + escape(storyLexer.getTokenSequence()) + "<<"); + + storyLexer.advance(); + tokenType = storyLexer.getTokenType(); + } + while (tokenType != null); + } + + private String rightPad(Object object, int length) { + StringBuilder builder = new StringBuilder(object.toString()); + while (builder.length() < length) { + builder.append(" "); + } + return builder.toString(); + } + + private String escape(CharSequence tokenSequence) { + return tokenSequence.toString().replace("\n", "\\n").replace("\r", "\\r"); + } + + +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilderTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilderTest.java new file mode 100644 index 00000000..0f32fe74 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilderTest.java @@ -0,0 +1,69 @@ +package com.github.kumaraman21.intellijbehave.jbehave.core.steps; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import java.util.Set; + +/** + * Unit test for {@link PatternVariantBuilder}. + *

+ * Modified version of {@code org.jbehave.core.steps.PatternVariantBuilderBehaviour}. + */ +class PatternVariantBuilderTest { + + @Test + void shouldReturnItselfForNoPatternString() { + PatternVariantBuilder builder = new PatternVariantBuilder("No variants"); + assertThat(builder.getInput()).isEqualTo("No variants"); + Set variants = builder.allVariants(); + assertThat(variants.iterator().next()).isEqualTo("No variants"); + assertThat(variants).hasSize(1); + } + + @Test + void shouldReturnTwoVariantsForOnePattern() { + PatternVariantBuilder builder = new PatternVariantBuilder("There are {Two|One} variants"); + assertThat(builder.getInput()).isEqualTo("There are {Two|One} variants"); + Set result = builder.allVariants(); + assertThat(result).hasSize(2).contains("There are One variants", "There are Two variants"); + } + + @Test + void shouldReturnFourVariantsForTwoPatterns() { + PatternVariantBuilder builder = new PatternVariantBuilder("There are {Two|One} variants, {hooray|alas}!"); + Set result = builder.allVariants(); + assertThat(result).hasSize(4) + .contains("There are One variants, hooray!", "There are Two variants, hooray!", "There are One variants, alas!", "There are Two variants, alas!"); + } + + @Test + void shouldReturnFourVariantsForTwoPatternsWithOptionElements() { + PatternVariantBuilder builder = new PatternVariantBuilder("There are {One|} variants{, hooray|}!"); + Set result = builder.allVariants(); + assertThat(result).hasSize(4) + .contains("There are One variants, hooray!", "There are variants, hooray!", "There are One variants!", "There are variants!"); + } + + @Test + void shouldHandleSpecialCharacters() { + PatternVariantBuilder builder = new PatternVariantBuilder("When $A {+|plus|is added to} $B"); + Set result = builder.allVariants(); + assertThat(result).hasSize(3).contains("When $A + $B", "When $A plus $B", "When $A is added to $B"); + } + + @Test + void hasUnclosedBracket() { + PatternVariantBuilder builder = new PatternVariantBuilder("When $A {+|plus|is added to $B"); + Set result = builder.allVariants(); + assertThat(result).hasSize(1).contains("When $A {+|plus|is added to $B"); + } + + @Test + void hasUnclosedBrackets() { + PatternVariantBuilder builder = new PatternVariantBuilder("When $A {+|plus|is added to} $B and }{$C"); + Set result = builder.allVariants(); + assertThat(result).hasSize(3).contains("When $A + $B and }{$C", "When $A plus $B and }{$C", "When $A is added to $B and }{$C"); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/parser/IntelliJBehaveBaseTestCase.java b/src/test/java/com/github/kumaraman21/intellijbehave/parser/IntelliJBehaveBaseTestCase.java new file mode 100644 index 00000000..557e0a03 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/parser/IntelliJBehaveBaseTestCase.java @@ -0,0 +1,80 @@ +package com.github.kumaraman21.intellijbehave.parser; + +import com.github.kumaraman21.intellijbehave.Samples; +import com.github.kumaraman21.intellijbehave.highlighter.StoryLexerFactory; +import com.intellij.lang.ASTNode; +import com.intellij.lang.PsiBuilder; +import com.intellij.lang.impl.PsiBuilderImpl; +import com.intellij.psi.impl.DebugUtil; +import com.intellij.testFramework.fixtures.BasePlatformTestCase; + +/** + * @author @aloyer + */ +public class IntelliJBehaveBaseTestCase extends BasePlatformTestCase { + +// protected CodeStyleSettings mySettings; + +// protected CodeStyleSettings getSettings() { +// return CodeStyle.getSettings(myProject); +// } + +// protected void setSettings() { +// final StoryFileType fileType = StoryFileType.STORY_FILE_TYPE; +// mySettings = getSettings(); +// mySettings.getIndentOptions(fileType).INDENT_SIZE = 2; +// mySettings.getIndentOptions(fileType).CONTINUATION_INDENT_SIZE = 2; +// mySettings.getIndentOptions(fileType).TAB_SIZE = 2; +// } + + @Override + protected void setUp() throws Exception { + super.setUp(); + System.setProperty("idea.platform.prefix", "Idea"); + } + + public void testCase_Simple() { + ASTNode astNode = doParse(Samples.SIMPLE_SAMPLE); + System.out.println("IntelliJBehaveBaseTestCase.testCase_Simple: " + DebugUtil.treeToString(astNode, false)); + } + + public void testCase_Long() { + ASTNode astNode = doParse(Samples.LONG_SAMPLE); + System.out.println("IntelliJBehaveBaseTestCase.testCase_Long: " + DebugUtil.treeToString(astNode, false)); + } + + public void testCase_Meta() { + ASTNode astNode = doParse(Samples.META_SAMPLE); + System.out.println("IntelliJBehaveBaseTestCase.testCase_Meta: " + DebugUtil.treeToString(astNode, false)); + } + + public void testCase_Examples() { + ASTNode astNode = doParse(Samples.EXAMPLES_SAMPLE); + System.out.println("IntelliJBehaveBaseTestCase.testCase_Examples: " + DebugUtil.treeToString(astNode, false)); + } + + public void testCase_Complex() { + ASTNode astNode = doParse(Samples.COMPLEX_SAMPLE); + System.out.println("IntelliJBehaveBaseTestCase.testCase_Complex: " + DebugUtil.treeToString(astNode, false)); + } + + public void testCase_SimpleFR() { + ASTNode astNode = doParse(Samples.SIMPLE_FR); + System.out.println("IntelliJBehaveBaseTestCase.testCase_Complex: " + DebugUtil.treeToString(astNode, false)); + } + + private ASTNode doParse(String content) { + PsiBuilder builder = new PsiBuilderImpl(getProject(), + null, + new StoryParserDefinition(), + new StoryLexerFactory().createLexer(), + null, + content, + null, + null); + + StoryParser parser = new StoryParser(); + return parser.parse(StoryElementType.STORY_FILE, builder); + } + +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/parser/StoryParserTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/parser/StoryParserTest.java new file mode 100644 index 00000000..b979bd7c --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/parser/StoryParserTest.java @@ -0,0 +1,26 @@ +package com.github.kumaraman21.intellijbehave.parser; + +import com.github.kumaraman21.intellijbehave.Samples; +import com.github.kumaraman21.intellijbehave.highlighter.StoryLexer; +import com.intellij.lang.PsiBuilder; +import com.intellij.lang.impl.PsiBuilderImpl; + +/** + * @author @aloyer + */ +public class StoryParserTest extends IntelliJBehaveBaseTestCase { + + public void _testParseStoryFile() { + PsiBuilder builder = new PsiBuilderImpl(getProject(), + null, + new StoryParserDefinition(), + new StoryLexer(), + null, + Samples.SIMPLE_SAMPLE, + null, + null); + + StoryParser parser = new StoryParser(); + parser.parse(StoryElementType.STORY_FILE, builder); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotationConverterTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotationConverterTest.java new file mode 100644 index 00000000..8b2c89e1 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotationConverterTest.java @@ -0,0 +1,265 @@ +package com.github.kumaraman21.intellijbehave.resolver; + +import static com.intellij.openapi.application.ReadAction.compute; +import static com.intellij.openapi.application.ReadAction.run; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.kumaraman21.intellijbehave.ContentEntryTestBase; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiClassOwner; +import org.jbehave.core.steps.StepType; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Integration test for {@link StepDefinitionAnnotationConverter}. + */ +public class StepDefinitionAnnotationConverterTest extends ContentEntryTestBase { + + @Nullable + @Override + protected String getTestDataPath() { + return "src/test/testData/resolver/stepdefannotationconverter"; + } + + @BeforeEach + void setUp() { + copySrcDirectoryToProject(); + } + + @Test + void convertsEmptyAnnotationsArrayToEmptySet() { + //When + var annotations = StepDefinitionAnnotationConverter.convertFrom(PsiAnnotation.EMPTY_ARRAY); + + //Then + assertThat(annotations).isEmpty(); + } + + @Test + void convertsSingleGivenAnnotationToSingleSet() { + //Given + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + var psiAnnotations = compute(() -> { + var stepDefsClass = ((PsiClassOwner) stepDefFile).getClasses()[0]; + var givenStepDef = stepDefsClass.findMethodsByName("givenStepDefWithOneAnnotationAttribute", false)[0]; + return givenStepDef.getAnnotations(); + }); + + //When + var annotations = StepDefinitionAnnotationConverter.convertFrom(psiAnnotations); + + //Then + var attribute = new StepDefinitionAnnotation(StepType.GIVEN, "one given $string", psiAnnotations[0]); + assertThat(annotations).containsExactly(attribute); + } + + @Test + void convertsSingleWhenAnnotationToSingleSet() { + //Given + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + var psiAnnotations = compute(() -> { + var stepDefsClass = ((PsiClassOwner) stepDefFile).getClasses()[0]; + var whenStepDef = stepDefsClass.findMethodsByName("whenStepDefWithOneAnnotationAttribute", false)[0]; + return whenStepDef.getAnnotations(); + }); + + //When + var annotations = StepDefinitionAnnotationConverter.convertFrom(psiAnnotations); + + //Then + var attribute = new StepDefinitionAnnotation(StepType.WHEN, "one when $string", psiAnnotations[0]); + assertThat(annotations).containsExactly(attribute); + } + + @Test + void convertsSingleThenAnnotationToSingleSet() { + //Given + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + var psiAnnotations = compute(() -> { + var stepDefsClass = ((PsiClassOwner) stepDefFile).getClasses()[0]; + var thenStepDef = stepDefsClass.findMethodsByName("thenStepDefWithOneAnnotationAttribute", false)[0]; + return thenStepDef.getAnnotations(); + }); + + //When + var annotations = StepDefinitionAnnotationConverter.convertFrom(psiAnnotations); + + //Then + var attribute = new StepDefinitionAnnotation(StepType.THEN, "one then $string", psiAnnotations[0]); + assertThat(annotations).containsExactly(attribute); + } + + @Test + void convertsSingleAliasAnnotationToSingleSet() { + //Given + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + var psiAnnotations = compute(() -> { + var stepDefsClass = ((PsiClassOwner) stepDefFile).getClasses()[0]; + var aliasStepDef = stepDefsClass.findMethodsByName("aliasStepDefWithOneAnnotationAttribute", false)[0]; + return aliasStepDef.getAnnotations(); + }); + + //When + var annotations = StepDefinitionAnnotationConverter.convertFrom(psiAnnotations); + + //Then + var attribute = new StepDefinitionAnnotation(null, "one alias $string", psiAnnotations[0]); + assertThat(annotations).containsExactly(attribute); + } + + @Test + void convertsSingleAliasesAnnotationWithSingleAttributeToSingleSet() { + //Given + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + var psiAnnotations = compute(() -> { + var stepDefsClass = ((PsiClassOwner) stepDefFile).getClasses()[0]; + var aliasesStepDef = stepDefsClass.findMethodsByName("aliasesStepDefWithOneAnnotationAttribute", false)[0]; + return aliasesStepDef.getAnnotations(); + }); + + //When + var annotations = compute(() -> StepDefinitionAnnotationConverter.convertFrom(psiAnnotations)); + + //Then + var attribute = new StepDefinitionAnnotation(null, "one aliases $string", psiAnnotations[0]); + assertThat(annotations).containsExactly(attribute); + } + + @Test + void convertsSingleAliasesAnnotationWithMultipleAttributesToMultiSet() { + //Given + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + var psiAnnotations = compute(() -> { + var stepDefsClass = ((PsiClassOwner) stepDefFile).getClasses()[0]; + var aliasesStepDef = stepDefsClass.findMethodsByName("aliasesStepDefWithMultipleAnnotationAttributes", false)[0]; + return aliasesStepDef.getAnnotations(); + }); + + //When + var annotations = compute(() -> StepDefinitionAnnotationConverter.convertFrom(psiAnnotations)); + + //Then + var firstAttribute = new StepDefinitionAnnotation(null, "multiple aliases $string", psiAnnotations[0]); + var secondAttribute = new StepDefinitionAnnotation(null, "aliases something else", psiAnnotations[0]); + assertThat(annotations).containsExactlyInAnyOrder(firstAttribute, secondAttribute); + } + +// @Test +// void convertsMixedStepTypedAndAliasesAnnotationsWhereTheStepTypeAnnotationComesFirst() { +// //Given +// var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); +// var psiAnnotations = compute(() -> { +// var stepDefsClass = ((PsiClassOwner) stepDefFile).getClasses()[0]; +// var whenAliasesStepDef = stepDefsClass.findMethodsByName("whenAndAliasesStepDef", false)[0]; +// return whenAliasesStepDef.getAnnotations(); +// }); +// +// //When +// var annotations = compute(() -> StepDefinitionAnnotationConverter.convertFrom(psiAnnotations)); +// +// //Then +// var whenAttribute = new StepDefinitionAnnotation(StepType.WHEN, "mixed when aliases $string", psiAnnotations[0]); +// var aliasOneAttribute = new StepDefinitionAnnotation(null, "mixed when aliases one $string", psiAnnotations[1]); +// var aliasTwoAttribute = new StepDefinitionAnnotation(null, "mixed when aliases two", psiAnnotations[1]); +// +// assertThat(annotations).containsExactlyInAnyOrder(whenAttribute, aliasOneAttribute, aliasTwoAttribute); +// } + + @Test + void convertsMixedAliasesAndStepTypedAnnotationsWhereTheAliasesAnnotationComesFirst() { + //Given + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + var psiAnnotations = compute(() -> { + var stepDefsClass = ((PsiClassOwner) stepDefFile).getClasses()[0]; + var aliasesWhenStepDef = stepDefsClass.findMethodsByName("aliasesAndWhenStepDef", false)[0]; + return aliasesWhenStepDef.getAnnotations(); + }); + + //When + var annotations = compute(() -> StepDefinitionAnnotationConverter.convertFrom(psiAnnotations)); + + //Then + var aliasOneAttribute = new StepDefinitionAnnotation(null, "mixed aliases when one $string", psiAnnotations[0]); + var aliasTwoAttribute = new StepDefinitionAnnotation(null, "mixed aliases when two", psiAnnotations[0]); + var whenAttribute = new StepDefinitionAnnotation(StepType.WHEN, "mixed aliases when $string", psiAnnotations[1]); + + assertThat(annotations).containsExactlyInAnyOrder(whenAttribute, aliasOneAttribute, aliasTwoAttribute); + } + +// @Test +// void convertsMultipleVariousAnnotations() { +// //Given +// var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); +// var psiAnnotations = new ArrayList(); +// run(() -> { +// var stepDefsClass = ((PsiClassOwner) stepDefFile).getClasses()[0]; +// var givenStepDef = stepDefsClass.findMethodsByName("givenStepDefWithOneAnnotationAttribute", false)[0]; +// var whenStepDef = stepDefsClass.findMethodsByName("whenStepDefWithOneAnnotationAttribute", false)[0]; +// var thenStepDef = stepDefsClass.findMethodsByName("thenStepDefWithOneAnnotationAttribute", false)[0]; +// var aliasStepDef = stepDefsClass.findMethodsByName("aliasStepDefWithOneAnnotationAttribute", false)[0]; +// var aliasesOneStepDef = stepDefsClass.findMethodsByName("aliasesStepDefWithOneAnnotationAttribute", false)[0]; +// var aliasesMultiStepDef = stepDefsClass.findMethodsByName("aliasesStepDefWithMultipleAnnotationAttributes", false)[0]; +// +// psiAnnotations.addAll(Arrays.asList(givenStepDef.getAnnotations())); +// psiAnnotations.addAll(Arrays.asList(whenStepDef.getAnnotations())); +// psiAnnotations.addAll(Arrays.asList(thenStepDef.getAnnotations())); +// psiAnnotations.addAll(Arrays.asList(aliasStepDef.getAnnotations())); +// psiAnnotations.addAll(Arrays.asList(aliasesOneStepDef.getAnnotations())); +// psiAnnotations.addAll(Arrays.asList(aliasesMultiStepDef.getAnnotations())); +// }); +// +// //When +// var annotations = compute(() -> StepDefinitionAnnotationConverter.convertFrom(psiAnnotations.toArray(PsiAnnotation.EMPTY_ARRAY))); +// +// //Then +// var givenAttribute = new StepDefinitionAnnotation(StepType.GIVEN, "one given $string", psiAnnotations.get(0)); +// var whenAttribute = new StepDefinitionAnnotation(StepType.WHEN, "one when $string", psiAnnotations.get(1)); +// var thenAttribute = new StepDefinitionAnnotation(StepType.THEN, "one then $string", psiAnnotations.get(2)); +// var aliasAttribute = new StepDefinitionAnnotation(null, "one alias $string", psiAnnotations.get(3)); +// var aliasesOneAttribute = new StepDefinitionAnnotation(null, "one aliases $string", psiAnnotations.get(4)); +// var aliasesMultiFirstAttribute = new StepDefinitionAnnotation(null, "multiple aliases $string", psiAnnotations.get(5)); +// var aliasesMultiSecondAttribute = new StepDefinitionAnnotation(null, "aliases something else", psiAnnotations.get(5)); +// +// assertThat(annotations).containsExactlyInAnyOrder( +// givenAttribute, +// whenAttribute, +// thenAttribute, +// aliasAttribute, +// aliasesOneAttribute, +// aliasesMultiFirstAttribute, +// aliasesMultiSecondAttribute +// ); +// } + + @Test + void doesntConvertAnnotationsHavingNoAttributes() { + //Given + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + var psiAnnotations = new ArrayList(); + run(() -> { + var stepDefsClass = ((PsiClassOwner) stepDefFile).getClasses()[0]; + var givenStepDef = stepDefsClass.findMethodsByName("givenStepDefWithoutAnnotationAttribute", false)[0]; + var whenStepDef = stepDefsClass.findMethodsByName("whenStepDefWithoutAnnotationAttribute", false)[0]; + var thenStepDef = stepDefsClass.findMethodsByName("thenStepDefWithoutAnnotationAttribute", false)[0]; + var aliasStepDef = stepDefsClass.findMethodsByName("aliasStepDefWithoutAnnotationAttribute", false)[0]; + var aliasesStepDef = stepDefsClass.findMethodsByName("aliasesStepDefWithoutAnnotationAttribute", false)[0]; + + psiAnnotations.addAll(Arrays.asList(givenStepDef.getAnnotations())); + psiAnnotations.addAll(Arrays.asList(whenStepDef.getAnnotations())); + psiAnnotations.addAll(Arrays.asList(thenStepDef.getAnnotations())); + psiAnnotations.addAll(Arrays.asList(aliasStepDef.getAnnotations())); + psiAnnotations.addAll(Arrays.asList(aliasesStepDef.getAnnotations())); + }); + + //When + var annotations = compute(() -> StepDefinitionAnnotationConverter.convertFrom(psiAnnotations.toArray(PsiAnnotation.EMPTY_ARRAY))); + + //Then + assertThat(annotations).isEmpty(); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionIteratorTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionIteratorTest.java new file mode 100644 index 00000000..cafa153a --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionIteratorTest.java @@ -0,0 +1,93 @@ +package com.github.kumaraman21.intellijbehave.resolver; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.kumaraman21.intellijbehave.ContentEntryTestBase; +import com.intellij.openapi.project.Project; +import org.jbehave.core.steps.StepType; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import java.util.function.Predicate; + +/** + * Integration test for {@link StepDefinitionIterator}. + */ +class StepDefinitionIteratorTest extends ContentEntryTestBase { + + @Nullable + @Override + protected String getTestDataPath() { + return "src/test/testData/resolver/stepdefiterator"; + } + + @Test + void processesEverything() { + copySrcDirectoryToProject(); + getFixture().copyFileToProject("main/java/OtherStepDefs.java"); + getFixture().copyFileToProject("main/kotlin/AnotherStepDefs.kt"); + getFixture().copyFileToProject("src/test/resources/iterator.story"); + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + + var iterator = new DummyIterator(StepType.GIVEN, getFixture().getProject(), __ -> true); + boolean isProcessed = iterator.processFile(stepDefFile.getVirtualFile()); + + assertThat(isProcessed).isTrue(); + } + + @Test + void processesNothing() { + copySrcDirectoryToProject(); + getFixture().copyFileToProject("main/java/OtherStepDefs.java"); + getFixture().copyFileToProject("main/kotlin/AnotherStepDefs.kt"); + getFixture().copyFileToProject("src/test/resources/iterator.story"); + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + + var iterator = new DummyIterator(StepType.WHEN, getFixture().getProject(), __ -> false); + boolean isProcessed = iterator.processFile(stepDefFile.getVirtualFile()); + + assertThat(isProcessed).isFalse(); + } + + @Test + void processesFiltered() { + copySrcDirectoryToProject(); + getFixture().copyFileToProject("main/java/OtherStepDefs.java"); + getFixture().copyFileToProject("main/kotlin/AnotherStepDefs.kt"); + getFixture().copyFileToProject("src/test/resources/iterator.story"); + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + + var iterator = new DummyIterator(StepType.THEN, getFixture().getProject(), ann -> ann.annotationText().contains("size")); + boolean isProcessed = iterator.processFile(stepDefFile.getVirtualFile()); + + assertThat(isProcessed).isTrue(); + } + + @Test + void processesFilteredKotlin() { + copySrcDirectoryToProject(); + getFixture().copyFileToProject("main/java/OtherStepDefs.java"); + getFixture().copyFileToProject("main/java/StepDefs.java"); + getFixture().copyFileToProject("src/test/resources/iterator.story"); + var stepDefFile = getFixture().configureByFile("main/kotlin/AnotherStepDefs.kt"); + + var iterator = new DummyIterator(StepType.THEN, getFixture().getProject(), ann -> ann.annotationText().contains("size")); + boolean isProcessed = iterator.processFile(stepDefFile.getVirtualFile()); + + assertThat(isProcessed).isFalse(); //False because there is a step that doesn't match the condition + } + + private static final class DummyIterator extends StepDefinitionIterator { + private final Predicate annotationPredicate; + + public DummyIterator(@Nullable StepType stepType, Project project, Predicate annotationPredicate) { + super(stepType, project); + this.annotationPredicate = annotationPredicate; + } + + @Override + public boolean processStepDefinition(StepDefinitionAnnotation stepDefinitionAnnotation) { + return annotationPredicate.test(stepDefinitionAnnotation); + } + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReferenceContributorTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReferenceContributorTest.java new file mode 100644 index 00000000..46b20b07 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReferenceContributorTest.java @@ -0,0 +1,49 @@ +package com.github.kumaraman21.intellijbehave.resolver; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.kumaraman21.intellijbehave.ContentEntryTestBase; +import com.intellij.openapi.application.ReadAction; +import com.intellij.psi.PsiReference; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; + +/** + * Integration test for {@link StepPsiReferenceContributor}. + */ +class StepPsiReferenceContributorTest extends ContentEntryTestBase { + + @Nullable + @Override + protected String getTestDataPath() { + return "src/test/testData/reference/storystep"; + } + + @Test + void shouldProviderReferenceForStep() { + copySrcDirectoryToProject(); + var storyFile = getFixture().configureByFile("test/resources/step_reference.story"); + getFixture().copyFileToProject("main/java/StepDefs.java"); + var step = getParentOfElementAtCaretIn(storyFile); + + PsiReference[] references = step.getReferences(); + assertThat(references).hasSize(1); + assertThat(references[0]) + .isInstanceOf(StepPsiReference.class) + .extracting(ref -> ReadAction.compute(() -> ref.resolve().getText())) + .isEqualTo(""" + @When("search for $string") + public void searchForText(@Named("string") String string) { + }"""); + } + + @Test + void shouldNotProvideReferenceForNonStepElement() { + copySrcDirectoryToProject(); + var storyFile = getFixture().configureByFile("test/resources/non_step_reference.story"); + getFixture().copyFileToProject("main/java/StepDefs.java"); + var step = getParentOfElementAtCaretIn(storyFile); + + assertThat(step.getReferences()).isEmpty(); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReferenceTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReferenceTest.java new file mode 100644 index 00000000..c669c0ba --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReferenceTest.java @@ -0,0 +1,56 @@ +package com.github.kumaraman21.intellijbehave.resolver; + +import static com.intellij.openapi.application.ReadAction.compute; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.kumaraman21.intellijbehave.ContentEntryTestBase; +import com.intellij.psi.PsiJavaFile; +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiReference; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; + +/** + * Integration test for {@link StepPsiReference}. + */ +class StepPsiReferenceTest extends ContentEntryTestBase { + + @Nullable + @Override + protected String getTestDataPath() { + return "src/test/testData/reference/steppsi"; + } + + //isReferenceTo + + @Test + void shouldBeReferenceToPsiMethod() { + copySrcDirectoryToProject(); + var storyFile = getFixture().configureByFile("test/resources/step_reference.story"); + getFixture().copyFileToProject("main/java/StepDefs.java"); + var step = getParentOfElementAtCaretIn(storyFile); + + PsiReference[] references = step.getReferences(); + assertThat(references).hasSize(1); + + var resolvedMethod = (PsiMethod) compute(() -> references[0].resolve()); + + assertThat(references[0].isReferenceTo(resolvedMethod)).isTrue(); + } + + @Test + void shouldNotBeReferenceToElement() { + copySrcDirectoryToProject(); + var storyFile = getFixture().configureByFile("test/resources/step_reference.story"); + var stepDefVirtualFile = getFixture().copyFileToProject("main/java/StepDefs.java"); + var step = getParentOfElementAtCaretIn(storyFile); + var stepDefFile = compute(() -> PsiManager.getInstance(getFixture().getProject()).findFile(stepDefVirtualFile)); + var notReferencedStepDefMethod = compute(() -> ((PsiJavaFile) stepDefFile).getClasses()[0].findMethodsByName("openAUrl", false)[0]); + + PsiReference[] references = step.getReferences(); + assertThat(references).hasSize(1); + + assertThat(references[0].isReferenceTo(notReferencedStepDefMethod)).isFalse(); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaMethodUsageSearcherTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaMethodUsageSearcherTest.java new file mode 100644 index 00000000..51eca02a --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaMethodUsageSearcherTest.java @@ -0,0 +1,95 @@ +package com.github.kumaraman21.intellijbehave.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.kumaraman21.intellijbehave.ContentEntryTestBase; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.search.SearchScope; +import com.intellij.psi.search.searches.MethodReferencesSearch; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; + +/** + * Integration test for {@link JBehaveJavaMethodUsageSearcher}. + */ +class JBehaveJavaMethodUsageSearcherTest extends ContentEntryTestBase { + + @Nullable + @Override + protected String getTestDataPath() { + return "src/test/testData/service/javamethodusagesearch"; + } + + @Test + void shouldReturnTrueIfThereIsNoStepText() { + copySrcDirectoryToProject(); + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + var method = (PsiMethod) getParentOfElementAtCaretIn(stepDefFile); + var queryParameters = new MethodReferencesSearch.SearchParameters(method, GlobalSearchScope.projectScope(getFixture().getProject()), false); + + var ref = new Ref(); + new JBehaveJavaMethodUsageSearcher().processQuery(queryParameters, reference -> { + ref.set(true); + return true; + }); + + assertThat(ref.get()).isNull(); + } + + @Test + void shouldNotProcessQueryIfStepTextIsEmpty() { + copySrcDirectoryToProject(); + var stepDefFile = getFixture().configureByFile("main/java/MoreStepDefs.java"); + var method = (PsiMethod) getParentOfElementAtCaretIn(stepDefFile); + var queryParameters = new MethodReferencesSearch.SearchParameters(method, GlobalSearchScope.projectScope(getFixture().getProject()), false); + + var ref = new Ref(); + new JBehaveJavaMethodUsageSearcher().processQuery(queryParameters, reference -> { + ref.set(true); + return true; + }); + + assertThat(ref.get()).isNull(); + } + + @Test + void shouldNotProcessQueryIfSearchScopeIsNotGlobal() { + copySrcDirectoryToProject(); + var stepDefFile = getFixture().configureByFile("main/java/OtherStepDefs.java"); + var method = (PsiMethod) getParentOfElementAtCaretIn(stepDefFile); + var queryParameters = new MethodReferencesSearch.SearchParameters(method, new DummyScope(), false); + + var ref = new Ref(); + new JBehaveJavaMethodUsageSearcher().processQuery(queryParameters, reference -> { + ref.set(true); + return true; + }); + + assertThat(ref.get()).isNull(); + } + +// @Test +// void shouldProcessQuery() { +// } + + private static final class DummyScope extends SearchScope { + @Override + public @NotNull SearchScope intersectWith(@NotNull SearchScope scope2) { + return scope2; + } + + @Override + public @NotNull SearchScope union(@NotNull SearchScope scope) { + return scope; + } + + @Override + public boolean contains(@NotNull VirtualFile file) { + return false; + } + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaStepDefinitionSearchTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaStepDefinitionSearchTest.java new file mode 100644 index 00000000..6c71b497 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaStepDefinitionSearchTest.java @@ -0,0 +1,76 @@ +package com.github.kumaraman21.intellijbehave.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.kumaraman21.intellijbehave.ContentEntryTestBase; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.search.searches.ReferencesSearch; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; + +/** + * Integration test for {@link JBehaveJavaStepDefinitionSearch}. + */ +class JBehaveJavaStepDefinitionSearchTest extends ContentEntryTestBase { + + @Nullable + @Override + protected String getTestDataPath() { + return "src/test/testData/service/stepdefsearch"; + } + + @Test + void shouldReturnTrueForNonPsiMethodElement() { + copySrcDirectoryToProject(); + var stepDefFile = getFixture().configureByFile("main/java/OtherStepDefs.java"); + var element = stepDefFile.findElementAt(getCaretOffset()); + var queryParameters = new ReferencesSearch.SearchParameters(element, GlobalSearchScope.projectScope(getFixture().getProject()), false); + + boolean shouldContinue = new JBehaveJavaStepDefinitionSearch().execute(queryParameters, __ -> true); + + assertThat(shouldContinue).isTrue(); + } + + @Test + void shouldReturnTrueForNonStepDefinitionMethod() { + copySrcDirectoryToProject(); + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + var method = (PsiMethod) getParentOfElementAtCaretIn(stepDefFile); + var queryParameters = new ReferencesSearch.SearchParameters(method, GlobalSearchScope.projectScope(getFixture().getProject()), false); + + boolean shouldContinue = new JBehaveJavaStepDefinitionSearch().execute(queryParameters, __ -> true); + + assertThat(shouldContinue).isTrue(); + } + +// @Test +// void shouldReturnTrueIfThereIsNoStepText() { } + +// @Test +// void shouldReturnTrueIfThereIsANullStepTextForNonStepDefinitionMethod() { } + + @Test + void shouldReturnTrueForValidStepDefinitionMethod() { + copySrcDirectoryToProject(); + var stepDefFile = getFixture().configureByFile("main/java/MoreStepDefs.java"); + var method = (PsiMethod) getParentOfElementAtCaretIn(stepDefFile); + var queryParameters = new ReferencesSearch.SearchParameters(method, GlobalSearchScope.projectScope(getFixture().getProject()), false); + + boolean shouldContinue = new JBehaveJavaStepDefinitionSearch().execute(queryParameters, ref -> true); + + assertThat(shouldContinue).isTrue(); + } + + @Test + void shouldReturnFalseForValidStepDefinitionMethodAndFalseConsumer() { + copySrcDirectoryToProject(); + var stepDefFile = getFixture().configureByFile("main/java/MoreStepDefs.java"); + var method = (PsiMethod) getParentOfElementAtCaretIn(stepDefFile); + var queryParameters = new ReferencesSearch.SearchParameters(method, GlobalSearchScope.projectScope(getFixture().getProject()), false); + + boolean shouldContinue = new JBehaveJavaStepDefinitionSearch().execute(queryParameters, ref -> false); + + assertThat(shouldContinue).isFalse(); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndexTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndexTest.java new file mode 100644 index 00000000..fe53551d --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndexTest.java @@ -0,0 +1,105 @@ +package com.github.kumaraman21.intellijbehave.service; + +import static com.intellij.openapi.application.ReadAction.compute; +import static java.util.stream.Collectors.toSet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.github.kumaraman21.intellijbehave.ContentEntryTestBase; +import com.github.kumaraman21.intellijbehave.parser.JBehaveStep; +import com.intellij.codeInsight.AnnotationUtil; +import com.intellij.openapi.application.ReadAction; +import com.intellij.psi.PsiSubstitutor; +import com.intellij.psi.impl.java.stubs.index.JavaFullClassNameIndex; +import org.junit.jupiter.api.Test; + +/** + * Functional test for {@link JBehaveStepsIndex}. + */ +class JBehaveStepsIndexTest extends ContentEntryTestBase { + + @Override + protected String getTestDataPath() { + return "src/test/testData/stepsindex"; + } + + // findStepDefinitions + + @Test + void shouldFindsSingleStepDefinition() { + copySrcDirectoryToProject(); + getFixture().copyFileToProject("main/java/StepDefs.java"); + getFixture().copyFileToProject("main/java/OtherStepDefs.java"); + + getFixture().configureByFile("test/resources/has_java_step_def.story"); + + var stepDefinitions = JBehaveStepsIndex.getInstance(getFixture().getProject()).findStepDefinitions(getStep()); + + assertThat(stepDefinitions).hasSize(1); + assertThat(stepDefinitions.iterator().next().getAnnotatedMethod().getContainingClass().getQualifiedName()).isEqualTo("StepDefs"); + assertThat(compute(() -> stepDefinitions.iterator().next().getAnnotatedMethod().getSignature(PsiSubstitutor.EMPTY).toString())) + .isEqualTo("MethodSignatureBackedByPsiMethod: openAUrl([PsiType:String])"); + } + + //NOTE: at the moment, this only returns the first found step definition, regardless of the step pattern + // existing for multiple different step types, e.g. for a @Given and a @Then step def, and regardless of step priority. + //Supporting returning multiple matching step definitions should be revisited. + @Test + void shouldFindsMultipleStepDefinitions() { + copySrcDirectoryToProject(); + getFixture().copyFileToProject("main/java/StepDefs.java"); + getFixture().copyFileToProject("main/java/OtherStepDefs.java"); + getFixture().configureByFile("test/resources/has_multiple_java_step_def.story"); + + var stepDefinitions = JBehaveStepsIndex.getInstance(getFixture().getProject()).findStepDefinitions(getStep()); + + assertThat(stepDefinitions).hasSize(1); + assertThat(stepDefinitions.iterator().next().getAnnotatedMethod().getContainingClass().getQualifiedName()).isEqualTo("OtherStepDefs"); + assertThat(stepDefinitions.iterator().next().getAnnotatedMethod().getSignature(PsiSubstitutor.EMPTY)) + .hasToString("MethodSignatureBackedByPsiMethod: checkResultListSize([PsiType:int])"); + } + + @Test + void shouldFindsNoStepDefinition() { + copySrcDirectoryToProject(); + getFixture().copyFileToProject("main/java/StepDefs.java"); + getFixture().copyFileToProject("main/java/OtherStepDefs.java"); + getFixture().configureByFile("test/resources/has_no_java_step_def.story"); + + var stepDefinitions = JBehaveStepsIndex.getInstance(getFixture().getProject()).findStepDefinitions(getStep()); + + assertThat(stepDefinitions).isEmpty(); + } + + //getAllStepAnnotations + + @Test + void shouldFindAllAnnotations() { + copySrcDirectoryToProject(); + getFixture().copyFileToProject("main/java/StepDefs.java"); + getFixture().copyFileToProject("main/java/OtherStepDefs.java"); + getFixture().copyFileToProject("main/kotlin/AnotherStepDefs.kt"); + + getFixture().configureByFile("test/resources/has_java_step_def.story"); + + var scope = getFixture().getModule().getModuleWithDependenciesAndLibrariesScope(true); + var thenAnnotations = ReadAction.compute(() -> JavaFullClassNameIndex.getInstance().getClasses("org.jbehave.core.annotations.Then", getFixture().getProject(), scope)); + if (thenAnnotations.isEmpty()) fail("The @Then step def annotation was not found."); + + var stepDefinitions = JBehaveStepsIndex.getInstance(getFixture().getProject()).getAllStepAnnotations(thenAnnotations.iterator().next(), scope); + + assertThat(stepDefinitions).hasSize(3); + var stepTexts = stepDefinitions.stream() + .map(annotation -> ReadAction.compute(() -> AnnotationUtil.getStringAttributeValue(annotation, "value"))) + .collect(toSet()); + + assertThat(stepTexts).containsExactlyInAnyOrder( + "result ends with $text", + "check result size is $size", + "result list size is $size"); + } + + private JBehaveStep getStep() { + return (JBehaveStep) getParentOfElementAtCaretIn(getFixture().getFile()); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndexWithNoDependencyTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndexWithNoDependencyTest.java new file mode 100644 index 00000000..e3c758fb --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndexWithNoDependencyTest.java @@ -0,0 +1,36 @@ +package com.github.kumaraman21.intellijbehave.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.kumaraman21.intellijbehave.ContentEntryProjectDescriptor; +import com.github.kumaraman21.intellijbehave.ContentEntryTestBase; +import com.github.kumaraman21.intellijbehave.parser.JBehaveStep; +import org.junit.jupiter.api.Test; + +/** + * Functional test for {@link JBehaveStepsIndex}. + */ +class JBehaveStepsIndexWithNoDependencyTest extends ContentEntryTestBase { + + public JBehaveStepsIndexWithNoDependencyTest() { + //Doesn't load jbehave-core to emulate the missing JBehave + super(new ContentEntryProjectDescriptor()); + } + + @Override + protected String getTestDataPath() { + return "src/test/testData/stepsindex"; + } + + @Test + void shouldFindNoStepDefinitionDueToNoJBehaveAnnotationsAvailable() { + getFixture().copyFileToProject("src/main/java/StepDefs.java"); + getFixture().copyFileToProject("src/main/java/OtherStepDefs.java"); + getFixture().configureByFile("src/test/resources/has_java_step_def.story"); + + JBehaveStep step = (JBehaveStep) getParentOfElementAtCaretIn(getFixture().getFile()); + var stepDefinitions = JBehaveStepsIndex.getInstance(getFixture().getProject()).findStepDefinitions(step); + + assertThat(stepDefinitions).isEmpty(); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtilContentsTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtilContentsTest.java new file mode 100644 index 00000000..a63ed6e2 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtilContentsTest.java @@ -0,0 +1,85 @@ +package com.github.kumaraman21.intellijbehave.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.kumaraman21.intellijbehave.ContentEntryTestBase; +import com.intellij.psi.search.GlobalSearchScope; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; + +/** + * Integration test for {@link JBehaveUtil}. + */ +class JBehaveUtilContentsTest extends ContentEntryTestBase { + + @Nullable + @Override + protected String getTestDataPath() { + return "src/test/testData/service/jbehaveutil"; + } + + //findJBehaveReferencesToElement + + @Test + void shouldContinueReferenceSearchForEmptyBiggestWord() { + copySrcDirectoryToProject(); + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + + var stepDefMethod = getParentOfElementAtCaretIn(stepDefFile); + + boolean findRef = JBehaveUtil.findJBehaveReferencesToElement( + stepDefMethod, + "", + __ -> true, + GlobalSearchScope.projectScope(getFixture().getProject())); + assertThat(findRef).isTrue(); + } + + @Test + void shouldContinueReferenceSearchForValidMethod() { + copySrcDirectoryToProject(); + getFixture().copyFileToProject("src/test/resources/reference.story"); + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + + var stepDefMethod = getParentOfElementAtCaretIn(stepDefFile); + + boolean findRef = JBehaveUtil.findJBehaveReferencesToElement( + stepDefMethod, + "search for $string", + __ -> true, + GlobalSearchScope.projectScope(getFixture().getProject())); + assertThat(findRef).isTrue(); + } + + @Test + void shouldNotContinueReferenceSearchForValidMethodWithFalseConsumer() { + copySrcDirectoryToProject(); + getFixture().copyFileToProject("src/test/resources/reference.story"); + var stepDefFile = getFixture().configureByFile("main/java/StepDefs.java"); + + var stepDefMethod = getParentOfElementAtCaretIn(stepDefFile); + + boolean findRef = JBehaveUtil.findJBehaveReferencesToElement( + stepDefMethod, + "search for $string", + __ -> false, + GlobalSearchScope.projectScope(getFixture().getProject())); + assertThat(findRef).isFalse(); + } + + @Test + void shouldContinueReferenceSearchForMethodWithNoReference() { + copySrcDirectoryToProject(); + getFixture().copyFileToProject("src/test/resources/reference.story"); + var stepDefFile = getFixture().configureByFile("main/java/OtherStepDefs.java"); + + var stepDefMethod = getParentOfElementAtCaretIn(stepDefFile); + + boolean findRef = JBehaveUtil.findJBehaveReferencesToElement( + stepDefMethod, + "result list size is $size", + __ -> true, + GlobalSearchScope.projectScope(getFixture().getProject())); + assertThat(findRef).isTrue(); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtilTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtilTest.java new file mode 100644 index 00000000..0012f235 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtilTest.java @@ -0,0 +1,507 @@ +package com.github.kumaraman21.intellijbehave.service; + +import static com.intellij.openapi.application.ReadAction.compute; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.kumaraman21.intellijbehave.JBehaveSupportTestBase; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.util.PsiTreeUtil; +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.When; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +/** + * Integration test for {@link JBehaveUtil}. + */ +class JBehaveUtilTest extends JBehaveSupportTestBase { + @Nullable + @Override + protected String getTestDataPath() { + return ""; //empty because it is not needed, but without overriding the method, the test setup fails + } + + //isJBehaveStepAnnotation + + @Test + void shouldBeJBehaveStepAnnotation() { + var stepDefFile = getFixture().configureByText("JBehaveStepAnnotation.java", """ + import org.jbehave.core.annotations.Given; + + class JBehaveStepAnnotation { + @Given("") + void stepDefMethod() { + } + } + """); + + var annotation = compute(() -> stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + + assertThat(annotation).isInstanceOf(PsiAnnotation.class); + assertThat(JBehaveUtil.isJBehaveStepAnnotation((PsiAnnotation) annotation)).isTrue(); + } + + @Test + void shouldNotBeJBehaveStepAnnotationForNonStepAnnotation() { + var stepDefFile = getFixture().configureByText("JBehaveStepAnnotation.java", """ + import org.jbehave.core.annotations.Alias; + + class JBehaveStepAnnotation { + @Alias("") + void stepDefMethod() { + } + } + """); + + var annotation = compute(() -> stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + + assertThat(annotation).isInstanceOf(PsiAnnotation.class); + assertThat(JBehaveUtil.isJBehaveStepAnnotation((PsiAnnotation) annotation)).isFalse(); + } + + @Test + void shouldNotBeJBehaveStepAnnotationForUnresolvedAnnotation() { + var stepDefFile = getFixture().configureByText("JBehaveStepAnnotation.java", """ + class JBehaveStepAnnotation { + @("") + void stepDefMethod() { + } + } + """); + + var annotation = compute(() -> stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + + assertThat(annotation).isInstanceOf(PsiAnnotation.class); + assertThat(JBehaveUtil.isJBehaveStepAnnotation((PsiAnnotation) annotation)).isFalse(); + } + + //isAnnotationOfClass + + @Test + void shouldBeAnnotationOfClass() { + var stepDefFile = getFixture().configureByText("JBehaveAnnotationOfClass.java", """ + import org.jbehave.core.annotations.Given; + + class JBehaveAnnotationOfClass { + @Given("") + void stepDefMethod() { + } + } + """); + + var annotation = compute(() -> stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(annotation).isInstanceOf(PsiAnnotation.class); + assertThat(JBehaveUtil.isAnnotationOfClass((PsiAnnotation) annotation, Given.class)).isTrue(); + } + + @Test + void shouldNotBeAnnotationOfClass() { + var stepDefFile = getFixture().configureByText("JBehaveAnnotationOfClass.java", """ + import org.jbehave.core.annotations.Given; + + class JBehaveAnnotationOfClass { + @Given("") + void stepDefMethod() { + } + } + """); + + var annotation = compute(() -> stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(annotation).isInstanceOf(PsiAnnotation.class); + assertThat(JBehaveUtil.isAnnotationOfClass((PsiAnnotation) annotation, When.class)).isFalse(); + } + + @Test + void shouldNotBeAnnotationOfClassForUnresolvedAnnotation() { + var stepDefFile = getFixture().configureByText("JBehaveAnnotationOfClass.java", """ + class JBehaveAnnotationOfClass { + @("") + void stepDefMethod() { + } + } + """); + + var annotation = compute(() -> stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(annotation).isInstanceOf(PsiAnnotation.class); + assertThat(JBehaveUtil.isAnnotationOfClass((PsiAnnotation) annotation, When.class)).isFalse(); + } + + //isStepDefinition + + @Test + void shouldBeStepDefinitionMethodWithSingleStepAnnotation() { + var stepDefFile = getFixture().configureByText("StepDefinitionMethod.java", """ + import org.jbehave.core.annotations.Given; + + class StepDefinitionMethod { + @Given("") + void stepDefMethod() { + } + } + """); + + var method = getParentOfElementAtCaretIn(stepDefFile); + assertThat(method).isInstanceOf(PsiMethod.class); + assertThat(JBehaveUtil.isStepDefinition((PsiMethod) method)).isTrue(); + } + + @Test + void shouldBeStepDefinitionMethodWithMultipleStepAnnotations() { + var stepDefFile = getFixture().configureByText("StepDefinitionMethod.java", """ + import org.jbehave.core.annotations.Given; + import org.jbehave.core.annotations.When; + + class StepDefinitionMethod { + @Given("") + @When("") + void stepDefMethod() { + } + } + """); + + var method = getParentOfElementAtCaretIn(stepDefFile); + assertThat(method).isInstanceOf(PsiMethod.class); + assertThat(JBehaveUtil.isStepDefinition((PsiMethod) method)).isTrue(); + } + + @Test + void shouldNotBeStepDefinitionMethodWithoutStepAnnotation() { + var stepDefFile = getFixture().configureByText("StepDefinitionMethod.java", """ + class StepDefinitionMethod { + void stepDefMethod() { + } + } + """); + + var method = getParentOfElementAtCaretIn(stepDefFile); + assertThat(method).isInstanceOf(PsiMethod.class); + assertThat(JBehaveUtil.isStepDefinition((PsiMethod) method)).isFalse(); + } + + @Test + void shouldNotBeStepDefinitionMethodWithAllStepAnnotationsWithNullValue() { + var stepDefFile = getFixture().configureByText("StepDefinitionMethod.java", """ + import org.jbehave.core.annotations.Given; + + class StepDefinitionMethod { + @Given + void stepDefMethod() { + } + } + """); + + var method = getParentOfElementAtCaretIn(stepDefFile); + assertThat(method).isInstanceOf(PsiMethod.class); + assertThat(JBehaveUtil.isStepDefinition((PsiMethod) method)).isFalse(); + } + + //getAnnotationTexts(PsiAnnotation) + + @Test + void shouldGetNoTextForInvalidAnnotation() { + var stepDefFile = getFixture().configureByText("AnnotationTexts.java", """ + import org.jbehave.core.annotations.Then; + + class AnnotationTexts { + @Then() + void stepDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(annotation).isInstanceOf(PsiAnnotation.class); + assertThat(JBehaveUtil.getAnnotationTexts((PsiAnnotation) annotation, null)).isEmpty(); + } + + @Test + void shouldGetTextsWithoutAliases() { + var stepDefFile = getFixture().configureByText("AnnotationTexts.java", """ + import org.jbehave.core.annotations.Then; + + class AnnotationTexts { + @Then("the {price|cost} of the product should be $price") + void stepDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(annotation).isInstanceOf(PsiAnnotation.class); + assertThat(JBehaveUtil.getAnnotationTexts((PsiAnnotation) annotation, null)).containsExactlyInAnyOrder( + "the price of the product should be $price", + "the cost of the product should be $price"); + } + + @Test + void shouldGetTextsWithAlias() { + var stepDefFile = getFixture().configureByText("AnnotationTexts.java", """ + import org.jbehave.core.annotations.Then; + import org.jbehave.core.annotations.Alias; + + class AnnotationTexts { + @Then("the {price|cost} of the product should be $price") + @Alias("the product should {cost|be sold for} $price") + void stepDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(annotation).isInstanceOf(PsiAnnotation.class); + assertThat(JBehaveUtil.getAnnotationTexts((PsiAnnotation) annotation, PsiTreeUtil.getParentOfType(annotation, PsiMethod.class))) + .containsExactlyInAnyOrder( + "the price of the product should be $price", + "the product should cost $price", + "the cost of the product should be $price", + "the product should be sold for $price" + ); + } + + @Test + void shouldGetTextsWithAliases() { + var stepDefFile = getFixture().configureByText("AnnotationTexts.java", """ + import org.jbehave.core.annotations.Then; + import org.jbehave.core.annotations.Aliases; + + class AnnotationTexts { + @Then("the {price|cost} of the product should be $price") + @Aliases(values = {"the product should cost $price", + "the product should be sold for $price"}) + void stepDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(annotation).isInstanceOf(PsiAnnotation.class); + assertThat(JBehaveUtil.getAnnotationTexts((PsiAnnotation) annotation, PsiTreeUtil.getParentOfType(annotation, PsiMethod.class))) + .containsExactlyInAnyOrder( + "the price of the product should be $price", + "the product should cost $price", + "the cost of the product should be $price", + "the product should be sold for $price" + ); + } + + @Test + void shouldGetTextsWithAliasAndAliases() { + var stepDefFile = getFixture().configureByText("AnnotationTexts.java", """ + import org.jbehave.core.annotations.Then; + import org.jbehave.core.annotations.Alias; + import org.jbehave.core.annotations.Aliases; + + class AnnotationTexts { + @Then("the {price|cost} of the product should be $price") + @Alias("the product should {cost|be sold for} $price") + @Aliases(values = {"the product should worth $price", + "the product should be sold for $price"}) + void stepDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(annotation).isInstanceOf(PsiAnnotation.class); + assertThat(JBehaveUtil.getAnnotationTexts((PsiAnnotation) annotation, null)).containsExactlyInAnyOrder( + "the product should worth $price", + "the price of the product should be $price", + "the product should cost $price", + "the cost of the product should be $price", + "the product should be sold for $price" + ); + } + + //getAnnotationTexts(PsiMethod) + + @Test + void shouldGetNoTextForMethodWithInvalidStepAnnotation() { + var stepDefFile = getFixture().configureByText("AnnotationTexts.java", """ + import org.jbehave.core.annotations.Then; + + class AnnotationTexts { + @Then() + void stepDefMethod(int price) { + } + } + """); + + var method = getParentOfElementAtCaretIn(stepDefFile); + assertThat(method).isInstanceOf(PsiMethod.class); + assertThat(JBehaveUtil.getAnnotationTexts((PsiMethod) method)).isEmpty(); + } + + @Test + void shouldGetTextsForMethodWithoutAliases() { + var stepDefFile = getFixture().configureByText("AnnotationTexts.java", """ + import org.jbehave.core.annotations.Then; + + class AnnotationTexts { + @Then("the {price|cost} of the product should be $price") + void stepDefMethod(int price) { + } + } + """); + + var method = getParentOfElementAtCaretIn(stepDefFile); + assertThat(method).isInstanceOf(PsiMethod.class); + assertThat(JBehaveUtil.getAnnotationTexts((PsiMethod) method)).containsExactlyInAnyOrder( + "the price of the product should be $price", + "the cost of the product should be $price"); + } + + @Test + void shouldGetTextsForMethodWithAlias() { + var stepDefFile = getFixture().configureByText("AnnotationTexts.java", """ + import org.jbehave.core.annotations.Then; + import org.jbehave.core.annotations.Alias; + + class AnnotationTexts { + @Then("the {price|cost} of the product should be $price") + @Alias("the product should {cost|be sold for} $price") + void stepDefMethod(int price) { + } + } + """); + + var method = getParentOfElementAtCaretIn(stepDefFile); + assertThat(method).isInstanceOf(PsiMethod.class); + assertThat(JBehaveUtil.getAnnotationTexts((PsiMethod) method)).containsExactlyInAnyOrder( + "the price of the product should be $price", + "the product should cost $price", + "the cost of the product should be $price", + "the product should be sold for $price" + ); + } + + @Test + void shouldGetTextsForMethodWithAliases() { + var stepDefFile = getFixture().configureByText("AnnotationTexts.java", """ + import org.jbehave.core.annotations.Then; + import org.jbehave.core.annotations.Aliases; + + class AnnotationTexts { + @Then("the {price|cost} of the product should be $price") + @Aliases(values = {"the product should cost $price", + "the product should be sold for $price"}) + void stepDefMethod(int price) { + } + } + """); + + var method = getParentOfElementAtCaretIn(stepDefFile); + assertThat(method).isInstanceOf(PsiMethod.class); + assertThat(JBehaveUtil.getAnnotationTexts((PsiMethod) method)).containsExactlyInAnyOrder( + "the price of the product should be $price", + "the product should cost $price", + "the cost of the product should be $price", + "the product should be sold for $price" + ); + } + + @Test + void shouldGetTextsForMethodWithAliasAndAliases() { + + var stepDefFile = getFixture().configureByText("AnnotationTexts.java", """ + import org.jbehave.core.annotations.Then; + import org.jbehave.core.annotations.Alias; + import org.jbehave.core.annotations.Aliases; + + class AnnotationTexts { + @Then("the {price|cost} of the product should be $price") + @Alias("the product should {cost|be sold for} $price") + @Aliases(values = {"the product should worth $price", + "the product should be sold for $price"}) + void stepDefMethod(int price) { + } + } + """); + + var method = getParentOfElementAtCaretIn(stepDefFile); + assertThat(method).isInstanceOf(PsiMethod.class); + assertThat(JBehaveUtil.getAnnotationTexts((PsiMethod) method)).containsExactlyInAnyOrder( + "the product should worth $price", + "the price of the product should be $price", + "the product should cost $price", + "the cost of the product should be $price", + "the product should be sold for $price" + ); + } + + //getAnnotationPriority + + @Test + void shouldGetDefaultPriority() { + var stepDefFile = getFixture().configureByText("AnnotationPriority.java", """ + import org.jbehave.core.annotations.Then; + + class AnnotationPriority { + @Then("the {price|cost} of the product should be $price") + void stepDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(annotation).isInstanceOf(PsiAnnotation.class); + assertThat(JBehaveUtil.getAnnotationPriority((PsiAnnotation) annotation)).isEqualTo(0); + } + + @Test + void shouldGetCustomPriority() { + var stepDefFile = getFixture().configureByText("AnnotationPriority.java", """ + import org.jbehave.core.annotations.Then; + + class AnnotationPriority { + @Then(value = "the {price|cost} of the product should be $price", priority = 30) + void stepDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(annotation).isInstanceOf(PsiAnnotation.class); + assertThat(JBehaveUtil.getAnnotationPriority((PsiAnnotation) annotation)).isEqualTo(30); + } + + @Test + void shouldGetFallbackPriorityWhenConfiguredWithInvalidValue() { + var stepDefFile = getFixture().configureByText("AnnotationPriority.java", """ + import org.jbehave.core.annotations.Then; + + class AnnotationPriority { + @Then(value = "the {price|cost} of the product should be $price", priority = "invalid") + void stepDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(annotation).isInstanceOf(PsiAnnotation.class); + assertThat(JBehaveUtil.getAnnotationPriority((PsiAnnotation) annotation)).isEqualTo(-1); + } + + //getTheBiggestWordToSearchByIndex + + @ParameterizedTest + @CsvSource({ + //Empty string for empty string + "'',''", + //Empty string for blank string + "' ',''", + //Without placeholder + "the {price|cost} of the product should be 100, product", + //With placeholder + "the {price|cost} of the product should be $price, product", + //First of multiple of the same-length words + "the {price|cost} of the product shouldd be $price, product", + //Empty string for placeholder-only, + "$price $cost, ''" + }) + void shouldReturnBiggestWord(String stepText, String biggestWord) { + assertThat(JBehaveUtil.getTheBiggestWordToSearchByIndex(stepText)).isEqualTo(biggestWord); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinitionTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinitionTest.java new file mode 100644 index 00000000..44914164 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinitionTest.java @@ -0,0 +1,303 @@ +package com.github.kumaraman21.intellijbehave.service; + +import static com.intellij.openapi.application.ReadAction.compute; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.kumaraman21.intellijbehave.JBehaveSupportTestBase; +import com.github.kumaraman21.intellijbehave.parser.JBehaveStep; +import com.intellij.psi.PsiAnnotation; +import org.jbehave.core.steps.StepType; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; + +/** + * Integration test for {@link JavaStepDefinition}. + */ +class JavaStepDefinitionTest extends JBehaveSupportTestBase { + + @Nullable + @Override + protected String getTestDataPath() { + return ""; + } + + //supportsStepAndMatches + + @Test + void shouldMatchStepText() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Then; + + class JavaStepDefinition { + @Then(value = "the price should be $price", priority = 500) + void steDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + + var storyFile = getFixture().configureByText("matches_step_text.story", """ + Scenario: Product price + + Then the price should be 200 + """); + + var step = (JBehaveStep) getParentOfElementAtCaretIn(storyFile); + + assertThat(new JavaStepDefinition(annotation).supportsStepAndMatches(step, "the price should be 200")).isTrue(); + } + + @Test + void shouldNotMatchStepText() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Then; + + class JavaStepDefinition { + @Then(value = "the price should be $price", priority = 500) + void steDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + + var storyFile = getFixture().configureByText("matches_step_text.story", """ + Scenario: Product price + + Then the price should be 200 + """); + + var step = (JBehaveStep) getParentOfElementAtCaretIn(storyFile); + + assertThat(new JavaStepDefinition(annotation).supportsStepAndMatches(step, "the price is 200")).isFalse(); + } + + @Test + void shouldSupportStep() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Then; + + class JavaStepDefinition { + @Then(value = "the price should be $price", priority = 500) + void steDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + + var storyFile = getFixture().configureByText("supports_step.story", """ + Scenario: Product price + + Then the price should be 200 + """); + + var step = (JBehaveStep) getParentOfElementAtCaretIn(storyFile); + + assertThat(new JavaStepDefinition(annotation).supportsStepAndMatches(step, "the price should be 200")).isTrue(); + } + + @Test + void shouldNotSupportStep() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Given; + + class JavaStepDefinition { + @Given(value = "the price should be $price", priority = 500) + void steDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + + var storyFile = getFixture().configureByText("supports_step.story", """ + Scenario: Product price + + Then the price should be 200 + """); + + var step = (JBehaveStep) getParentOfElementAtCaretIn(storyFile); + + assertThat(new JavaStepDefinition(annotation).supportsStepAndMatches(step, "the price should be 200")).isFalse(); + } + + @Test + void shouldNotSupportStepForAliasAnnotation() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Aliases; + + class JavaStepDefinition { + @Aliases(values = { + "the price should be $price", + "the cost should be $price" + }) + void steDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + + var storyFile = getFixture().configureByText("supports_step.story", """ + Scenario: Product price + + Then the price should be 200 + """); + + var step = (JBehaveStep) getParentOfElementAtCaretIn(storyFile); + + assertThat(new JavaStepDefinition(annotation).supportsStepAndMatches(step, "the price should be 200")).isFalse(); + } + + //getAnnotationTextFor + + @Test + void shouldGetAnnotationTextWhenThereIsOneSuchText() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Alias; + + class JavaStepDefinition { + @Alias("the price should be $price") + void steDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(new JavaStepDefinition(annotation).getAnnotationTextFor("the price should be 200")) + .isEqualTo("the price should be $price"); + } + + @Test + void shouldGetFirstMatchingAnnotationTextFromMultiple() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Aliases; + + class JavaStepDefinition { + @Aliases(values = { + "the price should be $price", + "the cost should be $price" + }) + void steDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(new JavaStepDefinition(annotation).getAnnotationTextFor("the cost should be 200")) + .isEqualTo("the cost should be $price"); + } + + @Test + void shouldReturnNoAnnotationTextWhenNotMatching() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Alias; + + class JavaStepDefinition { + @Aliases(values = { + "the price should be $price", + "the cost should be $price" + }) + void steDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(new JavaStepDefinition(annotation).getAnnotationTextFor("non matching text")).isNull(); + } + + //getAnnotatedMethod + + @Test + void shouldReturnTheAnnotatedMethod() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Then; + + class JavaStepDefinition { + @Then("the price should be $price") + void steDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(new JavaStepDefinition(annotation).getAnnotatedMethod()).extracting(method -> compute(method::getName)).isEqualTo("steDefMethod"); + } + + @Test + void shouldReturnNullWhenThereIsNoParentMethod() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Then; + + class JavaStepDefinition { + @Then("the price should be $price") + } + """); + + var annotation = compute(() -> (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(new JavaStepDefinition(annotation).getAnnotatedMethod()).isNull(); + } + + //getAnnotationType + + @Test + void shouldGetAnnotationType() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Then; + + class JavaStepDefinition { + @Then("the price should be $price") + void steDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(new JavaStepDefinition(annotation).getAnnotationType()).isEqualTo(StepType.THEN); + } + + @Test + void shouldReturnNullForNonMappedAnnotationType() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Alias; + + class JavaStepDefinition { + @Alias("the price should be $price") + void steDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(new JavaStepDefinition(annotation).getAnnotationType()).isNull(); + } + +// @Test +// void shouldReturnNullAnnotationTypeForNoAnnotation() { +// } + + //getAnnotationPriority + + @Test + void shouldGetAnnotationPriority() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Then; + + class JavaStepDefinition { + @Then(value = "the price should be $price", priority = 500) + void steDefMethod(int price) { + } + } + """); + + var annotation = compute(() -> (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent()); + assertThat(new JavaStepDefinition(annotation).getAnnotationPriority()).isEqualTo(500); + } + +// @Test +// void shouldReturnFallbackValueForNoAnnotation() { +// } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/service/OptimizedStepMatcherTest.kt b/src/test/java/com/github/kumaraman21/intellijbehave/service/OptimizedStepMatcherTest.kt new file mode 100644 index 00000000..993bbe62 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/service/OptimizedStepMatcherTest.kt @@ -0,0 +1,266 @@ +package com.github.kumaraman21.intellijbehave.service + +import org.assertj.core.api.Assertions.assertThat +import org.jbehave.core.parsers.RegexPrefixCapturingPatternParser +import org.jbehave.core.parsers.StepPatternParser +import org.jbehave.core.steps.StepType.GIVEN +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.DynamicTest.dynamicTest +import org.junit.jupiter.api.TestFactory +import java.util.stream.Stream + +/** + * Unit test for [OptimizedStepMatcher]. + */ +class OptimizedStepMatcherTest { + + private val stepPatternParser: StepPatternParser = RegexPrefixCapturingPatternParser() + + @TestFactory + fun validatesStepMatching(): Stream { + return Stream.of( + //This is an edge case, whether it returns false or true doesn't seem to make a difference + dynamicTest("empty texts", { + assertThat(matches("", "")).isTrue() + }), + + //No parameter - match + + dynamicTest("one word - no parameter") { + assertThat(matches("opened", "opened")).isTrue() + }, + dynamicTest("two words - no parameter") { + assertThat(matches("I opened", "I opened")).isTrue() + }, + dynamicTest("two-plus words - no parameter") { + assertThat(matches("I opened the HU homepage", "I opened the HU homepage")).isTrue() + }, + dynamicTest("multiple words - multiple spaces in step text - no parameter") { + assertThat(matches("I opened the HU homepage", "I opened the HU homepage")).isTrue() + }, + dynamicTest("multiple words - multiple spaces in annotation text - no parameter") { + assertThat(matches("I opened the HU homepage", "I opened the HU homepage")).isTrue() + }, + + //No parameter - mismatch + + dynamicTest("one word - no parameter - mismatch") { + assertThat(matches("opened", "closed")).isFalse() + }, + dynamicTest("two words - no parameter - mismatch") { + assertThat(matches("I opened", "I closed")).isFalse() + }, + dynamicTest("two-plus words - no parameter - mismatch") { + assertThat(matches("I opened the HU homepage", "I closed the HU homepage")).isFalse() + }, + dynamicTest("multiple words - multiple spaces in step text - no parameter - mismatch") { + assertThat(matches("I opened the HU homepage", "I closed the HU homepage")).isFalse() + }, + dynamicTest("multiple words - multiple spaces in annotation text - no parameter - mismatch") { + assertThat(matches("I opened the HU homepage", "I closed the HU homepage")).isFalse() + }, + dynamicTest("multiple words - spaces before annotation text - no parameter - mismatch trim") { + assertThat(matches(" I opened the HU homepage", "I opened the HU homepage")).isFalse() + }, + dynamicTest("multiple words - spaces after annotation text - no parameter - mismatch trim") { + assertThat(matches("I opened the HU homepage ", "I opened the HU homepage")).isFalse() + }, + dynamicTest("multiple words - spaces before annotation text - no parameter - mismatch") { + assertThat(matches(" I opened the HU homepage", "I closed the HU homepage")).isFalse() + }, + dynamicTest("multiple words - spaces after annotation text - no parameter - mismatch") { + assertThat(matches("I opened the HU homepage ", "I closed the HU homepage")).isFalse() + }, + + //Parameters - match + + dynamicTest("one parameter only - blank") { + assertThat(matches("\$thing", " ")).isTrue() + }, + dynamicTest("one parameter only") { + assertThat(matches("\$thing", "opened")).isTrue() + }, + dynamicTest("one parameter only - empty") { + assertThat(matches("\$thing", "")).isTrue() + }, + dynamicTest("two parameters only") { + assertThat(matches("\$some \$thing", "I opened")).isTrue() + }, + dynamicTest("two parameters only - empty") { + assertThat(matches("\$some \$thing", " ")).isTrue() + }, + dynamicTest("two parameters without whitespace - empty") { + assertThat(matches("\$some\$thing", "")).isTrue() + }, + + dynamicTest("one word - parameter") { + assertThat(matches("opened \$thing", "opened homepage")).isTrue() + }, + dynamicTest("two words - parameter") { + assertThat(matches("I \$indeed opened", "I really opened")).isTrue() + }, + dynamicTest("two-plus words - parameter") { + assertThat(matches("I opened the \$locale homepage", "I opened the HU homepage")).isTrue() + }, + dynamicTest("multiple words - multiple spaces in step text - parameter") { + assertThat(matches("I opened the \$locale homepage", "I opened the HU homepage")).isTrue() + }, + dynamicTest("multiple words - multiple spaces in annotation text - parameter") { + assertThat(matches("I opened the \$locale homepage", "I opened the HU homepage")).isTrue() + }, + dynamicTest("multiple words - spaces before annotation text - parameter") { + assertThat(matches(" I opened the \$locale homepage", " I opened the HU homepage")).isTrue() + }, + dynamicTest("multiple words - spaces after annotation text - parameter") { + assertThat(matches("I opened the \$locale homepage ", "I opened the HU homepage ")).isTrue() + }, + + dynamicTest("parameter - empty - beginning of annotation text") { + assertThat(matches("\$person opened the HU homepage", " opened the HU homepage")).isTrue() + }, + dynamicTest("parameter - empty and trimmed - beginning of annotation text") { + assertThat(matches("\$person opened the HU homepage", " opened the HU homepage")).isTrue() + }, + dynamicTest("parameter - non-empty - beginning of annotation text") { + assertThat(matches("\$person opened the HU homepage", "I opened the HU homepage")).isTrue() + }, + //FIXME: this should match because it matches in JBehave + dynamicTest("parameter - empty - middle of annotation text") { + assertThat(matches("I opened the \$locale homepage", "I opened the homepage")).isFalse() + }, + dynamicTest("parameter - non-empty - middle of annotation text") { + assertThat(matches("I opened the \$locale homepage", "I opened the HU homepage")).isTrue() + }, + dynamicTest("parameter - empty - end of annotation text") { + assertThat(matches("I opened the HU \$page", "I opened the HU ")).isTrue() + }, + dynamicTest("parameter - empty and trimmed - end of annotation text") { + assertThat(matches("I opened the HU \$page", "I opened the HU ")).isTrue() + }, + dynamicTest("parameter - non-empty - end of annotation text") { + assertThat(matches("I opened the HU \$page", "I opened the HU homepage")).isTrue() + }, + + dynamicTest("multiple parameters - empty - beginning of annotation text") { + assertThat(matches("\$person opened the \$locale homepage", " opened the HU homepage")).isTrue() + }, + dynamicTest("multiple parameters - empty and trimmed - beginning of annotation text") { + assertThat(matches("\$person opened the \$locale homepage", " opened the HU homepage")).isTrue() + }, + dynamicTest("multiple parameters - non-empty - beginning of annotation text") { + assertThat(matches("\$person opened the \$locale homepage", "I opened the HU homepage")).isTrue() + }, + //FIXME: this should match because it matches in JBehave + dynamicTest("multiple parameters - empty - middle of annotation text") { + assertThat(matches("\$person opened the \$locale homepage", "I opened the homepage")).isFalse() + }, + //FIXME: this should match because it matches in JBehave + dynamicTest("multiple parameters - empty and trimmed - middle of annotation text") { + assertThat(matches("\$person opened the \$locale homepage", " opened the homepage")).isFalse() + }, + dynamicTest("multiple parameters - non-empty - middle of annotation text") { + assertThat(matches("\$person opened the \$locale homepage", "I opened the HU homepage")).isTrue() + }, + dynamicTest("multiple parameters - empty - end of annotation text") { + assertThat(matches("\$person opened the HU \$page", "I opened the HU ")).isTrue() + }, + dynamicTest("multiple parameters - empty and trimmed - end of annotation text") { + assertThat(matches("\$person opened the HU \$page", "I opened the HU ")).isTrue() + }, + dynamicTest("multiple parameters - non-empty - end of annotation text") { + assertThat(matches("\$person opened the HU \$page", "I opened the HU homepage")).isTrue() + }, + + //Parameters - mismatch + + dynamicTest("two parameters - no whitespace at all- mismatch") { + assertThat(matches("\$some\$thing", "opened")).isFalse() + }, + dynamicTest("two parameters - no whitespace in annotation text - mismatch") { + assertThat(matches("\$some\$thing", "I opened the homepage")).isFalse() + }, + + dynamicTest("one word - parameter - mismatch") { + assertThat(matches("opened \$thing", "closed homepage")).isFalse() + }, + dynamicTest("two words - parameter - mismatch") { + assertThat(matches("I \$indeed opened", "I really closed")).isFalse() + }, + dynamicTest("two-plus words - parameter - mismatch") { + assertThat(matches("I opened the \$locale homepage", "I closed the HU homepage")).isFalse() + }, + dynamicTest("multiple words - multiple spaces in step text - parameter - mismatch") { + assertThat(matches("I opened the \$locale homepage", "I closed the HU homepage")).isFalse() + }, + dynamicTest("multiple words - multiple spaces in annotation text - parameter - mismatch") { + assertThat(matches("I opened the \$locale homepage", "I closed the HU homepage")).isFalse() + }, + dynamicTest("multiple words - spaces before annotation text - parameter - mismatch") { + assertThat(matches(" I opened the \$locale homepage", "I closed the HU homepage")).isFalse() + }, + dynamicTest("multiple words - spaces after annotation text - parameter - mismatch") { + assertThat(matches("I opened the \$locale homepage ", "I closed the HU homepage")).isFalse() + }, + + dynamicTest("parameter - empty - beginning of annotation text - mismatch") { + assertThat(matches("\$person opened the HU homepage", " closed the HU homepage")).isFalse() + }, + dynamicTest("parameter - empty and trimmed - beginning of annotation text - mismatch") { + assertThat(matches("\$person opened the HU homepage", "closed the HU homepage")).isFalse() + }, + dynamicTest("parameter - non-empty - beginning of annotation text - mismatch") { + assertThat(matches("\$person opened the HU homepage", "I closed the HU homepage")).isFalse() + }, + dynamicTest("parameter - empty - middle of annotation text - mismatch") { + assertThat(matches("I opened the \$locale homepage", "I closed the homepage")).isFalse() + }, + dynamicTest("parameter - empty and trimmed - middle of annotation text - mismatch") { + assertThat(matches("I opened the \$locale homepage", "I opened the homepage")).isFalse() + }, + dynamicTest("parameter - non-empty - middle of annotation text - mismatch") { + assertThat(matches("I opened the \$locale homepage", "I closed the HU homepage")).isFalse() + }, + dynamicTest("parameter - empty - end of annotation text - mismatch") { + assertThat(matches("I opened the HU \$page", "I closed the HU ")).isFalse() + }, + dynamicTest("parameter - empty and trimmed - end of annotation text - mismatch") { + assertThat(matches("I opened the HU \$page", "I closed the HU")).isFalse() + }, + dynamicTest("parameter - non-empty - end of annotation text - mismatch") { + assertThat(matches("I opened the HU \$page", "I closed the HU homepage")).isFalse() + }, + + dynamicTest("multiple parameters - empty - beginning of annotation text - mismatch") { + assertThat(matches("\$person opened the \$locale homepage", " closed the HU homepage")).isFalse() + }, + dynamicTest("multiple parameters - empty and trimmed - beginning of annotation text - mismatch") { + assertThat(matches("\$person opened the \$locale homepage", "opened the HU homepage")).isFalse() + }, + dynamicTest("multiple parameters - non-empty - beginning of annotation text - mismatch") { + assertThat(matches("\$person opened the \$locale homepage", "I closed the HU homepage")).isFalse() + }, + dynamicTest("multiple parameters - empty - middle of annotation text - mismatch") { + assertThat(matches("\$person opened the \$locale homepage", "I closed the homepage")).isFalse() + }, + dynamicTest("multiple parameters - empty and trimmed - middle of annotation text - mismatch") { + assertThat(matches("\$person opened the \$locale homepage", "opened the homepage")).isFalse() + }, + dynamicTest("multiple parameters - non-empty - middle of annotation text - mismatch") { + assertThat(matches("\$person opened the \$locale homepage", "I closed the HU homepage")).isFalse() + }, + dynamicTest("multiple parameters - empty - end of annotation text - mismatch") { + assertThat(matches("\$person opened the HU \$page", "I closed the HU ")).isFalse() + }, + dynamicTest("multiple parameters - empty and trimmed - end of annotation text - mismatch") { + assertThat(matches("\$person opened the HU \$page", "I opened the HU")).isFalse() + }, + dynamicTest("multiple parameters - non-empty - end of annotation text - mismatch") { + assertThat(matches("\$person opened the HU \$page", "I closed the HU homepage")).isFalse() + } + ) + } + + private fun matches(annotationText: String, stepText: String): Boolean { + return OptimizedStepMatcher(stepPatternParser.parseStep(GIVEN, annotationText)).matches(stepText) + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/spellchecker/JBehaveSpellcheckerStrategyTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/spellchecker/JBehaveSpellcheckerStrategyTest.java new file mode 100644 index 00000000..e3eb0a7e --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/spellchecker/JBehaveSpellcheckerStrategyTest.java @@ -0,0 +1,49 @@ +package com.github.kumaraman21.intellijbehave.spellchecker; + +import static com.intellij.spellchecker.tokenizer.SpellcheckingStrategy.EMPTY_TOKENIZER; +import static com.intellij.spellchecker.tokenizer.SpellcheckingStrategy.TEXT_TOKENIZER; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.kumaraman21.intellijbehave.language.StoryFileType; +import com.intellij.psi.PsiElement; +import com.intellij.testFramework.fixtures.BasePlatformTestCase; + +/** + * Unit test for {@link JBehaveSpellcheckerStrategy}. + */ +public class JBehaveSpellcheckerStrategyTest extends BasePlatformTestCase { + + public void testProvideSpellCheckFixesLeafStoryElements() { + String text = "Narrative:\n" + + "I want tea\n" + + "\n" + + "Scenario: Search for tea\n" + + "\n" + + "Given I open 'http://duckduckgo.com/'\n" + + "When I search for 'shu puerh'\n" + + "Then there should be 15 results\n" + + "Then the text 'shu puerh' should be present"; + + myFixture.configureByText(StoryFileType.STORY_FILE_TYPE, text); + PsiElement element = myFixture.getFile().findElementAt(myFixture.getCaretOffset()); + + assertThat(new JBehaveSpellcheckerStrategy().getTokenizer(element)).isSameAs(TEXT_TOKENIZER); + } + + public void testNotProvideSpellCheckFixesForNonStoryElements() { + String text = "Narrative:\n" + + "I want tea\n" + + "\n" + + "Scenario: Search for tea\n" + + "\n" + + "Given I open 'http://duckduckgo.com/'\n" + + "When I search for 'shu puerh'\n" + + "Then there should be 15 results\n" + + "Then the text 'shu puerh' should be present"; + + myFixture.configureByText(StoryFileType.STORY_FILE_TYPE, text); + PsiElement element = myFixture.getFile().findElementAt(myFixture.getCaretOffset()); + + assertThat(new JBehaveSpellcheckerStrategy().getTokenizer(element)).isSameAs(EMPTY_TOKENIZER); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/structure/JBehaveStructureViewElementTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/structure/JBehaveStructureViewElementTest.java new file mode 100644 index 00000000..8c7de776 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/structure/JBehaveStructureViewElementTest.java @@ -0,0 +1,178 @@ +package com.github.kumaraman21.intellijbehave.structure; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Collectors; +import javax.swing.*; + +import com.github.kumaraman21.intellijbehave.language.JBehaveIcons; +import com.github.kumaraman21.intellijbehave.parser.JBehaveStep; +import com.intellij.extapi.psi.ASTWrapperPsiElement; +import com.intellij.icons.AllIcons; +import com.intellij.ide.structureView.StructureViewTreeElement; +import com.intellij.psi.PsiFile; +import com.intellij.testFramework.fixtures.BasePlatformTestCase; + +/** + * Functional test for {@link JBehaveStructureViewElement}. + */ +public class JBehaveStructureViewElementTest extends BasePlatformTestCase { + @Override + protected String getTestDataPath() { + return "src/test/testData/structureview"; + } + + // Story + + public void testStoryItemIcon() { + PsiFile psiFile = myFixture.configureByText("a_story.story", "Narrative: this is the narrative"); + + Icon icon = new JBehaveStructureViewElement(getStoryElement(psiFile)).getIcon(false); + + assertThat(icon).isSameAs(JBehaveIcons.JB); + } + + public void testStoryItemWithFullText() { + PsiFile psiFile = myFixture.configureByText("a_story.story", + "Narrative: this is the narrative\n" + + "\n" + + "Scenario: a scenario"); + + String storyText = new JBehaveStructureViewElement(getStoryElement(psiFile)).getPresentableText(); + + assertThat(storyText).isEqualTo("Story"); + } + + public void testStoryItemWithTruncatedText() { + PsiFile psiFile = myFixture.configureByText("a_story.story", + "Narrative: this is the narrative\n" + + "with a second row of description\n" + + "and with a third row of description as well\n" + + "\n" + + "Scenario: a scenario"); + + String storyText = new JBehaveStructureViewElement(getStoryElement(psiFile)).getPresentableText(); + + assertThat(storyText).isEqualTo("Story"); + + } + + public void testStoryItemWithDefaultText() { + PsiFile psiFile = myFixture.configureByText("a_story.story", "Scenario: a scenario"); + + String storyText = new JBehaveStructureViewElement(getStoryElement(psiFile)).getPresentableText(); + + assertThat(storyText).isEqualTo("Story"); + } + + private ASTWrapperPsiElement getStoryElement(PsiFile psiFile) { + return (ASTWrapperPsiElement) psiFile.findElementAt(myFixture.getCaretOffset()).getParent().getParent(); + } + + // Lifecycle steps excluded + + public void testStoryLLifecycleSteps() { + PsiFile psiFile = myFixture.configureByText("a_story.story", + "Narrative: this is the narrative\n" + + "Lifecycle:\n" + + "Before:\n" + + "Scope: STORY\n" + + "Given a step that is executed before each story\n" + + "Scope: SCENARIO\n" + + "Given a step that is executed before each scenario\n" + + "Scenario: a scenario"); + + var children = new JBehaveStructureViewElement(getStoryElement(psiFile)).getChildrenBase() + .stream().map(StructureViewTreeElement::getValue).collect(Collectors.toList()); + + assertThat(children).doesNotHaveAnyElementsOfTypes(JBehaveStep.class); + } + + // Scenario + + public void testScenarioItemIcon() { + PsiFile psiFile = myFixture.configureByText("a_story.story", "Scenario: a scenario"); + + Icon icon = new JBehaveStructureViewElement(getElement(psiFile)).getIcon(false); + + assertThat(icon).isSameAs(AllIcons.Nodes.LogFolder); + } + + public void testScenarioItemWithFullText() { + PsiFile psiFile = myFixture.configureByText("a_story.story", "Scenario: a scenario"); + + String storyText = new JBehaveStructureViewElement(getElement(psiFile)).getPresentableText(); + + assertThat(storyText).isEqualTo("Scenario: a scenario"); + } + + public void testScenarioItemWithTruncatedText() { + PsiFile psiFile = myFixture.configureByText("a_story.story", + "Scenario: a scenario with a long long long and still long title"); + + String storyText = new JBehaveStructureViewElement(getElement(psiFile)).getPresentableText(); + + assertThat(storyText).isEqualTo("Scenario: a scenario with a long long long and sti..."); + + } + + public void testScenarioItemWithDefaultText() { + PsiFile psiFile = myFixture.configureByText("a_story.story", "Scenario:"); + + String storyText = new JBehaveStructureViewElement(getElement(psiFile)).getPresentableText(); + + assertThat(storyText).isEqualTo("Scenario"); + } + + private ASTWrapperPsiElement getElement(PsiFile psiFile) { + return (ASTWrapperPsiElement) psiFile.findElementAt(myFixture.getCaretOffset()).getParent(); + } + + // Step + + public void testStepItemIcon() { + PsiFile psiFile = myFixture.configureByText("a_story.story", + "Scenario: a scenario\n" + + "Given some precondition"); + + Icon icon = new JBehaveStructureViewElement(getElement(psiFile)).getIcon(false); + + assertThat(icon).isSameAs(AllIcons.Nodes.Static); + } + + public void testStepItemText() { + PsiFile psiFile = myFixture.configureByText("a_story.story", + "Scenario: a scenario\n" + + "Given some precondition"); + + String storyText = new JBehaveStructureViewElement(getElement(psiFile)).getPresentableText(); + + assertThat(storyText).isEqualTo("Given: some precondition"); + } + + // Examples + + public void testExamplesItemIcon() { + PsiFile psiFile = myFixture.configureByText("a_story.story", + "Scenario: a scenario\n" + + "Given some precondition\n" + + "Examples:\n" + + "| some | thing |"); + + Icon icon = new JBehaveStructureViewElement(getElement(psiFile)).getIcon(false); + + assertThat(icon).isNull(); + } + + public void testExamplesItemText() { + PsiFile psiFile = myFixture.configureByText("a_story.story", + "Scenario: a scenario\n" + + "Given some precondition\n" + + "Examples:\n" + + "| some | thing |"); + + String storyText = new JBehaveStructureViewElement(getElement(psiFile)).getPresentableText(); + + assertThat(storyText).isEqualTo("Examples"); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/utility/CharTreeTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/utility/CharTreeTest.java new file mode 100644 index 00000000..1ec00309 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/utility/CharTreeTest.java @@ -0,0 +1,31 @@ +package com.github.kumaraman21.intellijbehave.utility; + +import org.jbehave.core.i18n.LocalizedKeywords; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static com.github.kumaraman21.intellijbehave.utility.JBKeyword.*; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author @aloyer + */ +public class CharTreeTest { + + private CharTree charTree; + + @BeforeEach + public void setUp () { + LocalizedKeywords keywords = new LocalizedKeywords(); + charTree = new CharTree<>('/', null); + for (JBKeyword kw : JBKeyword.values()) { + String asString = kw.asString(keywords); + charTree.push(asString, kw); + } + } + + @Test + public void narrative_doubleDot() { + assertThat(charTree.lookup("Narrative: \n", 0)).isEqualTo(new CharTree.Entry<>(Narrative, 10)); + } +} diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/utility/LocalizedStorySupportTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/utility/LocalizedStorySupportTest.java new file mode 100644 index 00000000..789f5bdf --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/utility/LocalizedStorySupportTest.java @@ -0,0 +1,26 @@ +package com.github.kumaraman21.intellijbehave.utility; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * @author @aloyer + */ +public class LocalizedStorySupportTest { + + @Test + public void checkForLanguageDefinition_validCases() { + assertThat(LocalizedStorySupport.checkForLanguageDefinition(" !-- language:fr")).isEqualTo("fr"); + assertThat(LocalizedStorySupport.checkForLanguageDefinition(" !-- language: fr")).isEqualTo("fr"); + assertThat(LocalizedStorySupport.checkForLanguageDefinition(" !-- language: fr ")).isEqualTo("fr"); + assertThat(LocalizedStorySupport.checkForLanguageDefinition(" !-- language: fr ")).isEqualTo("fr"); + } + + @Test + public void checkForLanguageDefinition_invalidCases() { + assertThat(LocalizedStorySupport.checkForLanguageDefinition(" !-- languge:fr")).isNull(); + assertThat(LocalizedStorySupport.checkForLanguageDefinition(" !-- Language: fr")).isNull(); + assertThat(LocalizedStorySupport.checkForLanguageDefinition(" !-- lang: fr ")).isNull(); + } +} diff --git a/src/test/testData/codeinspector/undefinedstep/src/main/java/OtherStepDefs.java b/src/test/testData/codeinspector/undefinedstep/src/main/java/OtherStepDefs.java new file mode 100644 index 00000000..ebaa1d47 --- /dev/null +++ b/src/test/testData/codeinspector/undefinedstep/src/main/java/OtherStepDefs.java @@ -0,0 +1,10 @@ +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.Then; +import org.jbehave.core.steps.Steps; + +public class OtherStepDefs extends Steps { + + @Then("result list size is $size") + public void checkResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/codeinspector/undefinedstep/src/main/java/StepDefs.java b/src/test/testData/codeinspector/undefinedstep/src/main/java/StepDefs.java new file mode 100644 index 00000000..b3707a08 --- /dev/null +++ b/src/test/testData/codeinspector/undefinedstep/src/main/java/StepDefs.java @@ -0,0 +1,19 @@ +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.When; +import org.jbehave.core.steps.Steps; + +public class StepDefs extends Steps { + + @Given("Open url '$url'") + public void openAUrl(@Named("url") String url) { + } + + @When("search for $string") + public void searchForText(@Named("string") String string) { + } + + @Given(value = "result list size is $size", priority = 2) + public void setResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/codeinspector/undefinedstep/src/test/resources/undefined_steps.story b/src/test/testData/codeinspector/undefinedstep/src/test/resources/undefined_steps.story new file mode 100644 index 00000000..eb457a7c --- /dev/null +++ b/src/test/testData/codeinspector/undefinedstep/src/test/resources/undefined_steps.story @@ -0,0 +1,12 @@ +Narrative: +Testing a search result size + +Meta: +@Suite smoke testing +Scenario: open a url + +Given Open url 'http://some.url/path' +When search for 'something' +Then result list size is 10 +Then check result list size is 10 +Then check that result contains "an item" diff --git a/src/test/testData/codeinspector/unusedstepdeclarations/src/main/java/OtherStepDefs.java b/src/test/testData/codeinspector/unusedstepdeclarations/src/main/java/OtherStepDefs.java new file mode 100644 index 00000000..ebaa1d47 --- /dev/null +++ b/src/test/testData/codeinspector/unusedstepdeclarations/src/main/java/OtherStepDefs.java @@ -0,0 +1,10 @@ +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.Then; +import org.jbehave.core.steps.Steps; + +public class OtherStepDefs extends Steps { + + @Then("result list size is $size") + public void checkResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/codeinspector/unusedstepdeclarations/src/main/java/StepDefs.java b/src/test/testData/codeinspector/unusedstepdeclarations/src/main/java/StepDefs.java new file mode 100644 index 00000000..0c767412 --- /dev/null +++ b/src/test/testData/codeinspector/unusedstepdeclarations/src/main/java/StepDefs.java @@ -0,0 +1,19 @@ +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.When; +import org.jbehave.core.steps.Steps; + +public class StepDefs extends Steps { + + @Given("Open url '$url'") + public void openAUrl(@Named("url") String url) { + } + + @When("search for $string") + public void searchForText(@Named("string") String string) { + } + + @Given(value = "result list size is $size", priority = 2) + public void setResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/codeinspector/unusedstepdeclarations/src/test/resources/unused_step_declarations.story b/src/test/testData/codeinspector/unusedstepdeclarations/src/test/resources/unused_step_declarations.story new file mode 100644 index 00000000..e38d05aa --- /dev/null +++ b/src/test/testData/codeinspector/unusedstepdeclarations/src/test/resources/unused_step_declarations.story @@ -0,0 +1,10 @@ +Narrative: +Testing a search result size + +Meta: +@Suite smoke testing +Scenario: open a url + +Given Open url 'http://some.url/path' +!-- When search for 'something' +Then result list size is 10 diff --git a/src/test/testData/reference/steppsi/src/main/java/StepDefs.java b/src/test/testData/reference/steppsi/src/main/java/StepDefs.java new file mode 100644 index 00000000..783714c5 --- /dev/null +++ b/src/test/testData/reference/steppsi/src/main/java/StepDefs.java @@ -0,0 +1,20 @@ +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.Then; +import org.jbehave.core.annotations.When; +import org.jbehave.core.steps.Steps; + +public class StepDefs extends Steps { + + @Given("Open url '$url'") + public void openAUrl(@Named("url") String url) { + } + + @When("search for $string") + public void searchForText(@Named("string") String string) { + } + + @Then(value = "result list size is $size", priority = 2) + public void setResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/reference/steppsi/src/test/resources/step_reference.story b/src/test/testData/reference/steppsi/src/test/resources/step_reference.story new file mode 100644 index 00000000..87400f44 --- /dev/null +++ b/src/test/testData/reference/steppsi/src/test/resources/step_reference.story @@ -0,0 +1,12 @@ +Narrative: +Testing a search result size + +Meta: +@Suite smoke testing +Scenario: open a url + +Given Open url 'http://some.url/path' +When search for 'something' +Then result list size is 30 +When search for 'something else' +Then result list size is 25 \ No newline at end of file diff --git a/src/test/testData/reference/storystep/src/main/java/StepDefs.java b/src/test/testData/reference/storystep/src/main/java/StepDefs.java new file mode 100644 index 00000000..783714c5 --- /dev/null +++ b/src/test/testData/reference/storystep/src/main/java/StepDefs.java @@ -0,0 +1,20 @@ +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.Then; +import org.jbehave.core.annotations.When; +import org.jbehave.core.steps.Steps; + +public class StepDefs extends Steps { + + @Given("Open url '$url'") + public void openAUrl(@Named("url") String url) { + } + + @When("search for $string") + public void searchForText(@Named("string") String string) { + } + + @Then(value = "result list size is $size", priority = 2) + public void setResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/reference/storystep/src/test/resources/non_step_reference.story b/src/test/testData/reference/storystep/src/test/resources/non_step_reference.story new file mode 100644 index 00000000..aa847dc0 --- /dev/null +++ b/src/test/testData/reference/storystep/src/test/resources/non_step_reference.story @@ -0,0 +1,12 @@ +Narrative: +Testing a search result size + +Meta: +@Suite smoke testing +Scenario: open a url + +Given Open url 'http://some.url/path' +When search for 'something' +Then result list size is 30 +When search for 'something else' +Then result list size is 25 \ No newline at end of file diff --git a/src/test/testData/reference/storystep/src/test/resources/step_reference.story b/src/test/testData/reference/storystep/src/test/resources/step_reference.story new file mode 100644 index 00000000..87400f44 --- /dev/null +++ b/src/test/testData/reference/storystep/src/test/resources/step_reference.story @@ -0,0 +1,12 @@ +Narrative: +Testing a search result size + +Meta: +@Suite smoke testing +Scenario: open a url + +Given Open url 'http://some.url/path' +When search for 'something' +Then result list size is 30 +When search for 'something else' +Then result list size is 25 \ No newline at end of file diff --git a/src/test/testData/resolver/stepdefannotationconverter/src/main/java/StepDefs.java b/src/test/testData/resolver/stepdefannotationconverter/src/main/java/StepDefs.java new file mode 100644 index 00000000..2a3d3371 --- /dev/null +++ b/src/test/testData/resolver/stepdefannotationconverter/src/main/java/StepDefs.java @@ -0,0 +1,94 @@ +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.When; +import org.jbehave.core.annotations.Then; +import org.jbehave.core.annotations.Alias; +import org.jbehave.core.annotations.Aliases; +import org.jbehave.core.steps.Steps; + +public class StepDefs extends Steps { + + public void methodWithoutStepAnnotation() { + } + + //Given + + @Given() + public void givenStepDefWithoutAnnotationAttribute() { + } + + @Given("one given $string") + public void givenStepDefWithOneAnnotationAttribute() { + } + + @Given("multiple given $string", "given multiple something else") + public void givenStepDefWithMultipleAnnotationAttributes() { + } + + //When + + @When() + public void whenStepDefWithoutAnnotationAttribute() { + } + + @When("one when $string") + public void whenStepDefWithOneAnnotationAttribute() { + } + + @When("multiple when $string", "when multiple something else") + public void whenStepDefWithMultipleAnnotationAttributes() { + } + + //Then + + @Then() + public void thenStepDefWithoutAnnotationAttribute() { + } + + @Then("one then $string") + public void thenStepDefWithOneAnnotationAttribute() { + } + + @Then("multiple then $string", "then multiple something else") + public void thenStepDefWithMultipleAnnotationAttributes() { + } + + //Alias + + @Alias() + public void aliasStepDefWithoutAnnotationAttribute() { + } + + @Alias("one alias $string") + public void aliasStepDefWithOneAnnotationAttribute() { + } + + @Alias("multiple alias $string", "alias something else") + public void aliasStepDefWithMultipleAnnotationAttributes() { + } + + //Aliases + + @Aliases() + public void aliasesStepDefWithoutAnnotationAttribute() { + } + + @Aliases(values = {"one aliases $string"}) + public void aliasesStepDefWithOneAnnotationAttribute() { + } + + @Aliases(values = {"multiple aliases $string", "aliases something else"}) + public void aliasesStepDefWithMultipleAnnotationAttributes() { + } + + //Mixed + + @When("mixed when aliases $string") + @Aliases(values = {"mixed when aliases one $string", "mixed when aliases two"}) + public void whenAndAliasesStepDef() { + } + + @Aliases(values = {"mixed aliases when one $string", "mixed aliases when two"}) + @When("mixed aliases when $string") + public void aliasesAndWhenStepDef() { + } +} diff --git a/src/test/testData/resolver/stepdefiterator/src/main/java/OtherStepDefs.java b/src/test/testData/resolver/stepdefiterator/src/main/java/OtherStepDefs.java new file mode 100644 index 00000000..ebaa1d47 --- /dev/null +++ b/src/test/testData/resolver/stepdefiterator/src/main/java/OtherStepDefs.java @@ -0,0 +1,10 @@ +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.Then; +import org.jbehave.core.steps.Steps; + +public class OtherStepDefs extends Steps { + + @Then("result list size is $size") + public void checkResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/resolver/stepdefiterator/src/main/java/StepDefs.java b/src/test/testData/resolver/stepdefiterator/src/main/java/StepDefs.java new file mode 100644 index 00000000..b3707a08 --- /dev/null +++ b/src/test/testData/resolver/stepdefiterator/src/main/java/StepDefs.java @@ -0,0 +1,19 @@ +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.When; +import org.jbehave.core.steps.Steps; + +public class StepDefs extends Steps { + + @Given("Open url '$url'") + public void openAUrl(@Named("url") String url) { + } + + @When("search for $string") + public void searchForText(@Named("string") String string) { + } + + @Given(value = "result list size is $size", priority = 2) + public void setResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/resolver/stepdefiterator/src/main/kotlin/AnotherStepDefs.kt b/src/test/testData/resolver/stepdefiterator/src/main/kotlin/AnotherStepDefs.kt new file mode 100644 index 00000000..5c7d010e --- /dev/null +++ b/src/test/testData/resolver/stepdefiterator/src/main/kotlin/AnotherStepDefs.kt @@ -0,0 +1,12 @@ +import org.jbehave.core.annotations.Then +import org.jbehave.core.annotations.Named + +class AnotherStepDefs { + @Then("check result size is \$size") + fun checkSize(@Named("size") size: Int) { + } + + @Then("result ends with \$text") + fun checkEnding(@Named("text") text: Int) { + } +} \ No newline at end of file diff --git a/src/test/testData/resolver/stepdefiterator/src/test/resources/iterator.story b/src/test/testData/resolver/stepdefiterator/src/test/resources/iterator.story new file mode 100644 index 00000000..6483ba1f --- /dev/null +++ b/src/test/testData/resolver/stepdefiterator/src/test/resources/iterator.story @@ -0,0 +1,12 @@ +Narrative: +Testing a search result size + +Meta: +@Suite smoke testing +Scenario: open a url + +Given Open url 'http://some.url/path' +When search for 'something' +Then result list size is 10 +Then check result list size is 10 +Then check that result contains "an item" diff --git a/src/test/testData/service/javamethodusagesearch/src/main/java/MoreStepDefs.java b/src/test/testData/service/javamethodusagesearch/src/main/java/MoreStepDefs.java new file mode 100644 index 00000000..dffd1fca --- /dev/null +++ b/src/test/testData/service/javamethodusagesearch/src/main/java/MoreStepDefs.java @@ -0,0 +1,19 @@ +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.When; +import org.jbehave.core.steps.Steps; + +public class MoreStepDefs extends Steps { + + @Given("Open url '$url'") + public void openAUrl(@Named("url") String url) { + } + + @When("") + public void searchForText(@Named("string") String string) { + } + + @Given(value = "result list size is $size", priority = 2) + public void setResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/service/javamethodusagesearch/src/main/java/OtherStepDefs.java b/src/test/testData/service/javamethodusagesearch/src/main/java/OtherStepDefs.java new file mode 100644 index 00000000..98f98ff4 --- /dev/null +++ b/src/test/testData/service/javamethodusagesearch/src/main/java/OtherStepDefs.java @@ -0,0 +1,10 @@ +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.Then; +import org.jbehave.core.steps.Steps; + +public class OtherStepDefs extends Steps { + + @Then("result list size is $size") + public void checkResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/service/javamethodusagesearch/src/main/java/StepDefs.java b/src/test/testData/service/javamethodusagesearch/src/main/java/StepDefs.java new file mode 100644 index 00000000..c019d1c9 --- /dev/null +++ b/src/test/testData/service/javamethodusagesearch/src/main/java/StepDefs.java @@ -0,0 +1,18 @@ +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.When; +import org.jbehave.core.steps.Steps; + +public class StepDefs extends Steps { + + @Given("Open url '$url'") + public void openAUrl(@Named("url") String url) { + } + + public void searchForText(@Named("string") String string) { + } + + @Given(value = "result list size is $size", priority = 2) + public void setResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/service/javamethodusagesearch/src/test/resources/reference.story b/src/test/testData/service/javamethodusagesearch/src/test/resources/reference.story new file mode 100644 index 00000000..728270d2 --- /dev/null +++ b/src/test/testData/service/javamethodusagesearch/src/test/resources/reference.story @@ -0,0 +1,12 @@ +Narrative: +Testing a search result size + +Meta: +@Suite smoke testing +Scenario: open a url + +Given Open url 'http://some.url/path' +When search for 'something' +Then the result list size is 30 +When search for 'something else' +Then the result list size is 25 \ No newline at end of file diff --git a/src/test/testData/service/jbehaveutil/src/main/java/OtherStepDefs.java b/src/test/testData/service/jbehaveutil/src/main/java/OtherStepDefs.java new file mode 100644 index 00000000..e6b51aa7 --- /dev/null +++ b/src/test/testData/service/jbehaveutil/src/main/java/OtherStepDefs.java @@ -0,0 +1,10 @@ +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.Then; +import org.jbehave.core.steps.Steps; + +public class OtherStepDefs extends Steps { + + @Then("result list size is $size") + public void checkResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/service/jbehaveutil/src/main/java/StepDefs.java b/src/test/testData/service/jbehaveutil/src/main/java/StepDefs.java new file mode 100644 index 00000000..ae9d0573 --- /dev/null +++ b/src/test/testData/service/jbehaveutil/src/main/java/StepDefs.java @@ -0,0 +1,19 @@ +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.When; +import org.jbehave.core.steps.Steps; + +public class StepDefs extends Steps { + + @Given("Open url '$url'") + public void openAUrl(@Named("url") String url) { + } + + @When("search for $string") + public void searchForText(@Named("string") String string) { + } + + @Given(value = "result list size is $size", priority = 2) + public void setResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/service/jbehaveutil/src/test/resources/reference.story b/src/test/testData/service/jbehaveutil/src/test/resources/reference.story new file mode 100644 index 00000000..d54b467f --- /dev/null +++ b/src/test/testData/service/jbehaveutil/src/test/resources/reference.story @@ -0,0 +1,9 @@ +Narrative: +Testing a search result size + +Meta: +@Suite smoke testing +Scenario: open a url + +Given Open url 'http://some.url/path' +When search for 'something' diff --git a/src/test/testData/service/stepdefsearch/src/main/java/MoreStepDefs.java b/src/test/testData/service/stepdefsearch/src/main/java/MoreStepDefs.java new file mode 100644 index 00000000..855f52d1 --- /dev/null +++ b/src/test/testData/service/stepdefsearch/src/main/java/MoreStepDefs.java @@ -0,0 +1,19 @@ +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.When; +import org.jbehave.core.steps.Steps; + +public class MoreStepDefs extends Steps { + + @Given("Open url '$url'") + public void openAUrl(@Named("url") String url) { + } + + @When("search for $string") + public void searchForText(@Named("string") String string) { + } + + @Given(value = "result list size is $size", priority = 2) + public void setResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/service/stepdefsearch/src/main/java/OtherStepDefs.java b/src/test/testData/service/stepdefsearch/src/main/java/OtherStepDefs.java new file mode 100644 index 00000000..98f98ff4 --- /dev/null +++ b/src/test/testData/service/stepdefsearch/src/main/java/OtherStepDefs.java @@ -0,0 +1,10 @@ +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.Then; +import org.jbehave.core.steps.Steps; + +public class OtherStepDefs extends Steps { + + @Then("result list size is $size") + public void checkResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/service/stepdefsearch/src/main/java/StepDefs.java b/src/test/testData/service/stepdefsearch/src/main/java/StepDefs.java new file mode 100644 index 00000000..c019d1c9 --- /dev/null +++ b/src/test/testData/service/stepdefsearch/src/main/java/StepDefs.java @@ -0,0 +1,18 @@ +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.When; +import org.jbehave.core.steps.Steps; + +public class StepDefs extends Steps { + + @Given("Open url '$url'") + public void openAUrl(@Named("url") String url) { + } + + public void searchForText(@Named("string") String string) { + } + + @Given(value = "result list size is $size", priority = 2) + public void setResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/service/stepdefsearch/src/test/resources/reference.story b/src/test/testData/service/stepdefsearch/src/test/resources/reference.story new file mode 100644 index 00000000..d54b467f --- /dev/null +++ b/src/test/testData/service/stepdefsearch/src/test/resources/reference.story @@ -0,0 +1,9 @@ +Narrative: +Testing a search result size + +Meta: +@Suite smoke testing +Scenario: open a url + +Given Open url 'http://some.url/path' +When search for 'something' diff --git a/src/test/testData/stepsindex/src/main/java/OtherStepDefs.java b/src/test/testData/stepsindex/src/main/java/OtherStepDefs.java new file mode 100644 index 00000000..ebaa1d47 --- /dev/null +++ b/src/test/testData/stepsindex/src/main/java/OtherStepDefs.java @@ -0,0 +1,10 @@ +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.Then; +import org.jbehave.core.steps.Steps; + +public class OtherStepDefs extends Steps { + + @Then("result list size is $size") + public void checkResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/stepsindex/src/main/java/StepDefs.java b/src/test/testData/stepsindex/src/main/java/StepDefs.java new file mode 100644 index 00000000..b3707a08 --- /dev/null +++ b/src/test/testData/stepsindex/src/main/java/StepDefs.java @@ -0,0 +1,19 @@ +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.Named; +import org.jbehave.core.annotations.When; +import org.jbehave.core.steps.Steps; + +public class StepDefs extends Steps { + + @Given("Open url '$url'") + public void openAUrl(@Named("url") String url) { + } + + @When("search for $string") + public void searchForText(@Named("string") String string) { + } + + @Given(value = "result list size is $size", priority = 2) + public void setResultListSize(@Named("size") int size) { + } +} diff --git a/src/test/testData/stepsindex/src/main/kotlin/AnotherStepDefs.kt b/src/test/testData/stepsindex/src/main/kotlin/AnotherStepDefs.kt new file mode 100644 index 00000000..5c7d010e --- /dev/null +++ b/src/test/testData/stepsindex/src/main/kotlin/AnotherStepDefs.kt @@ -0,0 +1,12 @@ +import org.jbehave.core.annotations.Then +import org.jbehave.core.annotations.Named + +class AnotherStepDefs { + @Then("check result size is \$size") + fun checkSize(@Named("size") size: Int) { + } + + @Then("result ends with \$text") + fun checkEnding(@Named("text") text: Int) { + } +} \ No newline at end of file diff --git a/src/test/testData/stepsindex/src/test/resources/has_java_step_def.story b/src/test/testData/stepsindex/src/test/resources/has_java_step_def.story new file mode 100644 index 00000000..51ca4143 --- /dev/null +++ b/src/test/testData/stepsindex/src/test/resources/has_java_step_def.story @@ -0,0 +1,10 @@ +Narrative: +Testing a search result size + +Meta: +@Suite smoke testing +Scenario: open a url + +Given Open url 'http://some.url/path' +When search for 'something' +Then check result list size is 10 diff --git a/src/test/testData/stepsindex/src/test/resources/has_multiple_java_step_def.story b/src/test/testData/stepsindex/src/test/resources/has_multiple_java_step_def.story new file mode 100644 index 00000000..5983fc59 --- /dev/null +++ b/src/test/testData/stepsindex/src/test/resources/has_multiple_java_step_def.story @@ -0,0 +1,10 @@ +Narrative: +Testing a search result size + +Meta: +@Suite smoke testing +Scenario: open a url + +Given Open url 'http://some.url/path' +When search for 'something' +Then result list size is 10 diff --git a/src/test/testData/stepsindex/src/test/resources/has_no_java_step_def.story b/src/test/testData/stepsindex/src/test/resources/has_no_java_step_def.story new file mode 100644 index 00000000..4e132a8a --- /dev/null +++ b/src/test/testData/stepsindex/src/test/resources/has_no_java_step_def.story @@ -0,0 +1,11 @@ +Narrative: +Testing a search result size + +Meta: +@Suite smoke testing +Scenario: open a url + +Given Open url 'http://some.url/path' +When search for 'something' +Then check result list size is 10 +Then check that result contains "an item"