diff --git a/.github/workflows/build-test-package-publish.yml b/.github/workflows/build-test-package-publish.yml index 76a837d0..c7d968ad 100644 --- a/.github/workflows/build-test-package-publish.yml +++ b/.github/workflows/build-test-package-publish.yml @@ -114,14 +114,41 @@ jobs: - name: Ensure latest hatch is installed run: pipx install hatch + - name: Gradle Wrapper Validation + uses: gradle/actions/wrapper-validation@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: jetbrains + java-version: 21 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-cleanup: always + + # Set environment variables + - name: Export Properties + id: properties + shell: bash + run: | + echo "pluginVerifierHomeDir=~/.pluginVerifier" >> $GITHUB_OUTPUT + - run: hatch run lint:style if: always() - run: hatch run lint:typing if: always() + - run: | + cd intellij-client + gradle verifyPlugin --console=plain + + if: always() + package: - #needs: [code-quality, test] + needs: [code-quality, test] runs-on: ubuntu-latest @@ -155,13 +182,13 @@ jobs: - name: Setup Java uses: actions/setup-java@v4 with: - distribution: zulu + distribution: jetbrains java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 with: - gradle-home-cache-cleanup: true + cache-cleanup: always # Set environment variables - name: Export Properties diff --git a/CHANGELOG.md b/CHANGELOG.md index deb8b3cd..cc3a7e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. +## [0.105.0](https://github.com/robotcodedev/robotcode/compare/v0.104.0..v0.105.0) - 2025-01-06 + +### Documentation + +- Update readme's ([f4f576f](https://github.com/robotcodedev/robotcode/commit/f4f576f3222ac3cf08904a336615edf45c57f922)) + + +### Features + +- **intellij:** Added initial Robot Framework file templates and better syntax highlighting support based on a customized TextMate lexer/parser ([ef67d2c](https://github.com/robotcodedev/robotcode/commit/ef67d2c661c6ae8c8c734561b54f63d82f9b4e3c)) +- **intellij:** Implemented brace matcher for variables ([9ee1f22](https://github.com/robotcodedev/robotcode/commit/9ee1f22c2f077a1c92ac9f5f6dc37542fb24e79c)) +- **intellij:** Finalize syntax coloring and sematic highlightning ([faa905d](https://github.com/robotcodedev/robotcode/commit/faa905d048d4af076695b619d91ee97371cfd0fd)) +- **intellij:** Finalize syntax coloring and sematic highlightning part 2 ([97081a5](https://github.com/robotcodedev/robotcode/commit/97081a5f13a7f58d6825853330c8806506ad6fde)) +- **intellij:** Implemented support for highlightning embedded arguments, escape sequences, python expressions and so on ([c5d6cf3](https://github.com/robotcodedev/robotcode/commit/c5d6cf3722c22929799b154204711c697928da7e)) +- **langserver:** Correct highlightning to better highlight python expresseions, escape sequences, environment variable default value and embedded argument regexes ([5dba571](https://github.com/robotcodedev/robotcode/commit/5dba5713f746cb9fb1b36433be982493bb3905d2)) +- **language_server:** Enhanced hover representation of variables ([039682d](https://github.com/robotcodedev/robotcode/commit/039682dbdd19a6e1dc972f642dbfd5f1787b4dcd)) + + +### Testing + +- Update regression tests ([c5f4573](https://github.com/robotcodedev/robotcode/commit/c5f4573c1b9ea9a2db720c5044bf7a2cdc8e2bda)) + + ## [0.104.0](https://github.com/robotcodedev/robotcode/compare/v0.103.0..v0.104.0) - 2024-12-30 ### Bug Fixes diff --git a/README.md b/README.md index 25fd1368..16a88493 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,15 @@ # RobotCode - Language Support for Robot Framework in Visual Studio Code -[![VS Marketplace](https://img.shields.io/visual-studio-marketplace/v/d-biehl.robotcode?style=flat&label=VS%20Marketplace&logo=visual-studio-code)](https://marketplace.visualstudio.com/items?itemName=d-biehl.robotcode) -[![Installs](https://img.shields.io/visual-studio-marketplace/i/d-biehl.robotcode?style=flat)](https://marketplace.visualstudio.com/items?itemName=d-biehl.robotcode) -[![Build Status](https://img.shields.io/github/actions/workflow/status/robotcodedev/robotcode/build-test-package-publish.yml?branch=main&style=flat&logo=github)](https://github.com/robotcodedev/robotcode/actions?query=workflow:build_test_package_publish) [![License](https://img.shields.io/github/license/robotcodedev/robotcode?style=flat&logo=apache)](https://github.com/robotcodedev/robotcode/blob/master/LICENSE) +[![Build Status](https://img.shields.io/github/actions/workflow/status/robotcodedev/robotcode/build-test-package-publish.yml?branch=main&style=flat&logo=github)](https://github.com/robotcodedev/robotcode/actions?query=workflow:build_test_package_publish) + +[![VS Code Marketplace](https://img.shields.io/visual-studio-marketplace/v/d-biehl.robotcode?style=flat&label=VS%20Marketplace&logo=visual-studio-code)](https://marketplace.visualstudio.com/items?itemName=d-biehl.robotcode) +[![Installs](https://img.shields.io/visual-studio-marketplace/i/d-biehl.robotcode?style=flat)](https://marketplace.visualstudio.com/items?itemName=d-biehl.robotcode) + +[![JETBRAINS Marketplace](https://img.shields.io/jetbrains/plugin/v/26216.svg)](https://plugins.jetbrains.com/plugin/26216) +[![Downloads](https://img.shields.io/jetbrains/plugin/d/26216.svg)](https://plugins.jetbrains.com/plugin/26216) + [![PyPI - Version](https://img.shields.io/pypi/v/robotcode.svg?style=flat)](https://pypi.org/project/robotcode) [![Python Version](https://img.shields.io/pypi/pyversions/robotcode.svg?style=flat)](https://pypi.org/project/robotcode) @@ -12,16 +17,38 @@ --- -**RobotCode** is a Visual Studio Code extension that enhances your workflow with [Robot Framework](https://robotframework.org/). It offers features like code completion, debugging, test discovery and execution, refactoring, and more! +**RobotCode** is a Visual Studio Code extension that enhances your workflow with [Robot Framework](https://robotframework.org/). +It provides a rich set of features to help you write, run, and debug your Robot Framework tests directly within Visual Studio Code. + +## Why RobotCode? + +**Built on Robot Framework Core** +RobotCode is based on the Robot Framework Core and uses its parser, ensuring complete compatibility and consistency. This means you get the same syntax validation, error messages, and behavior as if you were running Robot Framework directly. + +**Powered by the Language Server Protocol** +RobotCode is built on the Language Server Protocol (LSP), a modern standard for implementing language support across multiple editors and IDEs. This ensures a seamless and responsive user experience, while making it easier to maintain compatibility with evolving IDE features. + +**Powerful Command Line Tools** +RobotCode extends the Robot Framework CLI with enhanced tools for test execution, analysis, and debugging. It supports [`robot.toml`](https://robotcode.io/03_reference/) configurations, integrates a Debug Adapter Protocol (DAP) compatible debugger, and provides an interactive REPL environment for experimenting with Robot Framework commands. Modular and flexible, these tools streamline your workflow for both development and production. ## Key Features -- **Code Editing**: Enjoy code auto-completion, navigation, syntax checking, and more. +- **Code Editing**: Enjoy code auto-completion, navigation and more. +- **IntelliSense**: Get code completion suggestions for keywords, variables, and more. +- **Refactoring**: Rename variables, keywords, arguments and more with ease and project wide. +- **Enhanced Syntax Highlighting**: Easily identify and read your Robot Framework code with support highlight embedded arguments, python expressions, environment variables with default values, and more. +- **Code Snippets**: Quickly insert common Robot Framework code snippets. +- **Test Discovery**: Discover and run Robot Framework test cases directly within VS Code. +- **Test Execution**: Execute Robot Framework test cases and suites directly within VS Code. +- **Test Reports**: View test reports directly within VS Code. +- **Debugging**: Debug your Robot Framework tests with ease. - **Command Line Tools**: A wide array of tools to assist in setting up and managing Robot Framework environments. +- **Code Analysis with Robocop**: Install [Robocop](https://robocop.readthedocs.io/) for additional code analysis. - **Code Formatting**: Format your code using Robot Framework’s built-in tools like `robot.tidy` or [Robotidy](https://robotidy.readthedocs.io/). -- **Test Running and Debugging**: Run and debug Robot Framework test cases directly within VS Code. - **Multi-root Workspace Support**: Manage multiple Robot Framework projects with different Python environments simultaneously. -- **Code Analysis with Robocop**: Install [Robocop](https://robocop.readthedocs.io/) for additional code analysis. +- **Customizable Settings**: Configure the extension to fit your needs. +- **RobotCode Repl and Notebooks**: Play with Robot Framework in a Jupyter Notebook-like environment. +- **And More!**: Check out the [official documentation](https://robotcode.io) for more details. ## Requirements diff --git a/cliff.toml b/cliff.toml index 56cf6b92..4a3daa0e 100644 --- a/cliff.toml +++ b/cliff.toml @@ -81,10 +81,7 @@ commit_parsers = [ { message = "^style", group = "Styling", skip = true }, { message = "^test", group = "Testing" }, { message = "^chore\\(release\\): prepare for", skip = true }, - { message = "^chore\\(deps\\)", skip = true }, - { message = "^chore\\(pr\\)", skip = true }, - { message = "^chore\\(pull\\)", skip = true }, - { message = "^chore\\(build\\)", skip = true }, + { message = "^chore\\(.*\\)", group = "Miscellaneous Tasks", skip = true }, { message = "^chore|ci", group = "Miscellaneous Tasks", skip = true }, { message = "^bump", skip = true }, { body = ".*security", group = "Security" }, diff --git a/docs/index.md b/docs/index.md index 15ae6047..ba310f63 100644 --- a/docs/index.md +++ b/docs/index.md @@ -47,10 +47,16 @@ features: --- -[![Visual Studio Marketplace](https://img.shields.io/visual-studio-marketplace/v/d-biehl.robotcode?style=flat&label=VS%20Marketplace&logo=visual-studio-code)](https://marketplace.visualstudio.com/items?itemName=d-biehl.robotcode) -[![Installs](https://img.shields.io/visual-studio-marketplace/i/d-biehl.robotcode?style=flat)](https://marketplace.visualstudio.com/items?itemName=d-biehl.robotcode) +[![License](https://img.shields.io/github/license/robotcodedev/robotcode?style=flat&logo=apache)](https://github.com/robotcodedev/robotcode/blob/master/LICENSE) [![Build Status](https://img.shields.io/github/actions/workflow/status/robotcodedev/robotcode/build-test-package-publish.yml?branch=main&style=flat&logo=github)](https://github.com/robotcodedev/robotcode/actions?query=workflow:build_test_package_publish) + +[![VS Code Marketplace](https://img.shields.io/visual-studio-marketplace/v/d-biehl.robotcode?style=flat&label=VS%20Marketplace&logo=visual-studio-code)](https://marketplace.visualstudio.com/items?itemName=d-biehl.robotcode) +[![Installs](https://img.shields.io/visual-studio-marketplace/i/d-biehl.robotcode?style=flat)](https://marketplace.visualstudio.com/items?itemName=d-biehl.robotcode) + +[![JETBRAINS Marketplace](https://img.shields.io/jetbrains/plugin/v/26216.svg)](https://plugins.jetbrains.com/plugin/26216) +[![Downloads](https://img.shields.io/jetbrains/plugin/d/26216.svg)](https://plugins.jetbrains.com/plugin/26216) + + [![PyPI - Version](https://img.shields.io/pypi/v/robotcode.svg?style=flat)](https://pypi.org/project/robotcode) -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/robotcode.svg?style=flat)](https://pypi.org/project/robotcode) -[![PyPI - Downloads](https://img.shields.io/pypi/dm/robotcode.svg?style=flat&label=downloads)](https://pypi.org/project/robotcode/) -{align="center"} \ No newline at end of file +[![Python Version](https://img.shields.io/pypi/pyversions/robotcode.svg?style=flat)](https://pypi.org/project/robotcode) +[![Downloads](https://img.shields.io/pypi/dm/robotcode.svg?style=flat&label=downloads)](https://pypi.org/project/robotcode) diff --git a/docs/package.json b/docs/package.json index d11390cd..efd94aad 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,10 +13,10 @@ "markdown-it-mathjax3": "^4.3.2", "markdown-it-task-lists": "^2.1.1", "typescript": "^5.7.2", - "typescript-eslint": "^8.17.0", + "typescript-eslint": "^8.19.0", "vitepress": "^1.5.0", "vitepress-plugin-tabs": "^0.5.0", - "vitepress-sidebar": "^1.29.0" + "vitepress-sidebar": "^1.30.2" }, "dependencies": { "docs": "file:" diff --git a/hatch.toml b/hatch.toml index 9f87cde5..d977095d 100644 --- a/hatch.toml +++ b/hatch.toml @@ -45,7 +45,7 @@ install-packages = "python ./scripts/install_packages.py" [envs.rfbeta] python = "3.13" -extra-dependencies = ["robotframework==7.2b1"] +extra-dependencies = ["robotframework==7.2rc1"] [envs.rfmaster313] python = "3.13" @@ -98,8 +98,8 @@ matrix.rf.dependencies = [ { value = "robotframework>=7.1, <7.2", if = [ "rf71", ] }, - { value = "robotframework>=7.2, <7.3", if = [ - "rf72b1", + { value = "robotframework==7.2rc1", if = [ + "rf72", ] }, ] @@ -130,8 +130,8 @@ matrix.rf.dependencies = [ { value = "robotframework>=7.1, <7.2", if = [ "rf71", ] }, - { value = "robotframework>=7.2, <7.3", if = [ - "rf72b1", + { value = "robotframework==7.2rc1", if = [ + "rf72", ] }, ] diff --git a/intellij-client/README.md b/intellij-client/README.md index ac039692..6fef2720 100644 --- a/intellij-client/README.md +++ b/intellij-client/README.md @@ -1,47 +1,78 @@ # robotcode4ij -![Build](https://github.com/robotcodedev/robotcode4ij/workflows/Build/badge.svg) -[![Version](https://img.shields.io/jetbrains/plugin/v/MARKETPLACE_ID.svg)](https://plugins.jetbrains.com/plugin/MARKETPLACE_ID) -[![Downloads](https://img.shields.io/jetbrains/plugin/d/MARKETPLACE_ID.svg)](https://plugins.jetbrains.com/plugin/MARKETPLACE_ID) - -## Template ToDo list -- [x] Create a new [IntelliJ Platform Plugin Template][template] project. -- [ ] Get familiar with the [template documentation][template]. -- [ ] Adjust the [pluginGroup](./gradle.properties) and [pluginName](./gradle.properties), as well as the [id](./src/main/resources/META-INF/plugin.xml) and [sources package](./src/main/kotlin). -- [ ] Adjust the plugin description in `README` (see [Tips][docs:plugin-description]) -- [ ] Review the [Legal Agreements](https://plugins.jetbrains.com/docs/marketplace/legal-agreements.html?from=IJPluginTemplate). -- [ ] [Publish a plugin manually](https://plugins.jetbrains.com/docs/intellij/publishing-plugin.html?from=IJPluginTemplate) for the first time. -- [ ] Set the `MARKETPLACE_ID` in the above README badges. You can obtain it once the plugin is published to JetBrains Marketplace. -- [ ] Set the [Plugin Signing](https://plugins.jetbrains.com/docs/intellij/plugin-signing.html?from=IJPluginTemplate) related [secrets](https://github.com/JetBrains/intellij-platform-plugin-template#environment-variables). -- [ ] Set the [Deployment Token](https://plugins.jetbrains.com/docs/marketplace/plugin-upload.html?from=IJPluginTemplate). -- [ ] Click the Watch button on the top of the [IntelliJ Platform Plugin Template][template] to be notified about releases containing new features and fixes. +[![JETBRAINS Marketplace](https://img.shields.io/jetbrains/plugin/v/26216.svg)](https://plugins.jetbrains.com/plugin/26216) +[![Downloads](https://img.shields.io/jetbrains/plugin/d/26216.svg)](https://plugins.jetbrains.com/plugin/26216) + -Robot Framework IntelliSense, linting, test execution and debugging, code formatting, refactoring, and many more. + +⚠️ **Important Notice** ⚠️ +This plugin is currently under active development and is not yet ready for production use. Please note that it may contain bugs or lack certain features. + +We invite you to join the Robot Framework and RobotCode community by reporting issues, suggesting features, and helping us improve the plugin. + +Your feedback is greatly appreciated! 🙂 + + +**RobotCode** is a PyCharm/IntelliJ Plugin that enhances your workflow with [Robot Framework](https://robotframework.org/). +It provides a rich set of features to help you write, run, and debug your Robot Framework tests directly within your IDE. + +## Why RobotCode? + +**Built on Robot Framework Core** +RobotCode is based on the Robot Framework Core and uses its parser, ensuring complete compatibility and consistency. This means you get the same syntax validation, error messages, and behavior as if you were running Robot Framework directly. + +**Powered by the Language Server Protocol** +RobotCode is built on the Language Server Protocol (LSP), a modern standard for implementing language support across multiple editors and IDEs. This ensures a seamless and responsive user experience, while making it easier to maintain compatibility with evolving IDE features. + +**Powerful Command Line Tools** +RobotCode extends the Robot Framework CLI with enhanced tools for test execution, analysis, and debugging. It supports [`robot.toml`](https://robotcode.io/03_reference/) configurations, integrates a Debug Adapter Protocol (DAP) compatible debugger, and provides an interactive REPL environment for experimenting with Robot Framework commands. Modular and flexible, these tools streamline your workflow for both development and production. + +## Key Features + +- **Smart Code Editing**: Auto-completion, syntax highlighting, and seamless navigation. +- **Refactoring**: Easily rename variables, keywords, and arguments across your project. +- **Integrated Debugging**: Debug Robot Framework tests directly within the IDE. +- **Test Management**: Discover, run, and monitor Robot Framework tests without leaving your IDE. +- **Rich Test Reports**: View detailed test results and logs directly in the IDE. +- **Code Analysis**: Leverage tools like [Robocop](https://robocop.readthedocs.io/) for linting and static code analysis. +- **Formatting Made Easy**: Use [Robotidy](https://robotidy.readthedocs.io/) for consistent code formatting. +- **Support for `robot.toml`**: Manage your Robot Framework projects with ease. +- **More Features Coming Soon!** + + +## Requirements + +- Python 3.8 or newer +- Robot Framework 4.1 or newer +- PyCharm 2024.3.1 or newer + +## Getting Started + +1. Install the [RobotCode Plugin](https://plugins.jetbrains.com/plugin/26216) from the JETBRAINS Marketplace. +2. Configure your Robot Framework Python environment +3. Start writing and running your Robot Framework tests! + +(Comming soon...) +For a more detailed guide, check out the [Let's get started](https://robotcode.io/02_get_started/) Guide on the [RobotCode](https://robotcode.io) website. + ## Installation - Using the IDE built-in plugin system: - - Settings/Preferences > Plugins > Marketplace > Search for "robotcode4ij" > + + Settings/Preferences > Plugins > Marketplace > Search for "RobotCode" > Install - + - Using JetBrains Marketplace: - Go to [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/MARKETPLACE_ID) and install it by clicking the Install to ... button in case your IDE is running. + Go to [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/26216) and install it by clicking the Install to ... button in case your IDE is running. - You can also download the [latest release](https://plugins.jetbrains.com/plugin/MARKETPLACE_ID/versions) from JetBrains Marketplace and install it manually using + You can also download the [latest release](https://plugins.jetbrains.com/plugin/26216/versions) from JetBrains Marketplace and install it manually using Settings/Preferences > Plugins > ⚙️ > Install plugin from disk... - Manually: - Download the [latest release](https://github.com/robotcodedev/robotcode4ij/releases/latest) and install it manually using + Download the [latest release](https://github.com/robotcodedev/robotcode/releases/latest) and install it manually using Settings/Preferences > Plugins > ⚙️ > Install plugin from disk... - - ---- -Plugin based on the [IntelliJ Platform Plugin Template][template]. - -[template]: https://github.com/JetBrains/intellij-platform-plugin-template -[docs:plugin-description]: https://plugins.jetbrains.com/docs/intellij/plugin-user-experience.html#plugin-description-and-presentation diff --git a/intellij-client/build.gradle.kts b/intellij-client/build.gradle.kts index 379c4ee6..5a40835b 100644 --- a/intellij-client/build.gradle.kts +++ b/intellij-client/build.gradle.kts @@ -1,7 +1,9 @@ import org.jetbrains.changelog.Changelog import org.jetbrains.changelog.markdownToHTML import org.jetbrains.intellij.platform.gradle.Constants.Constraints +import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType import org.jetbrains.intellij.platform.gradle.TestFrameworkType +import org.jetbrains.intellij.platform.gradle.tasks.PrepareSandboxTask import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest @@ -29,7 +31,7 @@ kotlin { // 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() @@ -41,7 +43,7 @@ dependencies { compileOnly(libs.kotlinxSerialization) testImplementation(kotlin("test")) testImplementation(libs.junit) - + // IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html intellijPlatform { create( @@ -49,31 +51,11 @@ dependencies { providers.gradleProperty("platformVersion"), useInstaller = false ) - + // 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(',') }) - - val platformPlugins = ArrayList() - // val localLsp4ij = file("../lsp4ij.old/build/idea-sandbox/plugins/LSP4IJ").absoluteFile - // if (localLsp4ij.isDirectory) { - // // In case Gradle fails to build because it can't find some missing jar, try deleting - // // ~/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/unzipped.com.jetbrains.plugins/com.redhat.devtools.lsp4ij* - // localPlugin(localLsp4ij.toString()) - // } else { - // // When running on CI or when there's no local lsp4ij - // val latestLsp4ijNightlyVersion = fetchLatestLsp4ijNightlyVersion() - // platformPlugins.add("com.redhat.devtools.lsp4ij:$latestLsp4ijNightlyVersion@nightly") - // } - - platformPlugins.add("com.redhat.devtools.lsp4ij:0.9.0") - //Uses `platformPlugins` property from the gradle.properties file. - platformPlugins.addAll(providers.gradleProperty("platformPlugins").map { it.split(',').map(String::trim).filter(String::isNotEmpty) }.get()) - - plugins(platformPlugins) - + plugins(providers.gradleProperty("platformPlugins").map { it.split(',') }) + pluginVerifier() zipSigner() testFramework(TestFrameworkType.Platform) @@ -85,12 +67,12 @@ 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") @@ -98,7 +80,7 @@ intellijPlatform { 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 -> @@ -111,19 +93,19 @@ intellijPlatform { ) } } - + ideaVersion { sinceBuild = providers.gradleProperty("pluginSinceBuild") untilBuild = providers.gradleProperty("pluginUntilBuild") } } - + signing { certificateChain = providers.environmentVariable("CERTIFICATE_CHAIN") privateKey = providers.environmentVariable("PRIVATE_KEY") password = providers.environmentVariable("PRIVATE_KEY_PASSWORD") } - + publishing { token = providers.environmentVariable("PUBLISH_TOKEN") // The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 @@ -132,7 +114,7 @@ intellijPlatform { channels = providers.gradleProperty("pluginVersion") .map { listOf(it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) } } - + pluginVerification { ides { recommended() @@ -158,6 +140,15 @@ kover { } } +val prepareSandboxConfig: PrepareSandboxTask.() -> Unit = { + from("..") { + include("package.json", "language-configuration.json", "syntaxes/**/*", "bundled/**/*") + exclude("**/bin") + exclude("**/__pycache__") + into("robotcode4ij/data") + } +} + tasks { runIde { // From https://app.slack.com/client/T5P9YATH9/C5U8BM1MK @@ -166,25 +157,20 @@ tasks { // systemProperty("terminal.new.ui", "false") // systemProperty("ide.tree.painter.compact.default", "true") } - + wrapper { gradleVersion = providers.gradleProperty("gradleVersion").get() } - + publishPlugin { dependsOn(patchChangelog) } - prepareSandbox { - from("..") { - include("package.json", "language-configuration.json", "syntaxes/**/*", "bundled/**/*" ) - - exclude("**/bin") - exclude("**/__pycache__") - into("robotcode4ij/data") - } - } + + prepareSandbox(prepareSandboxConfig) } + + // Configure UI tests plugin // Read more: https://github.com/JetBrains/intellij-ui-test-robot val runIdeForUiTests by intellijPlatformTesting.runIde.registering { @@ -198,12 +184,26 @@ val runIdeForUiTests by intellijPlatformTesting.runIde.registering { ) } } - + + prepareSandboxTask(prepareSandboxConfig) + plugins { robotServerPlugin(Constraints.LATEST_VERSION) } } +val runIdePyCharmProf by intellijPlatformTesting.runIde.registering { + type = IntelliJPlatformType.PyCharmProfessional + + prepareSandboxTask(prepareSandboxConfig) +} + +val runIdeIntellijIdeaC by intellijPlatformTesting.runIde.registering { + type = IntelliJPlatformType.IntellijIdeaCommunity + + prepareSandboxTask(prepareSandboxConfig) +} + fun fetchLatestLsp4ijNightlyVersion(): String { val client = HttpClient.newBuilder().build(); var onlineVersion = "" @@ -220,10 +220,10 @@ fun fetchLatestLsp4ijNightlyVersion(): String { onlineVersion = matcher.group(1) println("Latest approved nightly build: $onlineVersion") } - } catch (e:Exception) { + } catch (e: Exception) { println("Failed to fetch LSP4IJ nightly build version: ${e.message}") } - + val minVersion = "0.0.1-20231213-012910" return if (minVersion < onlineVersion) onlineVersion else minVersion } diff --git a/intellij-client/gradle.properties b/intellij-client/gradle.properties index 6ce92ab5..0c35fc30 100644 --- a/intellij-client/gradle.properties +++ b/intellij-client/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = dev.robotcode pluginName = RobotCode - Robot Framework Support pluginRepositoryUrl = https://github.com/robotcodedev/robotcode4ij # SemVer format -> https://semver.org -pluginVersion = 0.104.0 +pluginVersion = 0.105.0 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 243 @@ -13,17 +13,16 @@ pluginUntilBuild = 243.* # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension platformType = PC -platformVersion = 2024.3 +platformVersion = 2024.3.1 # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP -#platformPlugins = com.redhat.devtools.lsp4ij:0.3.0 -platformPlugins = +platformPlugins = com.redhat.devtools.lsp4ij:0.9.0 # Example: platformBundledPlugins = com.intellij.java platformBundledPlugins = PythonCore, org.jetbrains.plugins.textmate # Gradle Releases -> https://github.com/gradle/gradle/releases -gradleVersion = 8.11.1 +gradleVersion = 8.12 # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib kotlin.stdlib.default.dependency = false @@ -36,3 +35,4 @@ org.gradle.caching = true kotlin.daemon.jvmargs=-Xmx4096m +kotlin.code.style=official diff --git a/intellij-client/gradle/wrapper/gradle-wrapper.properties b/intellij-client/gradle/wrapper/gradle-wrapper.properties index e2847c82..cea7a793 100644 --- a/intellij-client/gradle/wrapper/gradle-wrapper.properties +++ b/intellij-client/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/BundledHelpers.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/BundledHelpers.kt deleted file mode 100644 index 06768284..00000000 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/BundledHelpers.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.robotcode.robotcode4ij - -import com.intellij.openapi.application.PathManager -import java.nio.file.Path - -class BundledHelpers { - companion object { - val basePath: Path = PathManager.getPluginsDir().resolve("robotcode4ij").resolve("data") - val bundledPath: Path = basePath.resolve("bundled") - val toolPath: Path = bundledPath.resolve("tool") - val robotCodePath: Path = toolPath.resolve("robotcode") - val checkRobotVersion: Path = toolPath.resolve("utils").resolve("check_robot_version.py") - } -} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeHelpers.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeHelpers.kt new file mode 100644 index 00000000..f1d4d688 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeHelpers.kt @@ -0,0 +1,112 @@ +package dev.robotcode.robotcode4ij + +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.util.ExecUtil +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.jetbrains.python.sdk.pythonSdk +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.exists +import kotlin.io.path.isRegularFile +import kotlin.io.path.pathString + +class RobotCodeHelpers { + companion object { + val basePath: Path = PathManager.getPluginsDir().resolve("robotcode4ij").resolve("data") + val bundledPath: Path = basePath.resolve("bundled") + val toolPath: Path = bundledPath.resolve("tool") + val robotCodePath: Path = toolPath.resolve("robotcode") + val checkRobotVersion: Path = toolPath.resolve("utils").resolve("check_robot_version.py") + + val PYTHON_AND_ROBOT_OK_KEY = Key.create("ROBOTCODE_PYTHON_AND_ROBOT_OK") + } +} + +fun Project.checkPythonAndRobotVersion(reset: Boolean = false): Boolean { + if (!reset && this.getUserData(RobotCodeHelpers.PYTHON_AND_ROBOT_OK_KEY) == true) { + return true + } + + val result = ApplicationManager.getApplication().executeOnPooledThread { + + val pythonInterpreter = this.pythonSdk?.homePath + + if (pythonInterpreter == null) { + thisLogger().info("No Python Interpreter defined for project '${this.name}'") + return@executeOnPooledThread false + } + + if (!Path(pythonInterpreter).exists()) { + thisLogger().warn("Python Interpreter $pythonInterpreter not exists") + return@executeOnPooledThread false + } + + if (!Path(pythonInterpreter).isRegularFile()) { + thisLogger().warn("Python Interpreter $pythonInterpreter is not a regular file") + return@executeOnPooledThread false + } + + thisLogger().info("Use Python Interpreter $pythonInterpreter for project '${this.name}'") + + val res = ExecUtil.execAndGetOutput( + GeneralCommandLine( + pythonInterpreter, "-u", "-c", "import sys; print(sys.version_info[:2]>=(3,8))" + ), timeoutInMilliseconds = 5000 + ) + if (res.exitCode != 0 || res.stdout.trim() != "True") { + thisLogger().warn("Invalid python version") + return@executeOnPooledThread false + } + + val res1 = ExecUtil.execAndGetOutput( + GeneralCommandLine(pythonInterpreter, "-u", RobotCodeHelpers.checkRobotVersion.pathString), + timeoutInMilliseconds = 5000 + ) + if (res1.exitCode != 0 || res1.stdout.trim() != "True") { + thisLogger().warn("Invalid Robot Framework version") + return@executeOnPooledThread false + } + + return@executeOnPooledThread true + + }.get() + + this.putUserData(RobotCodeHelpers.PYTHON_AND_ROBOT_OK_KEY, result) + + return result +} + + +fun Project.buildRobotCodeCommandLine( + args: Array = arrayOf(), + profiles: Array = arrayOf(), + extraArgs: Array = arrayOf(), + format: String = "", + noColor: Boolean = true, + noPager: Boolean = true +): GeneralCommandLine { + if (!this.checkPythonAndRobotVersion()) { + throw IllegalArgumentException("PythonSDK is not defined or robot version is not valid for project ${this.name}") + } + + val pythonInterpreter = this.pythonSdk?.homePath + val commandLine = GeneralCommandLine( + pythonInterpreter, + "-u", + "-X", + "utf8", + RobotCodeHelpers.robotCodePath.pathString, + *(if (format.isNotEmpty()) arrayOf("--format", format) else arrayOf()), + *(if (noColor) arrayOf("--no-color") else arrayOf()), + *(if (noPager) arrayOf("--no-pager") else arrayOf()), + *profiles.flatMap { listOf("--profile", it) }.toTypedArray(), + *extraArgs, + *args + ).withWorkDirectory(this.basePath).withCharset(Charsets.UTF_8) + + return commandLine +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeParserDefinition.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeParserDefinition.kt deleted file mode 100644 index ca1f9066..00000000 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeParserDefinition.kt +++ /dev/null @@ -1,84 +0,0 @@ -package dev.robotcode.robotcode4ij - -import com.intellij.extapi.psi.PsiFileBase -import com.intellij.lang.ASTNode -import com.intellij.lang.ParserDefinition -import com.intellij.lang.PsiBuilder -import com.intellij.lang.PsiParser -import com.intellij.lexer.EmptyLexer -import com.intellij.lexer.Lexer -import com.intellij.openapi.fileTypes.FileType -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.registry.Registry -import com.intellij.psi.FileViewProvider -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.impl.source.tree.CompositePsiElement -import com.intellij.psi.stubs.PsiFileStub -import com.intellij.psi.tree.IElementType -import com.intellij.psi.tree.IFileElementType -import com.intellij.psi.tree.IStubFileElementType -import com.intellij.psi.tree.TokenSet -import org.jetbrains.plugins.textmate.TextMateService -import org.jetbrains.plugins.textmate.language.syntax.lexer.TextMateHighlightingLexer - -val FILE = IStubFileElementType>("RobotSuiteFile", RobotFrameworkLanguage) - -class RobotCodeParserDefinition : ParserDefinition { - - override fun createLexer(project: Project?): Lexer { - val descriptor = TextMateService.getInstance().getLanguageDescriptorByExtension("robot") - if (descriptor != null) { - - return TextMateHighlightingLexer( - descriptor, - Registry.get("textmate.line.highlighting.limit").asInteger() - ) - } - return EmptyLexer() - } - - override fun createParser(project: Project?): PsiParser { - return RobotPsiParser() - } - - override fun getFileNodeType(): IFileElementType { - return FILE - } - - override fun getCommentTokens(): TokenSet { - return TokenSet.EMPTY - } - - override fun getStringLiteralElements(): TokenSet { - return TokenSet.EMPTY - } - - override fun createElement(node: ASTNode): PsiElement { - return RobotPsiElement(node.elementType) - } - - override fun createFile(viewProvider: FileViewProvider): PsiFile { - return RobotFile(viewProvider) - } -} - -class RobotFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, RobotFrameworkLanguage) { - override fun getFileType(): FileType { - return RobotSuiteFileType - } -} - -class RobotPsiParser : PsiParser { - override fun parse(root: IElementType, builder: PsiBuilder): ASTNode { - val mark = builder.mark() - while (!builder.eof()) { - builder.advanceLexer() - } - mark.done(root) - - return builder.treeBuilt - } -} - -class RobotPsiElement(type: IElementType) : CompositePsiElement(type) diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodePostStartupActivity.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodePostStartupActivity.kt index 8ab53c09..062d22d9 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodePostStartupActivity.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodePostStartupActivity.kt @@ -7,19 +7,22 @@ import com.intellij.platform.backend.workspace.workspaceModel import com.intellij.platform.workspace.jps.entities.ModuleEntity import com.intellij.platform.workspace.storage.EntityChange import dev.robotcode.robotcode4ij.lsp.langServerManager +import dev.robotcode.robotcode4ij.testing.testManger import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach class RobotCodePostStartupActivity : ProjectActivity { override suspend fun execute(project: Project) { - project.messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, RobotCodeVirtualFileListener(project)) - project.langServerManager.start() + project.testManger.refresh() + project.messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, RobotCodeVirtualFileListener(project)) project.workspaceModel.eventLog.onEach { val moduleChanges = it.getChanges(ModuleEntity::class.java) if (moduleChanges.filterIsInstance>().isNotEmpty()) { + project.checkPythonAndRobotVersion(true) project.langServerManager.restart() + project.testManger.refresh() } }.collect() } diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeRunConfiguration.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeRunConfiguration.kt deleted file mode 100644 index 81b1417f..00000000 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeRunConfiguration.kt +++ /dev/null @@ -1,12 +0,0 @@ -package dev.robotcode.robotcode4ij - - -import com.intellij.execution.configurations.ConfigurationTypeBase - - -class RobotCodeRunConfiguration : - ConfigurationTypeBase("robotcode", "RobotCode", "Run Robot Framework Tests", RobotIcons.RobotCode) { - init { - addFactory(RobotCodeRunConfigurationFactory(this)) - } -} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeTextMateBundleProvider.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeTextMateBundleProvider.kt deleted file mode 100644 index 8cade471..00000000 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeTextMateBundleProvider.kt +++ /dev/null @@ -1,17 +0,0 @@ -package dev.robotcode.robotcode4ij - - -import org.jetbrains.plugins.textmate.api.TextMateBundleProvider - -private val robotCodeBundle = getBundles() - -private fun getBundles(): List { - return listOf(TextMateBundleProvider.PluginBundle("robotcode", BundledHelpers.basePath)) -} - -class RobotCodeTextMateBundleProvider : TextMateBundleProvider { - - override fun getBundles(): List { - return robotCodeBundle - } -} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/TextMateBundleHolder.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/TextMateBundleHolder.kt new file mode 100644 index 00000000..ecc931fa --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/TextMateBundleHolder.kt @@ -0,0 +1,32 @@ +package dev.robotcode.robotcode4ij + +import com.intellij.util.containers.Interner +import org.jetbrains.plugins.textmate.TextMateService +import org.jetbrains.plugins.textmate.language.TextMateLanguageDescriptor +import org.jetbrains.plugins.textmate.language.syntax.TextMateSyntaxTable + + +object TextMateBundleHolder { + private val interner = Interner.createWeakInterner() + + val descriptor: TextMateLanguageDescriptor by lazy { + + val reader = TextMateService.getInstance().readBundle(RobotCodeHelpers.basePath) + ?: throw IllegalStateException("Failed to read robotcode textmate bundle") + + val syntaxTable = TextMateSyntaxTable() + + val grammarIterator = reader.readGrammars().iterator() + while (grammarIterator.hasNext()) { + val grammar = grammarIterator.next() + val rootScopeName = syntaxTable.loadSyntax(grammar.plist.value, interner) ?: continue + if (rootScopeName == "source.robotframework") { + val syntax = syntaxTable.getSyntax(rootScopeName) + return@lazy TextMateLanguageDescriptor(rootScopeName, syntax) + } + } + + throw IllegalStateException("Failed to find robotcode textmate in bundle") + } + +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/actions/RobotCreateFileAction.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/actions/RobotCreateFileAction.kt new file mode 100644 index 00000000..44cb95ce --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/actions/RobotCreateFileAction.kt @@ -0,0 +1,40 @@ +package dev.robotcode.robotcode4ij.actions + +import com.intellij.ide.actions.CreateFileFromTemplateAction +import com.intellij.ide.actions.CreateFileFromTemplateDialog +import com.intellij.ide.fileTemplates.FileTemplateManager +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDirectory +import dev.robotcode.robotcode4ij.RobotIcons +import dev.robotcode.robotcode4ij.RobotResourceFileType +import dev.robotcode.robotcode4ij.RobotSuiteFileType + +class RobotCreateFileAction : CreateFileFromTemplateAction( + "Robot Framework File", "Robot Framework file", + RobotIcons + .Suite +), + DumbAware { + override fun buildDialog(project: Project, directory: PsiDirectory, builder: CreateFileFromTemplateDialog.Builder) { + builder.setTitle("New Robot Framework File") + FileTemplateManager.getInstance(project) + .allTemplates + .forEach { + if (it.extension == RobotSuiteFileType.defaultExtension) { + builder.addKind(it.name, RobotIcons.Suite, it.name) + } else if (it.extension == RobotResourceFileType.defaultExtension) { + builder.addKind(it.name, RobotIcons.Resource, it.name) + } + } + builder + .addKind("Suite file", RobotIcons.Suite, "Robot Suite File") + .addKind("Resource file", RobotIcons.Resource, "Robot Resource File") + + } + + override fun getActionName(directory: PsiDirectory?, newName: String, templateName: String?): String { + return "Create Robot Framework File" + } + +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/editor/RobotCodeBraceMatcher.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/editor/RobotCodeBraceMatcher.kt new file mode 100644 index 00000000..2fcda9a0 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/editor/RobotCodeBraceMatcher.kt @@ -0,0 +1,30 @@ +package dev.robotcode.robotcode4ij.editor + +import com.intellij.lang.BracePair +import com.intellij.lang.PairedBraceMatcher +import com.intellij.psi.PsiFile +import com.intellij.psi.tree.IElementType +import dev.robotcode.robotcode4ij.psi.ENVIRONMENT_VARIABLE_BEGIN +import dev.robotcode.robotcode4ij.psi.ENVIRONMENT_VARIABLE_END +import dev.robotcode.robotcode4ij.psi.VARIABLE_BEGIN +import dev.robotcode.robotcode4ij.psi.VARIABLE_END + +private val PAIRS = arrayOf( + BracePair(VARIABLE_BEGIN, VARIABLE_END, false), + BracePair(ENVIRONMENT_VARIABLE_BEGIN, ENVIRONMENT_VARIABLE_END, false) +) + +class RobotCodeBraceMatcher : PairedBraceMatcher { + + override fun getPairs(): Array { + return PAIRS + } + + override fun isPairedBracesAllowedBeforeType(lbraceType: IElementType, contextType: IElementType?): Boolean { + return true + } + + override fun getCodeConstructStart(file: PsiFile?, openingBraceOffset: Int): Int { + return openingBraceOffset + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/editor/RobotCodeCommenter.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/editor/RobotCodeCommenter.kt new file mode 100644 index 00000000..c57dcc4b --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/editor/RobotCodeCommenter.kt @@ -0,0 +1,25 @@ +package dev.robotcode.robotcode4ij.editor + +import com.intellij.lang.Commenter + +class RobotCodeCommenter : Commenter { + override fun getLineCommentPrefix(): String { + return "#" + } + + override fun getBlockCommentPrefix(): String? { + return null + } + + override fun getBlockCommentSuffix(): String? { + return null + } + + override fun getCommentedBlockCommentPrefix(): String? { + return null + } + + override fun getCommentedBlockCommentSuffix(): String? { + return null + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeConfigurationType.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeConfigurationType.kt new file mode 100644 index 00000000..c74b4f22 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeConfigurationType.kt @@ -0,0 +1,17 @@ +package dev.robotcode.robotcode4ij.execution + +import com.intellij.execution.configurations.ConfigurationTypeBase +import dev.robotcode.robotcode4ij.RobotIcons + +class RobotCodeConfigurationType : ConfigurationTypeBase( + "RobotCodeConfigurationType", + "Robot Framework", + "Run Robot Framework tests", + RobotIcons.RobotCode +) { + val configurationFactory: RobotCodeRunConfigurationFactory = RobotCodeRunConfigurationFactory(this) + + init { + addFactory(configurationFactory) + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeProgramRunner.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeProgramRunner.kt new file mode 100644 index 00000000..411e5e5e --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeProgramRunner.kt @@ -0,0 +1,34 @@ +package dev.robotcode.robotcode4ij.execution + +import com.intellij.execution.configurations.RunProfile +import com.intellij.execution.configurations.RunProfileState +import com.intellij.execution.configurations.RunnerSettings +import com.intellij.execution.executors.DefaultDebugExecutor +import com.intellij.execution.executors.DefaultRunExecutor +import com.intellij.execution.runners.AsyncProgramRunner +import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.execution.runners.showRunContent +import com.intellij.execution.ui.RunContentDescriptor +import com.intellij.openapi.fileEditor.FileDocumentManager +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.resolvedPromise + +class RobotCodeProgramRunner : AsyncProgramRunner() { + override fun getRunnerId(): String { + return "robotCodeProgramRunner" + } + + override fun canRun(executorId: String, profile: RunProfile): Boolean { + return (executorId == DefaultRunExecutor.EXECUTOR_ID || executorId == DefaultDebugExecutor.EXECUTOR_ID) && profile is RobotCodeRunConfiguration + } + + override fun execute(environment: ExecutionEnvironment, state: RunProfileState): Promise { + FileDocumentManager.getInstance().saveAllDocuments() + + return resolvedPromise(doExecute(state, environment)) + } + + private fun doExecute(state: RunProfileState, environment: ExecutionEnvironment): RunContentDescriptor? { + return showRunContent(state.execute(environment.executor, this), environment) + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfiguration.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfiguration.kt new file mode 100644 index 00000000..bbb60a41 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfiguration.kt @@ -0,0 +1,27 @@ +package dev.robotcode.robotcode4ij.execution + +import com.intellij.execution.Executor +import com.intellij.execution.compound.CompoundRunConfigurationSettingsEditor +import com.intellij.execution.configurations.ConfigurationFactory +import com.intellij.execution.configurations.LocatableConfigurationBase +import com.intellij.execution.configurations.RunConfiguration +import com.intellij.execution.configurations.RunProfileState +import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.openapi.options.SettingsEditor +import com.intellij.openapi.project.Project + +class RobotCodeRunConfiguration(project: Project, factory: ConfigurationFactory) : + LocatableConfigurationBase + (project, factory, "Robot Framework") { + override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState { + return RobotCodeRunProfileState(environment) + } + + override fun getConfigurationEditor(): SettingsEditor { + // TODO: Implement configuration editor + return RobotCodeRunConfigurationEditor() + } + + var suite: String = "" + var test: String = "" +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationEditor.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationEditor.kt new file mode 100644 index 00000000..831a6f51 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationEditor.kt @@ -0,0 +1,43 @@ +package dev.robotcode.robotcode4ij.execution + +import com.intellij.execution.configuration.EnvironmentVariablesComponent +import com.intellij.util.ui.ComponentWithEmptyText +import com.intellij.ui.RawCommandLineEditor +import com.intellij.openapi.options.SettingsEditor +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.panel +import javax.swing.JComponent + +class RobotCodeRunConfigurationEditor : SettingsEditor() { + + private val environmentVariablesField = EnvironmentVariablesComponent() + + private val argumentsField = + RawCommandLineEditor().apply { + if (textField is ComponentWithEmptyText) { + (textField as ComponentWithEmptyText).emptyText.text = + "Additional flags, e.g. --skip-cache, or --parallel=2" + } + } + + override fun resetEditorFrom(s: RobotCodeRunConfiguration) { + // TODO("Not yet implemented") + } + + override fun applyEditorTo(s: RobotCodeRunConfiguration) { + // TODO("Not yet implemented") + } + + override fun createEditor(): JComponent { + return panel { + row("&Robot:") { + textField().label("Suite:") + } + row(environmentVariablesField.label) { + cell(environmentVariablesField.component).align(AlignX.FILL) + } + row("A&rguments:") { cell(argumentsField).align(AlignX.FILL) } + } + } + +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeRunConfigurationFactory.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationFactory.kt similarity index 75% rename from intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeRunConfigurationFactory.kt rename to intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationFactory.kt index 853203b6..bb74ab8f 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeRunConfigurationFactory.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationFactory.kt @@ -1,18 +1,19 @@ -package dev.robotcode.robotcode4ij +package dev.robotcode.robotcode4ij.execution import com.intellij.execution.configurations.ConfigurationFactory import com.intellij.execution.configurations.ConfigurationType import com.intellij.execution.configurations.RunConfiguration import com.intellij.openapi.project.Project +import dev.robotcode.robotcode4ij.RobotIcons import javax.swing.Icon class RobotCodeRunConfigurationFactory(type: ConfigurationType) : ConfigurationFactory(type) { override fun createTemplateConfiguration(project: Project): RunConfiguration { - TODO("Not yet implemented") + return RobotCodeRunConfiguration(project, this) } override fun getId(): String { - return "RobotCode[Default]" + return "ROBOT_FRAMEWORK_TEST" } override fun getIcon(): Icon? { diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationProducer.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationProducer.kt new file mode 100644 index 00000000..4d85aabe --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationProducer.kt @@ -0,0 +1,68 @@ +package dev.robotcode.robotcode4ij.execution + +import com.intellij.execution.actions.ConfigurationContext +import com.intellij.execution.actions.LazyRunConfigurationProducer +import com.intellij.execution.configurations.ConfigurationFactory +import com.intellij.execution.configurations.runConfigurationType +import com.intellij.openapi.util.Ref +import com.intellij.psi.PsiElement +import com.intellij.psi.util.elementType +import dev.robotcode.robotcode4ij.psi.FILE +import dev.robotcode.robotcode4ij.psi.RobotSuiteFile +import dev.robotcode.robotcode4ij.psi.TESTCASE_NAME + + +class RobotCodeRunConfigurationProducer : LazyRunConfigurationProducer() { + override fun getConfigurationFactory(): ConfigurationFactory { + return runConfigurationType().configurationFactories.first() + } + + override fun setupConfigurationFromContext( + configuration: RobotCodeRunConfiguration, + context: ConfigurationContext, + sourceElement: Ref + ): Boolean { + // TODO + val psiElement = sourceElement.get() + val psiFile = psiElement.containingFile as? RobotSuiteFile ?: return false + val virtualFile = psiFile.virtualFile ?: return false + + when (psiElement.elementType) { + TESTCASE_NAME -> { + configuration.name = psiElement.text + configuration.suite = virtualFile.url + configuration.test = psiElement.text + return true + } + + FILE -> { + configuration.name = virtualFile.presentableName + configuration.suite = virtualFile.url + return true + } + + else -> return false + } + } + + override fun isConfigurationFromContext( + configuration: RobotCodeRunConfiguration, + context: ConfigurationContext + ): Boolean { + val psiElement = context.psiLocation + val psiFile = psiElement?.containingFile as? RobotSuiteFile ?: return false + val virtualFile = psiFile.virtualFile ?: return false + + return when (psiElement.elementType) { + TESTCASE_NAME -> { + configuration.suite == virtualFile.url && configuration.test == psiElement.text + } + + FILE -> { + configuration.suite == virtualFile.url + } + + else -> false + } + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunLineMarkerContributor.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunLineMarkerContributor.kt new file mode 100644 index 00000000..a6c1e101 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunLineMarkerContributor.kt @@ -0,0 +1,16 @@ +package dev.robotcode.robotcode4ij.execution + +import com.intellij.execution.lineMarker.RunLineMarkerContributor +import com.intellij.icons.AllIcons +import com.intellij.psi.PsiElement +import com.intellij.psi.util.elementType +import dev.robotcode.robotcode4ij.psi.FILE +import dev.robotcode.robotcode4ij.psi.TESTCASE_NAME + +class RobotCodeRunLineMarkerContributor : RunLineMarkerContributor() { + override fun getInfo(element: PsiElement): Info? { + if (element.elementType != TESTCASE_NAME && element.elementType != FILE) return null + + return withExecutorActions(AllIcons.RunConfigurations.TestState.Run) + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunProfileState.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunProfileState.kt new file mode 100644 index 00000000..e770e474 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunProfileState.kt @@ -0,0 +1,24 @@ +package dev.robotcode.robotcode4ij.execution + +import com.intellij.execution.configurations.CommandLineState +import com.intellij.execution.process.KillableProcessHandler +import com.intellij.execution.process.ProcessHandler +import com.intellij.execution.process.ProcessTerminatedListener +import com.intellij.execution.runners.ExecutionEnvironment +import dev.robotcode.robotcode4ij.buildRobotCodeCommandLine + +class RobotCodeRunProfileState(environment: ExecutionEnvironment) : CommandLineState(environment) { + override fun startProcess(): ProcessHandler { + val project = environment.project + val profile = environment.runProfile as? RobotCodeRunConfiguration + // TODO: Add support for configurable paths + val defaultPaths = arrayOf("--default-path", ".") + + val commandLine = project.buildRobotCodeCommandLine(arrayOf(*defaultPaths, "run")) + + val handler = KillableProcessHandler(commandLine) + ProcessTerminatedListener.attach(handler) + return handler + } + +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotColors.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/highlighting/Colors.kt similarity index 69% rename from intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotColors.kt rename to intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/highlighting/Colors.kt index 24936815..739fcef4 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotColors.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/highlighting/Colors.kt @@ -1,10 +1,10 @@ -package dev.robotcode.robotcode4ij +package dev.robotcode.robotcode4ij.highlighting import com.intellij.openapi.editor.DefaultLanguageHighlighterColors import com.intellij.openapi.editor.colors.TextAttributesKey import com.intellij.openapi.editor.colors.TextAttributesKey.createTextAttributesKey -object RobotColors { +object Colors { val HEADER: TextAttributesKey = createTextAttributesKey("ROBOTFRAMEWORK_HEADER", DefaultLanguageHighlighterColors.KEYWORD) @@ -24,9 +24,6 @@ object RobotColors { val CONTROL_FLOW: TextAttributesKey = createTextAttributesKey("ROBOTFRAMEWORK_CONTROL_FLOW", DefaultLanguageHighlighterColors.KEYWORD) - val EMBEDDED_ARGUMENT: TextAttributesKey = - createTextAttributesKey("ROBOTFRAMEWORK_EMBEDDED_ARGUMENT", DefaultLanguageHighlighterColors.STRING) - val VARIABLE: TextAttributesKey = createTextAttributesKey("ROBOTFRAMEWORK_VARIABLE", DefaultLanguageHighlighterColors.GLOBAL_VARIABLE) val VARIABLE_EXPRESSION: TextAttributesKey = @@ -39,5 +36,24 @@ object RobotColors { val NAMESPACE: TextAttributesKey = createTextAttributesKey("ROBOTFRAMEWORK_NAMESPACE", DefaultLanguageHighlighterColors.CLASS_REFERENCE) + val ARGUMENT: TextAttributesKey = + createTextAttributesKey("ROBOTFRAMEWORK_ARGUMENT", DefaultLanguageHighlighterColors.STRING) + val EMBEDDED_ARGUMENT: TextAttributesKey = + createTextAttributesKey("ROBOTFRAMEWORK_EMBEDDED_ARGUMENT", DefaultLanguageHighlighterColors.STRING) + + val LINE_COMMENT: TextAttributesKey = + createTextAttributesKey("ROBOTFRAMEWORK_LINE_COMMENT", DefaultLanguageHighlighterColors.LINE_COMMENT) + + val BLOCK_COMMENT: TextAttributesKey = + createTextAttributesKey("ROBOTFRAMEWORK_BLOCK_COMMENT", DefaultLanguageHighlighterColors.BLOCK_COMMENT) + + val OPERATOR: TextAttributesKey = + createTextAttributesKey("ROBOTFRAMEWORK_OPERATOR", DefaultLanguageHighlighterColors.OPERATION_SIGN) + + val BDD_PREFIX: TextAttributesKey = + createTextAttributesKey("ROBOTFRAMEWORK_BDD_PREFIX", DefaultLanguageHighlighterColors.METADATA) + + val CONTINUATION: TextAttributesKey = + createTextAttributesKey("ROBOTFRAMEWORK_CONTINUATION", DefaultLanguageHighlighterColors.DOT) } diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/highlighting/RobotCodeLexer.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/highlighting/RobotCodeLexer.kt new file mode 100644 index 00000000..5ddcd6ae --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/highlighting/RobotCodeLexer.kt @@ -0,0 +1,164 @@ +package dev.robotcode.robotcode4ij.highlighting + +import com.intellij.lexer.LexerBase +import com.intellij.openapi.util.registry.Registry +import com.intellij.psi.TokenType +import com.intellij.psi.tree.IElementType +import dev.robotcode.robotcode4ij.TextMateBundleHolder +import dev.robotcode.robotcode4ij.psi.ARGUMENT +import dev.robotcode.robotcode4ij.psi.COMMENT_BLOCK +import dev.robotcode.robotcode4ij.psi.COMMENT_LINE +import dev.robotcode.robotcode4ij.psi.CONTINUATION +import dev.robotcode.robotcode4ij.psi.CONTROL_FLOW +import dev.robotcode.robotcode4ij.psi.ENVIRONMENT_VARIABLE_BEGIN +import dev.robotcode.robotcode4ij.psi.ENVIRONMENT_VARIABLE_END +import dev.robotcode.robotcode4ij.psi.EXPRESSION_VARIABLE_BEGIN +import dev.robotcode.robotcode4ij.psi.EXPRESSION_VARIABLE_END +import dev.robotcode.robotcode4ij.psi.HEADER +import dev.robotcode.robotcode4ij.psi.KEYWORD_CALL +import dev.robotcode.robotcode4ij.psi.KEYWORD_NAME +import dev.robotcode.robotcode4ij.psi.OPERATOR +import dev.robotcode.robotcode4ij.psi.RobotTextMateElementType +import dev.robotcode.robotcode4ij.psi.SETTING +import dev.robotcode.robotcode4ij.psi.TESTCASE_NAME +import dev.robotcode.robotcode4ij.psi.VARIABLE +import dev.robotcode.robotcode4ij.psi.VARIABLE_BEGIN +import dev.robotcode.robotcode4ij.psi.VARIABLE_END +import org.jetbrains.plugins.textmate.language.syntax.lexer.TextMateLexer +import org.jetbrains.plugins.textmate.language.syntax.lexer.TextMateScope +import java.util.* +import kotlin.math.min + +class RobotCodeLexer : LexerBase() { + companion object { + val mapping by lazy { + mapOf( + "comment.line.robotframework" to COMMENT_LINE, + "comment.line.rest.robotframework" to COMMENT_LINE, + "comment.block.robotframework" to COMMENT_BLOCK, + + "punctuation.definition.variable.begin.robotframework" to VARIABLE_BEGIN, + "punctuation.definition.variable.end.robotframework" to VARIABLE_END, + "punctuation.definition.envvar.begin.robotframework" to ENVIRONMENT_VARIABLE_BEGIN, + "punctuation.definition.envvar.end.robotframework" to ENVIRONMENT_VARIABLE_END, + "punctuation.definition.expression.begin.robotframework" to EXPRESSION_VARIABLE_BEGIN, + "punctuation.definition.expression.end.robotframework" to EXPRESSION_VARIABLE_END, + + "entity.name.function.testcase.name.robotframework" to TESTCASE_NAME, + "entity.name.function.keyword.name.robotframework" to KEYWORD_NAME, + + "keyword.other.header.robotframework" to HEADER, + "keyword.other.header.settings.robotframework" to HEADER, + "keyword.other.header.variable.robotframework" to HEADER, + "keyword.other.header.testcase.robotframework" to HEADER, + "keyword.other.header.task.robotframework" to HEADER, + "keyword.other.header.keyword.robotframework" to HEADER, + "keyword.other.header.comment.robotframework" to HEADER, + + "keyword.control.settings.robotframework" to SETTING, + "keyword.control.settings.documentation.robotframework" to SETTING, + + "entity.name.function.keyword-call.robotframework" to KEYWORD_CALL, + "keyword.control.flow.robotframework" to CONTROL_FLOW, + + "keyword.other.robotframework" to SETTING, + + "variable.name.readwrite.robotframework" to VARIABLE, + "keyword.operator.robotframework" to OPERATOR, + + "constant.character.robotframework" to ARGUMENT, + "string.unquoted.argument.robotframework" to ARGUMENT, + + "keyword.operator.continue.robotframework" to CONTINUATION, + + "punctuation.definition.variable.python.begin.robotframework" to VARIABLE_BEGIN, + ) + } + } + + + private val myLexer = + TextMateLexer( + TextMateBundleHolder.descriptor, Registry.get("textmate.line.highlighting.limit").asInteger(), + true + ) + private var currentLineTokens = LinkedList() + private lateinit var buffer: CharSequence + private var endOffset = 0 + private var currentOffset = 0 + private var tokenType: IElementType? = null + private var tokenStart = 0 + private var tokenEnd = 0 + private var restartable = false + + override fun start(buffer: CharSequence, startOffset: Int, endOffset: Int, initialState: Int) { + this.buffer = buffer + this.endOffset = endOffset + this.currentOffset = startOffset + this.endOffset = endOffset + this.currentLineTokens.clear() + this.restartable = initialState == 0 + myLexer.init(buffer, startOffset) + this.advance() + } + + override fun getState(): Int { + return if (restartable) 0 else 1 + } + + override fun getTokenType(): IElementType? { + return tokenType + } + + override fun getTokenStart(): Int { + return tokenStart + } + + override fun getTokenEnd(): Int { + return tokenEnd + } + + override fun advance() { + if (this.currentOffset >= this.endOffset) { + this.updateState(null as TextMateLexer.Token?, this.endOffset) + } else { + if (currentLineTokens.isEmpty()) { + myLexer.advanceLine(this.currentLineTokens) + } + + this.updateState( + currentLineTokens.poll(), + myLexer.currentOffset + ) + } + } + + private fun updateState(token: TextMateLexer.Token?, fallbackOffset: Int) { + if (token != null) { + this.tokenType = + (if (token.scope === TextMateScope.WHITESPACE) TokenType.WHITE_SPACE else mapping[token.scope.scopeName] + ?: RobotTextMateElementType.create(token.scope)) + + tokenStart = token.startOffset + tokenEnd = min(token.endOffset.toDouble(), endOffset.toDouble()).toInt() + currentOffset = token.endOffset + restartable = token.restartable + } else { + tokenType = null + tokenStart = fallbackOffset + tokenEnd = fallbackOffset + currentOffset = fallbackOffset + restartable = true + } + } + + override fun getBufferSequence(): CharSequence { + return buffer + } + + override fun getBufferEnd(): Int { + return endOffset + } +} + + diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/highlighting/RobotCodeSyntaxHighlighter.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/highlighting/RobotCodeSyntaxHighlighter.kt new file mode 100644 index 00000000..724d594a --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/highlighting/RobotCodeSyntaxHighlighter.kt @@ -0,0 +1,109 @@ +package dev.robotcode.robotcode4ij.highlighting + +import com.intellij.lexer.Lexer +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.fileTypes.PlainSyntaxHighlighter +import com.intellij.openapi.fileTypes.SyntaxHighlighterBase +import com.intellij.psi.tree.IElementType +import com.intellij.util.containers.ContainerUtil +import dev.robotcode.robotcode4ij.psi.ARGUMENT +import dev.robotcode.robotcode4ij.psi.COMMENT_BLOCK +import dev.robotcode.robotcode4ij.psi.COMMENT_LINE +import dev.robotcode.robotcode4ij.psi.CONTINUATION +import dev.robotcode.robotcode4ij.psi.CONTROL_FLOW +import dev.robotcode.robotcode4ij.psi.ENVIRONMENT_VARIABLE_BEGIN +import dev.robotcode.robotcode4ij.psi.ENVIRONMENT_VARIABLE_END +import dev.robotcode.robotcode4ij.psi.HEADER +import dev.robotcode.robotcode4ij.psi.KEYWORD_CALL +import dev.robotcode.robotcode4ij.psi.KEYWORD_NAME +import dev.robotcode.robotcode4ij.psi.OPERATOR +import dev.robotcode.robotcode4ij.psi.RobotTextMateElementType +import dev.robotcode.robotcode4ij.psi.SETTING +import dev.robotcode.robotcode4ij.psi.TESTCASE_NAME +import dev.robotcode.robotcode4ij.psi.VARIABLE +import dev.robotcode.robotcode4ij.psi.VARIABLE_BEGIN +import dev.robotcode.robotcode4ij.psi.VARIABLE_END +import org.jetbrains.plugins.textmate.TextMateService +import org.jetbrains.plugins.textmate.language.TextMateScopeComparator +import org.jetbrains.plugins.textmate.language.syntax.highlighting.TextMateTheme +import org.jetbrains.plugins.textmate.language.syntax.lexer.TextMateScope +import java.util.function.Function + + +class RobotCodeSyntaxHighlighter : SyntaxHighlighterBase() { + companion object { + val elementTypeMap = mapOf( + COMMENT_LINE to arrayOf(Colors.LINE_COMMENT), + COMMENT_BLOCK to arrayOf(Colors.BLOCK_COMMENT), + VARIABLE_BEGIN to arrayOf(Colors.VARIABLE_BEGIN), + VARIABLE_END to arrayOf(Colors.VARIABLE_END), + ENVIRONMENT_VARIABLE_BEGIN to arrayOf(Colors.VARIABLE_BEGIN), + ENVIRONMENT_VARIABLE_END to arrayOf(Colors.VARIABLE_END), + TESTCASE_NAME to arrayOf(Colors.TESTCASE_NAME), + KEYWORD_NAME to arrayOf(Colors.KEYWORD_NAME), + HEADER to arrayOf(Colors.HEADER), + SETTING to arrayOf(Colors.SETTING), + KEYWORD_CALL to arrayOf(Colors.KEYWORD_CALL), + CONTROL_FLOW to arrayOf(Colors.CONTROL_FLOW), + VARIABLE to arrayOf(Colors.VARIABLE), + OPERATOR to arrayOf(Colors.OPERATOR), + ARGUMENT to arrayOf(Colors.ARGUMENT), + CONTINUATION to arrayOf(Colors.CONTINUATION), + ) + + val PLAIN_SYNTAX_HIGHLIGHTER: PlainSyntaxHighlighter = PlainSyntaxHighlighter() + } + + private val myLexer = RobotCodeLexer() + + override fun getHighlightingLexer(): Lexer { + return myLexer + } + + override fun getTokenHighlights(tokenType: IElementType?): Array { + val result = elementTypeMap[tokenType] + if (result != null) return result + + if (tokenType !is RobotTextMateElementType) return PLAIN_SYNTAX_HIGHLIGHTER.getTokenHighlights(tokenType) + + val service = TextMateService.getInstance() + val customHighlightingColors = service.customHighlightingColors + + val highlightingRules = ContainerUtil.union(customHighlightingColors.keys, TextMateTheme.INSTANCE.rules) + + val textMateScope = trimEmbeddedScope(tokenType) + val selectors: List = ContainerUtil.reverse( + TextMateScopeComparator(textMateScope, Function.identity()) + .sortAndFilter(highlightingRules) + ) + val result1 = ContainerUtil.map2Array( + selectors, + TextAttributesKey::class.java + ) { rule: CharSequence -> + val customTextAttributes = customHighlightingColors[rule] + customTextAttributes?.getTextAttributesKey(TextMateTheme.INSTANCE) + ?: TextMateTheme.INSTANCE.getTextAttributesKey(rule) + } + + return result1 + } + + private fun trimEmbeddedScope(tokenType: RobotTextMateElementType): TextMateScope { + var current: TextMateScope? = tokenType.scope + val trail: MutableList = ArrayList() + while (current != null) { + val scopeName = current.scopeName + if (scopeName != null && scopeName.contains(".embedded.")) { + var result = TextMateScope.EMPTY + for (i in trail.indices.reversed()) { + result = result.add(trail[i]) + } + return result + } + trail.add(scopeName) + current = current.parent + } + return tokenType.scope + } +} + diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/highlighting/RobotCodeSyntaxHighlighterFactory.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/highlighting/RobotCodeSyntaxHighlighterFactory.kt new file mode 100644 index 00000000..fca98b48 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/highlighting/RobotCodeSyntaxHighlighterFactory.kt @@ -0,0 +1,12 @@ +package dev.robotcode.robotcode4ij.highlighting + +import com.intellij.openapi.fileTypes.SyntaxHighlighter +import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile + +class RobotCodeSyntaxHighlighterFactory : SyntaxHighlighterFactory() { + override fun getSyntaxHighlighter(project: Project?, virtualFile: VirtualFile?): SyntaxHighlighter { + return RobotCodeSyntaxHighlighter() + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServer.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServer.kt index 9c5eb541..5a225b9b 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServer.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServer.kt @@ -3,16 +3,13 @@ package dev.robotcode.robotcode4ij.lsp import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.project.Project -import com.jetbrains.python.sdk.pythonSdk import com.redhat.devtools.lsp4ij.server.LanguageServerLogErrorHandler import com.redhat.devtools.lsp4ij.server.OSProcessStreamConnectionProvider -import dev.robotcode.robotcode4ij.BundledHelpers +import dev.robotcode.robotcode4ij.buildRobotCodeCommandLine import java.io.InputStream import java.io.OutputStream import java.net.ServerSocket import java.net.Socket -import java.nio.charset.StandardCharsets -import kotlin.io.path.pathString class RobotCodeLanguageServer(private val project: Project) : OSProcessStreamConnectionProvider(), LanguageServerLogErrorHandler { @@ -28,19 +25,7 @@ class RobotCodeLanguageServer(private val project: Project) : OSProcessStreamCon } private fun buildCommandLine(port: Int = 6610): GeneralCommandLine { - val pythonInterpreter = project.pythonSdk?.homePath - if (pythonInterpreter != null) { - return GeneralCommandLine( - pythonInterpreter, "-u", "-X", "utf8", - //"-m", "robotcode.cli", - BundledHelpers.robotCodePath.pathString, - //"--log", "--log-level", "DEBUG", - // "--debugpy", - // "--debugpy-wait-for-client", - "language-server", "--socket", "$port" - ).withWorkDirectory(project.basePath).withCharset(StandardCharsets.UTF_8) - } - throw IllegalArgumentException("PythonSDK is not defined for project ${project.name}") + return project.buildRobotCodeCommandLine(arrayOf("language-server", "--socket", "$port")) } override fun logError(error: String?) { @@ -48,6 +33,7 @@ class RobotCodeLanguageServer(private val project: Project) : OSProcessStreamCon thisLogger().error(error) } + // TODO: Implement this method // override fun getInitializationOptions(rootUri: VirtualFile?): Any { // return null // } @@ -55,7 +41,7 @@ class RobotCodeLanguageServer(private val project: Project) : OSProcessStreamCon override fun start() { serverSocket = ServerSocket(0) commandLine = buildCommandLine(serverSocket!!.localPort) - thisLogger().info("Start robotcode with command $commandLine") + thisLogger().info("Start robotcode language server with command $commandLine") super.start() clientSocket = serverSocket!!.accept() inputStream = clientSocket!!.getInputStream() diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerFactory.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerFactory.kt index f6c2a3ec..27485649 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerFactory.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerFactory.kt @@ -4,21 +4,25 @@ import com.intellij.openapi.project.Project import com.redhat.devtools.lsp4ij.LanguageServerEnablementSupport import com.redhat.devtools.lsp4ij.LanguageServerFactory import com.redhat.devtools.lsp4ij.client.LanguageClientImpl +import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider -import dev.robotcode.robotcode4ij.lsp.RobotCodeLanguageServerManager.Companion.ENABLED_KEY -import org.eclipse.lsp4j.services.LanguageServer +import dev.robotcode.robotcode4ij.lsp.RobotCodeLanguageServerManager.Companion.LANGUAGE_SERVER_ENABLED_KEY +import dev.robotcode.robotcode4ij.lsp.features.RobotDiagnosticsFeature class RobotCodeLanguageServerFactory : LanguageServerFactory, LanguageServerEnablementSupport { override fun createConnectionProvider(project: Project): StreamConnectionProvider { return RobotCodeLanguageServer(project) } + override fun createClientFeatures(): LSPClientFeatures { + return super.createClientFeatures().setDiagnosticFeature(RobotDiagnosticsFeature()) + } override fun createLanguageClient(project: Project): LanguageClientImpl { return RobotCodeLanguageClient(project) } override fun isEnabled(project: Project): Boolean { - if (project.getUserData(ENABLED_KEY) == true) { + if (project.getUserData(LANGUAGE_SERVER_ENABLED_KEY) == true) { return true } @@ -26,7 +30,7 @@ class RobotCodeLanguageServerFactory : LanguageServerFactory, LanguageServerEnab } override fun setEnabled(enabled: Boolean, project: Project) { - project.putUserData(ENABLED_KEY, enabled) + project.putUserData(LANGUAGE_SERVER_ENABLED_KEY, enabled) } } diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerManager.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerManager.kt index fca0e5b7..e96e08c8 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerManager.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerManager.kt @@ -1,86 +1,32 @@ package dev.robotcode.robotcode4ij.lsp -import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.execution.util.ExecUtil import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key import com.intellij.openapi.util.removeUserData -import com.jetbrains.python.sdk.pythonSdk import com.redhat.devtools.lsp4ij.LanguageServerManager import com.redhat.devtools.lsp4ij.ServerStatus -import dev.robotcode.robotcode4ij.BundledHelpers -import kotlin.io.path.Path -import kotlin.io.path.exists -import kotlin.io.path.isRegularFile -import kotlin.io.path.pathString +import dev.robotcode.robotcode4ij.checkPythonAndRobotVersion @Service(Service.Level.PROJECT) class RobotCodeLanguageServerManager(private val project: Project) { companion object { const val LANGUAGE_SERVER_ID = "RobotCode" - val ENABLED_KEY = Key.create("ROBOTCODE_ENABLED") + val LANGUAGE_SERVER_ENABLED_KEY = Key.create("ROBOTCODE_LANGUAGE_SERVER_ENABLED") } fun tryConfigureProject(): Boolean { - val pythonInterpreter = project.pythonSdk?.homePath + project.removeUserData(LANGUAGE_SERVER_ENABLED_KEY) - project.removeUserData(ENABLED_KEY) + val result = project.checkPythonAndRobotVersion() - val result = ApplicationManager.getApplication().executeOnPooledThread { - checkPythonAndRobot(pythonInterpreter) - }.get() - - project.putUserData(ENABLED_KEY, result) + project.putUserData(LANGUAGE_SERVER_ENABLED_KEY, result) return result } - private fun checkPythonAndRobot(pythonInterpreter: String?): Boolean { - if (pythonInterpreter == null) { - thisLogger().info("No Python Interpreter defined for project '${project.name}'") - return false - } - - if (!Path(pythonInterpreter).exists()) { - thisLogger().warn("Python Interpreter $pythonInterpreter not exists") - return false - } - - if (!Path(pythonInterpreter).isRegularFile()) { - thisLogger().warn("Python Interpreter $pythonInterpreter is not a regular file") - return false - } - - thisLogger().info("Use Python Interpreter $pythonInterpreter for project '${project.name}'") - - val res = ExecUtil.execAndGetOutput( - GeneralCommandLine( - pythonInterpreter, "-u", "-c", - "import sys; print(sys.version_info[:2]>=(3,8))" - ), timeoutInMilliseconds = 5000 - ) - if (res.exitCode != 0 || res.stdout.trim() != "True") { - thisLogger().warn("Invalid python version") - return false - } - - val res1 = ExecUtil.execAndGetOutput( - GeneralCommandLine(pythonInterpreter, "-u", BundledHelpers.checkRobotVersion.pathString), - timeoutInMilliseconds = 5000 - ) - if (res1.exitCode != 0 || res1.stdout.trim() != "True") { - thisLogger().warn("Invalid Robot Framework version") - return false - } - - return true - } - private var lease: Disposable? = null fun start() { diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeSemanticTokensColorsProvider.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeSemanticTokensColorsProvider.kt index 6bbefa81..e679aa73 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeSemanticTokensColorsProvider.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeSemanticTokensColorsProvider.kt @@ -3,54 +3,45 @@ package dev.robotcode.robotcode4ij.lsp import com.intellij.openapi.editor.colors.TextAttributesKey import com.intellij.psi.PsiFile import com.redhat.devtools.lsp4ij.features.semanticTokens.SemanticTokensColorsProvider -import dev.robotcode.robotcode4ij.RobotColors +import dev.robotcode.robotcode4ij.highlighting.Colors +private val mapping by lazy { + mapOf( + "header" to Colors.HEADER, + "headerKeyword" to Colors.HEADER, + "headerComment" to Colors.HEADER, + "headerSettings" to Colors.HEADER, + "headerVariable" to Colors.HEADER, + "headerTestcase" to Colors.HEADER, + "headerTask" to Colors.HEADER, + + "setting" to Colors.SETTING, + "settingImport" to Colors.SETTING_IMPORT, + "controlFlow" to Colors.CONTROL_FLOW, + + "testcaseName" to Colors.TESTCASE_NAME, + "keywordName" to Colors.KEYWORD_NAME, + "keywordCall" to Colors.KEYWORD_CALL, + + "argument" to Colors.ARGUMENT, + "embeddedArgument" to Colors.EMBEDDED_ARGUMENT, + + "variable" to Colors.VARIABLE, + "variableExpression" to Colors.VARIABLE_EXPRESSION, + "variableBegin" to Colors.VARIABLE_BEGIN, + "variableEnd" to Colors.VARIABLE_END, + + "namespace" to Colors.NAMESPACE, + "bddPrefix" to Colors.BDD_PREFIX, + "continuation" to Colors.CONTINUATION + ) +} class RobotCodeSemanticTokensColorsProvider : SemanticTokensColorsProvider { - - override fun getTextAttributesKey( tokenType: String, tokenModifiers: MutableList, file: PsiFile - ): TextAttributesKey? { // TODO implement RobotCodeSemanticTokensColorsProvider - return when (tokenType) { - "header", - "headerKeyword", - "headerComment", - "headerSettings", - "headerVariable", - "headerTestcase", - "headerTask", - -> RobotColors.HEADER - - "setting" -> RobotColors.SETTING - "settingImport" -> RobotColors.SETTING_IMPORT - "controlFlow" -> RobotColors.CONTROL_FLOW - - "testcaseName" -> RobotColors.TESTCASE_NAME - "keywordName" -> RobotColors.KEYWORD_NAME - "keywordCall" -> RobotColors.KEYWORD_CALL - "embeddedArgument" -> RobotColors.EMBEDDED_ARGUMENT - - "variable" -> RobotColors.VARIABLE - "variableExpression" -> RobotColors.VARIABLE_EXPRESSION - "variableBegin" -> RobotColors.VARIABLE_BEGIN - "variableEnd" -> RobotColors.VARIABLE_END - - "namespace" -> RobotColors.NAMESPACE - else -> null - } - - // return when (tokenType) { - // "headerTestcase" -> { - // val myAttrs = DefaultLanguageHighlighterColors.KEYWORD.defaultAttributes.clone(); - // - // myAttrs.effectType = EffectType.LINE_UNDERSCORE; - // myAttrs.effectColor = myAttrs.foregroundColor; - // myAttrs.fontType = Font.ITALIC; - // return TextAttributesKey.createTextAttributesKey( - // "tokenType", myAttrs - // ) - // } + ): TextAttributesKey? { + return mapping[tokenType] } } diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/features/RobotDiagnosticsFeature.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/features/RobotDiagnosticsFeature.kt new file mode 100644 index 00000000..9714068b --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/features/RobotDiagnosticsFeature.kt @@ -0,0 +1,22 @@ +package dev.robotcode.robotcode4ij.lsp.features + +import com.intellij.lang.annotation.HighlightSeverity +import com.redhat.devtools.lsp4ij.client.features.LSPDiagnosticFeature +import org.eclipse.lsp4j.Diagnostic +import org.eclipse.lsp4j.DiagnosticSeverity + +@Suppress("UnstableApiUsage") class RobotDiagnosticsFeature : LSPDiagnosticFeature() { + override fun getHighlightSeverity(diagnostic: Diagnostic): HighlightSeverity? { + return when (diagnostic.severity) { + DiagnosticSeverity.Hint -> { + HighlightSeverity.INFORMATION + } + + DiagnosticSeverity.Information -> { + HighlightSeverity.INFORMATION + } + + else -> super.getHighlightSeverity(diagnostic) + } + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/ElementTypes.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/ElementTypes.kt new file mode 100644 index 00000000..bbf0b32a --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/ElementTypes.kt @@ -0,0 +1,72 @@ +package dev.robotcode.robotcode4ij.psi + +import com.intellij.psi.stubs.PsiFileStub +import com.intellij.psi.tree.IElementType +import com.intellij.psi.tree.IStubFileElementType +import com.intellij.psi.tree.TokenSet +import dev.robotcode.robotcode4ij.RobotFrameworkLanguage +import org.jetbrains.plugins.textmate.language.syntax.lexer.TextMateScope + +val FILE = IStubFileElementType>("RobotFrameworkFile", RobotFrameworkLanguage) + +class IRobotFrameworkElementType(debugName: String) : IElementType(debugName, RobotFrameworkLanguage) + +val HEADER = IRobotFrameworkElementType("HEADER") +val SETTING = IRobotFrameworkElementType("SETTING") + +val TESTCASE_NAME = IRobotFrameworkElementType("TESTCASE_NAME") +val KEYWORD_NAME = IRobotFrameworkElementType("TESTCASE_NAME") + +val COMMENT_LINE = IRobotFrameworkElementType("COMMENT_LINE") +val COMMENT_BLOCK = IRobotFrameworkElementType("COMMENT_BLOCK") + +val ARGUMENT = IRobotFrameworkElementType("ARGUMENT") + +val VARIABLE_BEGIN = IRobotFrameworkElementType("VARIABLE_BEGIN") +val VARIABLE_END = IRobotFrameworkElementType("VARIABLE_END") +val EXPRESSION_VARIABLE_BEGIN = IRobotFrameworkElementType("EXPRESSION_VARIABLE_BEGIN") +val EXPRESSION_VARIABLE_END = IRobotFrameworkElementType("EXPRESSION_VARIABLE_END") +val ENVIRONMENT_VARIABLE_BEGIN = IRobotFrameworkElementType("VARIABLE_BEGIN") +val ENVIRONMENT_VARIABLE_END = IRobotFrameworkElementType("VARIABLE_END") + +val KEYWORD_CALL = IRobotFrameworkElementType("KEYWORD_CALL") +val CONTROL_FLOW = IRobotFrameworkElementType("CONTROL_FLOW") +val VARIABLE = IRobotFrameworkElementType("VARIABLE") + +val OPERATOR = IRobotFrameworkElementType("OPERATOR") +val CONTINUATION = IRobotFrameworkElementType("CONTINUATION") + + +val COMMENT_TOKENS = TokenSet.create(COMMENT_LINE, COMMENT_BLOCK) +val STRING_TOKENS = TokenSet.create(ARGUMENT) + + +class RobotTextMateElementType private constructor ( + val scope: TextMateScope, + debugName: String = "ROBOT_TEXTMATE_ELEMENT_TYPE(${scope.scopeName})", + register: Boolean = false +) : IElementType( + debugName, RobotFrameworkLanguage, register +) { + override fun toString(): String { + return "RobotTextMateElementType($scope)" + } + + override fun hashCode(): Int { + return scope.hashCode() + } + + override fun equals(other: Any?): Boolean { + return other is RobotTextMateElementType && other.scope == scope + } + + companion object { + private val cache = mutableMapOf() + fun create(scope: TextMateScope?): RobotTextMateElementType { + return cache.getOrPut(scope!!) { + RobotTextMateElementType(scope, register = true) + } + } + } +} + diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/Elements.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/Elements.kt new file mode 100644 index 00000000..a81c265f --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/Elements.kt @@ -0,0 +1,25 @@ +package dev.robotcode.robotcode4ij.psi + +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.impl.source.tree.CompositePsiElement +import com.intellij.psi.tree.IElementType + +open class SimpleASTWrapperPsiElement(tcNode: ASTNode) : ASTWrapperPsiElement(tcNode) { + override fun getChildren(): Array { + return EMPTY_ARRAY + } + + override fun getFirstChild(): PsiElement? { + return null + } + + override fun getLastChild(): PsiElement? { + return null + } + + override fun acceptChildren(visitor: PsiElementVisitor) { + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/RobotCodeParserDefinition.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/RobotCodeParserDefinition.kt new file mode 100644 index 00000000..33847551 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/RobotCodeParserDefinition.kt @@ -0,0 +1,58 @@ +package dev.robotcode.robotcode4ij.psi + +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.IFileElementType +import com.intellij.psi.tree.TokenSet +import dev.robotcode.robotcode4ij.RobotResourceFileType +import dev.robotcode.robotcode4ij.RobotSuiteFileType +import dev.robotcode.robotcode4ij.highlighting.RobotCodeLexer + +class RobotCodeParserDefinition : ParserDefinition { + + override fun createLexer(project: Project?): Lexer { + return RobotCodeLexer() + } + + override fun createParser(project: Project?): PsiParser { + return RobotPsiParser() + } + + override fun getFileNodeType(): IFileElementType { + return FILE + } + + override fun getCommentTokens(): TokenSet { + return COMMENT_TOKENS + } + + override fun getStringLiteralElements(): TokenSet { + // return STRING_TOKENS + return TokenSet.EMPTY + } + + override fun createElement(node: ASTNode): PsiElement { + return when (node.elementType) { + is IRobotFrameworkElementType -> SimpleASTWrapperPsiElement(node) + is RobotTextMateElementType -> ASTWrapperPsiElement(node) + + else -> throw IllegalArgumentException("Unknown element type: ${node.elementType}") + } + } + + override fun createFile(viewProvider: FileViewProvider): PsiFile { + return when (viewProvider.fileType) { + RobotSuiteFileType -> RobotSuiteFile(viewProvider) + RobotResourceFileType -> RobotResourceFile(viewProvider) + else -> throw IllegalArgumentException("Invalid file type: ${viewProvider.fileType}") + } + } +} + diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/RobotPsiParser.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/RobotPsiParser.kt new file mode 100644 index 00000000..c446a22c --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/RobotPsiParser.kt @@ -0,0 +1,33 @@ +package dev.robotcode.robotcode4ij.psi + +import com.intellij.lang.ASTNode +import com.intellij.lang.LightPsiParser +import com.intellij.lang.PsiBuilder +import com.intellij.lang.PsiParser +import com.intellij.psi.tree.IElementType + +class RobotPsiParser : PsiParser, LightPsiParser { + override fun parse(root: IElementType, builder: PsiBuilder): ASTNode { + parseLight(root, builder) + return builder.treeBuilt + } + + override fun parseLight(root: IElementType, builder: PsiBuilder) { + val mark = builder.mark() + while (!builder.eof()) { + (builder.tokenType as? IRobotFrameworkElementType)?.let { + val token = builder.mark() + builder.advanceLexer() + token.done(it) + } ?: run { + (builder.tokenType as? RobotTextMateElementType)?.let { + val token = builder.mark() + builder.advanceLexer() + token.done(it) + } ?: builder.advanceLexer() + } + + } + mark.done(root) + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/RobotSuiteFile.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/RobotSuiteFile.kt new file mode 100644 index 00000000..ae363e63 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/RobotSuiteFile.kt @@ -0,0 +1,26 @@ +package dev.robotcode.robotcode4ij.psi + +import com.intellij.extapi.psi.PsiFileBase +import com.intellij.openapi.fileTypes.FileType +import com.intellij.psi.FileViewProvider +import dev.robotcode.robotcode4ij.RobotFrameworkLanguage +import dev.robotcode.robotcode4ij.RobotResourceFileType +import dev.robotcode.robotcode4ij.RobotSuiteFileType + +class RobotSuiteFile(viewProvider: FileViewProvider) : PsiFileBase( + viewProvider, + RobotFrameworkLanguage +) { + override fun getFileType(): FileType { + return RobotSuiteFileType + } +} + +class RobotResourceFile(viewProvider: FileViewProvider) : PsiFileBase( + viewProvider, + RobotFrameworkLanguage +) { + override fun getFileType(): FileType { + return RobotResourceFileType + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/services/MyProjectService.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/services/MyProjectService.kt deleted file mode 100644 index b00d27b7..00000000 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/services/MyProjectService.kt +++ /dev/null @@ -1,15 +0,0 @@ -package dev.robotcode.robotcode4ij.services - -import com.intellij.openapi.components.Service -import com.intellij.openapi.diagnostic.thisLogger -import com.intellij.openapi.project.Project -import dev.robotcode.robotcode4ij.RobotCodeBundle - -@Service(Service.Level.PROJECT) -class MyProjectService(project: Project) { - - init { - thisLogger().info(RobotCodeBundle.message("projectService", project.name)) - thisLogger().warn("Don't forget to remove all non-needed sample code files with their corresponding registration entries in `plugin.xml`.") - } -} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeCodeStyleSettings.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeCodeStyleSettings.kt similarity index 60% rename from intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeCodeStyleSettings.kt rename to intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeCodeStyleSettings.kt index b2256081..f153da1e 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeCodeStyleSettings.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeCodeStyleSettings.kt @@ -1,7 +1,7 @@ -package dev.robotcode.robotcode4ij +package dev.robotcode.robotcode4ij.settings import com.intellij.psi.codeStyle.CodeStyleSettings import com.intellij.psi.codeStyle.CustomCodeStyleSettings class RobotCodeCodeStyleSettings(settings: CodeStyleSettings) : - CustomCodeStyleSettings("GdCodeStyleSettings", settings) + CustomCodeStyleSettings("RobotFrameworkCodeStyleSettings", settings) diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeCodeStyleSettingsProvider.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeCodeStyleSettingsProvider.kt similarity index 81% rename from intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeCodeStyleSettingsProvider.kt rename to intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeCodeStyleSettingsProvider.kt index 4a47fdae..8468dd70 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeCodeStyleSettingsProvider.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeCodeStyleSettingsProvider.kt @@ -1,4 +1,4 @@ -package dev.robotcode.robotcode4ij +package dev.robotcode.robotcode4ij.settings import com.intellij.application.options.CodeStyleAbstractConfigurable import com.intellij.application.options.CodeStyleAbstractPanel @@ -7,6 +7,7 @@ import com.intellij.psi.codeStyle.CodeStyleConfigurable import com.intellij.psi.codeStyle.CodeStyleSettings import com.intellij.psi.codeStyle.CodeStyleSettingsProvider import com.intellij.psi.codeStyle.CustomCodeStyleSettings +import dev.robotcode.robotcode4ij.RobotFrameworkLanguage class RobotCodeCodeStyleSettingsProvider : CodeStyleSettingsProvider() { override fun getConfigurableDisplayName() = "Robot Framework" @@ -21,12 +22,12 @@ class RobotCodeCodeStyleSettingsProvider : CodeStyleSettingsProvider() { ): CodeStyleConfigurable { return object : CodeStyleAbstractConfigurable(settings, modelSettings, this.configurableDisplayName) { override fun createPanel(settings: CodeStyleSettings): CodeStyleAbstractPanel { - return GdCodeStyleMainPanel(currentSettings, settings) + return CodeStyleMainPanel(currentSettings, settings) } } } - private class GdCodeStyleMainPanel(currentSettings: CodeStyleSettings, settings: CodeStyleSettings) : + private class CodeStyleMainPanel(currentSettings: CodeStyleSettings, settings: CodeStyleSettings) : TabbedLanguageCodeStylePanel(RobotFrameworkLanguage, currentSettings, settings) } diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeColorSettingsPage.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeColorSettingsPage.kt index 5c05d057..13ac6561 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeColorSettingsPage.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeColorSettingsPage.kt @@ -2,30 +2,39 @@ package dev.robotcode.robotcode4ij.settings import com.intellij.openapi.editor.colors.TextAttributesKey import com.intellij.openapi.fileTypes.SyntaxHighlighter -import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory import com.intellij.openapi.options.colors.AttributesDescriptor import com.intellij.openapi.options.colors.ColorDescriptor import com.intellij.openapi.options.colors.ColorSettingsPage -import dev.robotcode.robotcode4ij.RobotColors -import dev.robotcode.robotcode4ij.RobotFrameworkLanguage import dev.robotcode.robotcode4ij.RobotIcons +import dev.robotcode.robotcode4ij.highlighting.RobotCodeSyntaxHighlighter +import dev.robotcode.robotcode4ij.highlighting.Colors import javax.swing.Icon class RobotCodeColorSettingsPage : ColorSettingsPage { private val descriptors: Array = arrayOf( - AttributesDescriptor("Header", RobotColors.HEADER), - AttributesDescriptor("Test case name", RobotColors.TESTCASE_NAME), - AttributesDescriptor("Keyword name", RobotColors.KEYWORD_NAME), - AttributesDescriptor("Keyword call", RobotColors.KEYWORD_CALL), - AttributesDescriptor("Setting", RobotColors.SETTING), - AttributesDescriptor("Setting import", RobotColors.SETTING_IMPORT), - AttributesDescriptor("Control flow", RobotColors.CONTROL_FLOW), - AttributesDescriptor("Embedded argument", RobotColors.EMBEDDED_ARGUMENT), - AttributesDescriptor("Variable", RobotColors.VARIABLE), - AttributesDescriptor("Variable expression", RobotColors.VARIABLE_EXPRESSION), - AttributesDescriptor("Variable begin", RobotColors.VARIABLE_BEGIN), - AttributesDescriptor("Variable end", RobotColors.VARIABLE_END), + AttributesDescriptor("Header", Colors.HEADER), + AttributesDescriptor("Test case name", Colors.TESTCASE_NAME), + AttributesDescriptor("Keyword name", Colors.KEYWORD_NAME), + AttributesDescriptor("Keyword call", Colors.KEYWORD_CALL), + AttributesDescriptor("Setting", Colors.SETTING), + AttributesDescriptor("Setting import", Colors.SETTING_IMPORT), + AttributesDescriptor("Control flow", Colors.CONTROL_FLOW), + AttributesDescriptor("Argument", Colors.ARGUMENT), + AttributesDescriptor("Embedded argument", Colors.EMBEDDED_ARGUMENT), + AttributesDescriptor("Variable", Colors.VARIABLE), + AttributesDescriptor("Variable expression", Colors.VARIABLE_EXPRESSION), + AttributesDescriptor("Variable begin", Colors.VARIABLE_BEGIN), + AttributesDescriptor("Variable end", Colors.VARIABLE_END), + + AttributesDescriptor("Line comment", Colors.LINE_COMMENT), + AttributesDescriptor("Block comment", Colors.BLOCK_COMMENT), + + AttributesDescriptor("Operator", Colors.OPERATOR), + AttributesDescriptor("Namespace", Colors.NAMESPACE), + AttributesDescriptor("BDD prefix", Colors.BDD_PREFIX), + + AttributesDescriptor("Continuation", Colors.CONTINUATION), ) override fun getAttributeDescriptors(): Array { @@ -45,21 +54,48 @@ class RobotCodeColorSettingsPage : ColorSettingsPage { } override fun getHighlighter(): SyntaxHighlighter { - return SyntaxHighlighterFactory.getSyntaxHighlighter(RobotFrameworkLanguage, null, null) + return RobotCodeSyntaxHighlighter() } override fun getDemoText(): String { return """ - *** Settings *** - Library SeleniumLibrary + *** Settings *** + Library SeleniumLibrary + + *** Variables *** + ${'$'}{URL} http://example.com # a comment + + *** Test Cases *** + Example Test + Open Application ${'$'}{URL} + Log %{APP_DATA=unknown} + Close Application - *** Variables *** - ${'$'}{URL} http://example.com + BDD Example Test + Given application is ppen + When I enter something into the Search Field + Then Something Should Happen - *** Test Cases *** - Example Test - Open Browser ${'$'}{URL} - Close Browser + Another Test + [Documentation] This is a test + ... with multiple lines + [Arguments] + ... ${'$'}{arg1} + ... ${'$'}{arg2} + + Log ${'$'}{arg1} ${'$'}{arg2} + + *** Keywords *** + Open Application + [Arguments] ${'$'}{url} + Open Browser ${'$'}{url} + + Close Application + Close Browser + + *** Comments *** + this is a comment block + with multiple lines """.trimIndent() } diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeLangCodeStyleSettingsProvider.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeLangCodeStyleSettingsProvider.kt similarity index 62% rename from intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeLangCodeStyleSettingsProvider.kt rename to intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeLangCodeStyleSettingsProvider.kt index 57b2c762..fb008cfe 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/RobotCodeLangCodeStyleSettingsProvider.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeLangCodeStyleSettingsProvider.kt @@ -1,4 +1,4 @@ -package dev.robotcode.robotcode4ij +package dev.robotcode.robotcode4ij.settings import com.intellij.application.options.IndentOptionsEditor import com.intellij.application.options.SmartIndentOptionsEditor @@ -7,12 +7,17 @@ import com.intellij.psi.codeStyle.CodeStyleSettings import com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable import com.intellij.psi.codeStyle.CustomCodeStyleSettings import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider +import dev.robotcode.robotcode4ij.RobotFrameworkLanguage class RobotCodeLangCodeStyleSettingsProvider : LanguageCodeStyleSettingsProvider() { override fun getLanguage(): Language { return RobotFrameworkLanguage } + override fun getFileExt(): String { + return "robot" + } + override fun getIndentOptionsEditor(): IndentOptionsEditor { return SmartIndentOptionsEditor() } @@ -50,15 +55,35 @@ class RobotCodeLangCodeStyleSettingsProvider : LanguageCodeStyleSettingsProvider } override fun getCodeSample(settingsType: SettingsType): String { - return "*** Settings ***\n" + - "Library SeleniumLibrary\n" + - "\n" + - "*** Variables ***\n" + - "${'$'}{BROWSER} Chrome\n" + - "\n" + - "*** Test Cases ***\n" + - "Open Browser\n" + - " Open Browser https://www.google.com \${BROWSER}\n" + - " Close Browser\n" + return """ + *** Settings *** + Library SeleniumLibrary + + *** Variables *** + ${'$'}{URL} http://example.com # a comment + + *** Test Cases *** + Example Test + Open Application ${'$'}{URL} + Log %{APP_DATA:unknown} + Close Application + + BDD Example Test + Given application is ppen + When I enter something into the Search Field + Then Something Should Happen + + *** Keywords *** + Open Application + [Arguments] ${'$'}{url} + Open Browser ${'$'}{url} + + Close Application + Close Browser + + *** Comments *** + this is a comment block + with multiple lines + """.trimIndent() } } diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/testing/RobotCodeTestManager.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/testing/RobotCodeTestManager.kt new file mode 100644 index 00000000..d11d257c --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/testing/RobotCodeTestManager.kt @@ -0,0 +1,135 @@ +package dev.robotcode.robotcode4ij.testing + +import com.intellij.execution.process.CapturingProcessHandler +import com.intellij.execution.process.CapturingProcessRunner +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import dev.robotcode.robotcode4ij.buildRobotCodeCommandLine +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement + +@Serializable data class Position(val line: UInt, val character: UInt) + +@Serializable data class Range(val start: Position, val end: Position) + +@Serializable data class RobotCodeTestItem( + val type: String, + val id: String, + val name: String, + val longname: String, + val description: String? = null, + val uri: String? = null, + val relSource: String? = null, + val source: String? = null, + val needsParseInclude: Boolean? = null, + val children: Array? = null, + val range: Range? = null, + val error: String? = null, + val tags: Array? = null, + val rpa: Boolean? = null +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RobotCodeTestItem + + if (needsParseInclude != other.needsParseInclude) return false + if (type != other.type) return false + if (id != other.id) return false + if (name != other.name) return false + if (longname != other.longname) return false + if (description != other.description) return false + if (uri != other.uri) return false + if (relSource != other.relSource) return false + if (children != null) { + if (other.children == null) return false + if (!children.contentEquals(other.children)) return false + } else if (other.children != null) return false + if (range != other.range) return false + if (error != other.error) return false + if (tags != null) { + if (other.tags == null) return false + if (!tags.contentEquals(other.tags)) return false + } else if (other.tags != null) return false + + return true + } + + override fun hashCode(): Int { + var result = needsParseInclude?.hashCode() ?: 0 + result = 31 * result + type.hashCode() + result = 31 * result + id.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + longname.hashCode() + result = 31 * result + description.hashCode() + result = 31 * result + (uri?.hashCode() ?: 0) + result = 31 * result + (relSource?.hashCode() ?: 0) + result = 31 * result + (children?.contentHashCode() ?: 0) + result = 31 * result + (range?.hashCode() ?: 0) + result = 31 * result + (error?.hashCode() ?: 0) + result = 31 * result + (tags?.contentHashCode() ?: 0) + return result + } +} + +@Serializable data class RobotCodeDiscoverResult( + val items: Array? = null, + val diagnostics: Map? = null // TODO val diagnostics: { [Key: string]: Diagnostic[] }; +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RobotCodeDiscoverResult + + if (items != null) { + if (other.items == null) return false + if (!items.contentEquals(other.items)) return false + } else if (other.items != null) return false + if (diagnostics != other.diagnostics) return false + + return true + } + + override fun hashCode(): Int { + var result = items?.contentHashCode() ?: 0 + result = 31 * result + (diagnostics?.hashCode() ?: 0) + return result + } +} + +@Service(Service.Level.PROJECT) class RobotCodeTestManager(private val project: Project) { + var testItems: Array = arrayOf() + private set + + fun refresh() { + // TODO: Add support for configurable paths + val defaultPaths = arrayOf("--default-path", ".") + + try { + val cmdLine = project.buildRobotCodeCommandLine( + arrayOf(*defaultPaths, "discover", "all"), format = "json" + ) + + testItems = ApplicationManager.getApplication().executeOnPooledThread { + val result = CapturingProcessRunner(CapturingProcessHandler(cmdLine)).runProcess() + if (result.exitCode != 0) { + throw RuntimeException("Failed to discover test items: ${result.stderr}") + } + Json.decodeFromString(result.stdout) + }.get().items ?: arrayOf() + } catch (e: Exception) { + thisLogger().warn("Failed to discover test items", e) + } + } +} + +val Project.testManger: RobotCodeTestManager + get() { + return this.service() + } diff --git a/intellij-client/src/main/resources/META-INF/plugin.xml b/intellij-client/src/main/resources/META-INF/plugin.xml index 79cab987..ffffef10 100644 --- a/intellij-client/src/main/resources/META-INF/plugin.xml +++ b/intellij-client/src/main/resources/META-INF/plugin.xml @@ -1,8 +1,9 @@ - + dev.robotcode.robotcode4ij RobotCode - Robot Framework Support - robotcode.dev + robotcode.dev + Programming Languages com.intellij.modules.platform com.intellij.modules.python @@ -12,13 +13,11 @@ com.github.jnhyperion.hyperrobotframeworkplugin com.millennialmedia.intellibot com.github.nghiatm.robotframeworkplugin + net.modulo3.robot messages.RobotCode - - @@ -28,20 +27,16 @@ language="robotframework" extensions="resource"/> - - - - + implementationClass="dev.robotcode.robotcode4ij.psi.RobotCodeParserDefinition"/> + + implementationClass="dev.robotcode.robotcode4ij.highlighting.RobotCodeSyntaxHighlighterFactory"/> + + + - - - - - + + + + implementation="dev.robotcode.robotcode4ij.settings.RobotCodeLangCodeStyleSettingsProvider"/> + + + + + + + + + + + + + + + +