├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .run ├── Run IDE with Plugin.run.xml ├── Run Plugin Tests.run.xml └── Run Plugin Verification.run.xml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── qodana.yaml ├── settings.gradle.kts └── src ├── main ├── kotlin │ └── com │ │ └── dprint │ │ ├── actions │ │ ├── ClearCacheAction.kt │ │ ├── ReformatAction.kt │ │ └── RestartAction.kt │ │ ├── config │ │ ├── DprintConfigurable.kt │ │ ├── ProjectConfiguration.kt │ │ └── UserConfiguration.kt │ │ ├── formatter │ │ ├── DprintDocumentMerger.kt │ │ ├── DprintExternalFormatter.kt │ │ └── DprintFormattingTask.kt │ │ ├── i18n │ │ └── DprintBundle.kt │ │ ├── listeners │ │ ├── ConfigChangedAction.kt │ │ ├── FileOpenedListener.kt │ │ ├── OnSaveAction.kt │ │ └── ProjectStartupListener.kt │ │ ├── messages │ │ ├── DprintAction.kt │ │ └── DprintMessage.kt │ │ ├── services │ │ ├── FormatterService.kt │ │ └── editorservice │ │ │ ├── EditorServiceManager.kt │ │ │ ├── EditorServiceTaskQueue.kt │ │ │ ├── FormatResult.kt │ │ │ ├── IEditorService.kt │ │ │ ├── exceptions │ │ │ ├── HandlerNotImplementedException.kt │ │ │ ├── ProcessUnavailableException.kt │ │ │ └── UnsupportedMessagePartException.kt │ │ │ ├── process │ │ │ ├── EditorProcess.kt │ │ │ └── StdErrListener.kt │ │ │ ├── v4 │ │ │ └── EditorServiceV4.kt │ │ │ └── v5 │ │ │ ├── EditorServiceV5.kt │ │ │ ├── IncomingMessage.kt │ │ │ ├── MessageType.kt │ │ │ ├── OutgoingMessage.kt │ │ │ ├── PendingMessages.kt │ │ │ └── StdoutListener.kt │ │ ├── toolwindow │ │ ├── Console.kt │ │ └── ConsoleToolWindowFactory.kt │ │ └── utils │ │ ├── FileUtils.kt │ │ └── LogUtils.kt └── resources │ ├── META-INF │ └── plugin.xml │ └── messages │ └── Bundle.properties └── test └── kotlin └── com └── dprint ├── formatter └── DprintFormattingTaskTest.kt └── services ├── FormatterServiceImplTest.kt └── editorservice ├── process └── EditorProcessTest.kt └── v5 ├── EditorServiceV5ImplTest.kt ├── IncomingMessageTest.kt └── OutgoingMessageTest.kt /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | max_line_length = 120 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration: 2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | # Maintain dependencies for Gradle dependencies 7 | - package-ecosystem: "gradle" 8 | directory: "/" 9 | target-branch: "next" 10 | schedule: 11 | interval: "daily" 12 | # Maintain dependencies for GitHub Actions 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | target-branch: "next" 16 | schedule: 17 | interval: "daily" 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow is created for testing and preparing the plugin release in the following steps: 2 | # - Validate Gradle Wrapper. 3 | # - Run 'test' and 'verifyPlugin' tasks. 4 | # - Run the 'buildPlugin' task and prepare artifact for further tests. 5 | # - Run the 'runPluginVerifier' task. 6 | # - Create a draft release. 7 | # 8 | # The workflow is triggered on push and pull_request events. 9 | # 10 | # GitHub Actions reference: https://help.github.com/en/actions 11 | # 12 | ## JBIJPPTPL 13 | 14 | name: Build 15 | on: 16 | # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g., for dependabot pull requests) 17 | push: 18 | branches: [ main ] 19 | # Trigger the workflow on any pull request 20 | pull_request: 21 | 22 | # Allow manually triggering 23 | workflow_dispatch: 24 | 25 | 26 | concurrency: 27 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 28 | cancel-in-progress: true 29 | 30 | jobs: 31 | 32 | # Prepare environment and build the plugin 33 | build: 34 | name: Build 35 | runs-on: ubuntu-latest 36 | outputs: 37 | version: ${{ steps.properties.outputs.version }} 38 | changelog: ${{ steps.properties.outputs.changelog }} 39 | pluginVerifierHomeDir: ${{ steps.properties.outputs.pluginVerifierHomeDir }} 40 | steps: 41 | 42 | # Check out the current repository 43 | - name: Fetch Sources 44 | uses: actions/checkout@v4 45 | 46 | # Validate wrapper 47 | - name: Gradle Wrapper Validation 48 | uses: gradle/actions/wrapper-validation@v4 49 | 50 | # Set up Java environment for the next steps 51 | - name: Setup Java 52 | uses: actions/setup-java@v4 53 | with: 54 | distribution: zulu 55 | java-version: 17 56 | 57 | # Setup Gradle 58 | - name: Setup Gradle 59 | uses: gradle/actions/setup-gradle@v4 60 | 61 | # Set environment variables 62 | - name: Export Properties 63 | id: properties 64 | shell: bash 65 | run: | 66 | PROPERTIES="$(./gradlew properties --console=plain -q)" 67 | VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" 68 | CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" 69 | 70 | echo "version=$VERSION" >> $GITHUB_OUTPUT 71 | echo "pluginVerifierHomeDir=~/.pluginVerifier" >> $GITHUB_OUTPUT 72 | echo "changelog<> $GITHUB_OUTPUT 73 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 74 | echo "EOF" >> $GITHUB_OUTPUT 75 | 76 | # Build plugin 77 | - name: Build plugin 78 | run: ./gradlew buildPlugin 79 | 80 | # Prepare plugin archive content for creating artifact 81 | - name: Prepare Plugin Artifact 82 | id: artifact 83 | shell: bash 84 | run: | 85 | cd ${{ github.workspace }}/build/distributions 86 | FILENAME=`ls *.zip` 87 | unzip "$FILENAME" -d content 88 | 89 | echo "filename=${FILENAME:0:-4}" >> $GITHUB_OUTPUT 90 | 91 | # Store already-built plugin as an artifact for downloading 92 | - name: Upload artifact 93 | uses: actions/upload-artifact@v4 94 | with: 95 | name: ${{ steps.artifact.outputs.filename }} 96 | path: ./build/distributions/content/*/* 97 | 98 | # Run tests and upload a code coverage report 99 | test: 100 | name: Test 101 | needs: [ build ] 102 | runs-on: ubuntu-latest 103 | steps: 104 | 105 | # Check out the current repository 106 | - name: Fetch Sources 107 | uses: actions/checkout@v4 108 | 109 | # Set up Java environment for the next steps 110 | - name: Setup Java 111 | uses: actions/setup-java@v4 112 | with: 113 | distribution: zulu 114 | java-version: 17 115 | 116 | # Setup Gradle 117 | - name: Setup Gradle 118 | uses: gradle/actions/setup-gradle@v4 119 | 120 | # Run tests 121 | - name: Run Tests 122 | run: ./gradlew check 123 | 124 | # Collect Tests Result of failed tests 125 | - name: Collect Tests Result 126 | if: ${{ failure() }} 127 | uses: actions/upload-artifact@v4 128 | with: 129 | name: tests-result 130 | path: ${{ github.workspace }}/build/reports/tests 131 | 132 | # Run plugin structure verification along with IntelliJ Plugin Verifier 133 | verify: 134 | name: Verify plugin 135 | needs: [ build ] 136 | runs-on: ubuntu-latest 137 | steps: 138 | 139 | # Free GitHub Actions Environment Disk Space 140 | - name: Maximize Build Space 141 | uses: jlumbroso/free-disk-space@main 142 | with: 143 | tool-cache: false 144 | large-packages: false 145 | 146 | # Check out the current repository 147 | - name: Fetch Sources 148 | uses: actions/checkout@v4 149 | 150 | # Set up Java environment for the next steps 151 | - name: Setup Java 152 | uses: actions/setup-java@v4 153 | with: 154 | distribution: zulu 155 | java-version: 17 156 | 157 | # Setup Gradle 158 | - name: Setup Gradle 159 | uses: gradle/actions/setup-gradle@v4 160 | 161 | # Cache Plugin Verifier IDEs 162 | - name: Setup Plugin Verifier IDEs Cache 163 | uses: actions/cache@v4 164 | with: 165 | path: ${{ needs.build.outputs.pluginVerifierHomeDir }}/ides 166 | key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }} 167 | 168 | # Run Verify Plugin task and IntelliJ Plugin Verifier tool 169 | - name: Run Plugin Verification tasks 170 | run: ./gradlew verifyPlugin -Dplugin.verifier.home.dir=${{ needs.build.outputs.pluginVerifierHomeDir }} 171 | 172 | # Collect Plugin Verifier Result 173 | - name: Collect Plugin Verifier Result 174 | if: ${{ always() }} 175 | uses: actions/upload-artifact@v4 176 | with: 177 | name: pluginVerifier-result 178 | path: ${{ github.workspace }}/build/reports/pluginVerifier 179 | 180 | # Prepare a draft release for GitHub Releases page for the manual verification 181 | # If accepted and published, release workflow would be triggered 182 | releaseDraft: 183 | name: Release draft 184 | if: github.event_name != 'pull_request' 185 | needs: [ build, test, verify ] 186 | runs-on: ubuntu-latest 187 | permissions: 188 | contents: write 189 | steps: 190 | 191 | # Check out the current repository 192 | - name: Fetch Sources 193 | uses: actions/checkout@v4 194 | 195 | # Remove old release drafts by using the curl request for the available releases with a draft flag 196 | - name: Remove Old Release Drafts 197 | env: 198 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 199 | run: | 200 | gh api repos/{owner}/{repo}/releases \ 201 | --jq '.[] | select(.draft == true) | .id' \ 202 | | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} 203 | 204 | # Create a new release draft which is not publicly visible and requires manual acceptance 205 | - name: Create Release Draft 206 | env: 207 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 208 | run: | 209 | gh release create "v${{ needs.build.outputs.version }}" \ 210 | --draft \ 211 | --title "v${{ needs.build.outputs.version }}" \ 212 | --notes "$(cat << 'EOM' 213 | ${{ needs.build.outputs.changelog }} 214 | EOM 215 | )" 216 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared with the Build workflow. 2 | # Running the publishPlugin task requires all following secrets to be provided: PUBLISH_TOKEN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD, CERTIFICATE_CHAIN. 3 | # See https://plugins.jetbrains.com/docs/intellij/plugin-signing.html for more information. 4 | 5 | name: Release 6 | on: 7 | release: 8 | types: [ prereleased, released ] 9 | 10 | jobs: 11 | 12 | # Prepare and publish the plugin to JetBrains Marketplace repository 13 | release: 14 | name: Publish Plugin 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | steps: 20 | - name: Free up disk space 21 | run: | 22 | echo "Freeing disk space before Gradle setup..." 23 | sudo rm -rf /usr/share/dotnet /opt/ghc /opt/hostedtoolcache /usr/local/lib/android 24 | df -h 25 | 26 | # Check out the current repository 27 | - name: Fetch Sources 28 | uses: actions/checkout@v4 29 | with: 30 | ref: ${{ github.event.release.tag_name }} 31 | 32 | # Set up Java environment for the next steps 33 | - name: Setup Java 34 | uses: actions/setup-java@v4 35 | with: 36 | distribution: zulu 37 | java-version: 17 38 | 39 | # Setup Gradle 40 | - name: Setup Gradle 41 | uses: gradle/actions/setup-gradle@v4 42 | with: 43 | cache-disabled: true 44 | 45 | # Set environment variables 46 | - name: Export Properties 47 | id: properties 48 | shell: bash 49 | run: | 50 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' 51 | ${{ github.event.release.body }} 52 | EOM 53 | )" 54 | echo "changelog<> $GITHUB_OUTPUT 55 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 56 | echo "EOF" >> $GITHUB_OUTPUT 57 | 58 | # Update the Unreleased section with the current release note 59 | - name: Patch Changelog 60 | if: ${{ steps.properties.outputs.changelog != '' }} 61 | env: 62 | CHANGELOG: ${{ steps.properties.outputs.changelog }} 63 | run: | 64 | ./gradlew patchChangelog --release-note="$CHANGELOG" 65 | 66 | # Publish the plugin to JetBrains Marketplace 67 | - name: Publish Plugin 68 | env: 69 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 70 | run: ./gradlew publishPlugin 71 | 72 | # Upload artifact as a release asset 73 | - name: Upload Release Asset 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* 77 | 78 | # Create a pull request 79 | - name: Create Pull Request 80 | if: ${{ steps.properties.outputs.changelog != '' }} 81 | env: 82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 83 | run: | 84 | VERSION="${{ github.event.release.tag_name }}" 85 | BRANCH="changelog-update-$VERSION" 86 | LABEL="release changelog" 87 | 88 | git config user.email "action@github.com" 89 | git config user.name "GitHub Action" 90 | 91 | git checkout -b $BRANCH 92 | git commit -am "Changelog update - $VERSION" 93 | git push --set-upstream origin $BRANCH 94 | gh label create "$LABEL" \ 95 | --description "Pull requests with release changelog update" \ 96 | --force \ 97 | || true 98 | 99 | gh pr create \ 100 | --title "Changelog update - \`$VERSION\`" \ 101 | --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ 102 | --label "$LABEL" \ 103 | --head $BRANCH 104 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .intellijPlatform 2 | .gradle 3 | .kotlin 4 | .idea 5 | build 6 | **/.DS_Store -------------------------------------------------------------------------------- /.run/Run IDE with Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.run/Run Plugin Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.run/Run Plugin Verification.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 25 | 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # dprint Changelog 4 | 5 | ## [Unreleased] 6 | 7 | ## 0.8.3 - 2025-06-07 8 | 9 | - While range formatting is not currently available, allow format fragment flows to come through so logging occurs. 10 | - Publish formatting stats through the DprintAction message bus topic. 11 | 12 | ## 0.8.2 - 2025-01-14 13 | 14 | - Update plugin name for marketplace verification 15 | 16 | ## 0.8.1 - 2024-12-06 17 | 18 | - Reinstate require restart, even though the plugin doesn't technically need it, it avoids issues that a restart would 19 | fix. 20 | 21 | ## 0.8.0 - 2024-12-05 22 | 23 | - Removed json validation of config file 24 | - Set minimum versions to 2024 25 | - Updated to newer build tooling 26 | 27 | ## 0.7.0 - 2024-07-20 28 | 29 | - Fixed an issue where initialisation could get into an infinite loop. 30 | - Default to the IntelliJ formatter when the canFormat cache is cold for a file. 31 | - Stop self-healing restarts and show error baloons every time a startup fails. 32 | 33 | ## 0.6.0 - 2024-02-08 34 | 35 | - Fixed issue where on save actions were running twice per save. 36 | - Enforce that only a initialisation or format can be queued to run at a time. 37 | - Added checkbox to `Tools -> Actions on Save` menu. 38 | - Added notification when the underlying dprint daemon cannot initialise. 39 | - Fixed an issue where an error could be thrown when closing or changing projects due to a race condition. 40 | - Added timeout configuration for the initialisation of the dprint daemon and for commands. 41 | 42 | ## 0.5.0 - 2023-12-22 43 | 44 | - Add check for dead processes to warn users that the dprint daemon is not responding 45 | - Increase severity of logging in the event processes die or errors are seen in process communication 46 | - This may be a little noisy, and if so disabling the plugin is recommended unless the underlying issue with the 47 | process can be fixed 48 | - For intermittent or one off errors, just restart the dprint plugin via the `Restart dprint` action 49 | - Upgrade dependencies 50 | - Attempt to fix changelog update on publish 51 | 52 | ## 0.4.4 - 2023-11-23 53 | 54 | - Fixed issue for plugins that require the underlying process to be running in the working projects git repository. 55 | 56 | ## 0.4.3 - 2023-10-11 57 | 58 | - Update to latest dependencies 59 | 60 | ## 0.4.2 61 | 62 | - Fix issue with run on save config not saving 63 | 64 | ## 0.4.1 65 | 66 | - Fix null pointer issue in external formatter 67 | 68 | ## 0.4.0 69 | 70 | - Run dprint after Eslint fixes have been applied 71 | - Ensure dprint doesn't attempt to check/format scratch files (perf optimisation) or diff views 72 | - Add verbose logging config 73 | - Add override default IntelliJ formatter config 74 | 75 | ## 0.3.9 76 | 77 | - Fix issue where windows systems reported invalid executables 78 | - Add automatic node module detection from the base project path 79 | - Add 2022.2 to supported versions 80 | 81 | ## 0.3.8 82 | 83 | - Fix issue causing IntelliJ to hang on shutdown 84 | 85 | ## 0.3.7 86 | 87 | - Performance improvements 88 | - Invalidate cache on restart 89 | 90 | ## 0.3.6 91 | 92 | - Fix issue where using the IntelliJ formatter would result in a no-op on every second format, IntelliJ is reporting 93 | larger formatting ranges that content length and dprint would not format these files 94 | - Better handling of virtual files 95 | - Silence an error that is thrown when restarting dprint 96 | - Improve verbose logging in the console 97 | - Add a listener to detect config changes, note this only detects changes made inside IntelliJ 98 | 99 | ## 0.3.5 100 | 101 | - Fix issue when performing code refactoring 102 | 103 | ## 0.3.4 104 | 105 | - Reduce timeout when checking if a file can be formatted in the external formatter 106 | - Cache whether files can be formatted by dprint and create an action to clear this 107 | - Remove custom synchronization and move to an IntelliJ background task queue for dprint tasks (this appears to solve 108 | the hard to reproduce lock up issues) 109 | 110 | ## 0.3.3 111 | 112 | - Handle execution exceptions when running can format 113 | - Ensure on save action is only run when on user triggered saves 114 | 115 | ## 0.3.2 116 | 117 | - Fix intermittent lock up when running format 118 | 119 | ## 0.3.1 120 | 121 | - Fix versioning to allow for 2021.3.x installs 122 | 123 | ## 0.3.0 124 | 125 | - Introduced support for v5 of the dprint schema 126 | - Added a dprint tool window to provide better output of the formatting process 127 | - Added the `Restart Dprint` action so the underlying editor service can be restarted without needed to go to 128 | preferences 129 | - Removed the default key command of `cmd/ctrl+shift+option+D`, it clashed with too many other key commands. Users can 130 | still map this manually should they want it. 131 | 132 | ## 0.2.3 133 | 134 | - Support all future versions of IntelliJ 135 | 136 | ## 0.2.2 137 | 138 | - Fix error that was thrown when code refactoring tools were used 139 | - Synchronize the editor service so two processes can't interrupt each others formatting 140 | - Reduce log spam 141 | 142 | ## 0.2.1 143 | 144 | - Fix issues with changelog 145 | 146 | ## 0.2.0 147 | 148 | - Added support for the inbuilt IntelliJ formatter. This allows dprint to be run at the same time as optimizing imports 149 | using `shift + option + command + L` 150 | 151 | ## 0.1.3 152 | 153 | - Fix issue where the inability to parse the schema would stop a project form opening. 154 | 155 | ## 0.1.2 156 | 157 | - Release first public version of the plugin. 158 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Canva 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dprint 2 | 3 | 4 | This plugin adds support for dprint, a flexible and extensible code formatter ([dprint.dev](https://dprint.dev/)). It is 5 | in active early development, please report bugs and feature requests to 6 | our [github](https://github.com/dprint/dprint-intellij/issues). 7 | 8 | To use this plugin: 9 | 10 | - Install and configure dprint for your repository, [dprint.dev/setup](https://dprint.dev/setup/) 11 | - Configure this plugin at `Preferences` -> `Tools` -> `dprint`. 12 | - Ensure `Enable dprint` is checked 13 | - To run dprint on save check `Run dprint on save`. 14 | - To enable overriding the default IntelliJ formatter check `Default formatter override`. If a file can be 15 | formatted via dprint, the default IntelliJ formatter will be overridden and dprint will be run in its place when 16 | using Option+Shift+Cmd+L on macOS or Alt+Shift+Ctrl+L on Windows and Linux. 17 | - To enable verbose logging from the underlying dprint daemon process check `Verbose daemon logging` 18 | - If your `dprint.json` config file isn't at the base of your project, provide the absolute path to your config in 19 | the `Config file path` field, otherwise it will be detected automatically. 20 | - If dprint isn't on your path or in `node_modules`, provide the absolute path to the dprint executable in 21 | the `Executable path` field, otherwise it will be detected automatically. 22 | - Use the "Reformat with dprint" action by using the "Find Action" popup (Cmd/Ctrl+Shift+A). 23 | - Output from the plugin will be displayed in the dprint tool window. This includes config errors and any syntax errors 24 | that may be stopping your file from being formatted. 25 | 26 | This plugin uses a long-running process known as the `editor-service`. If you change your `dprint.json` file outside of 27 | IntelliJ or dprint is not formatting as expected, run the `Restart dprint` action or in `Preferences` -> `Tools` -> 28 | `dprint` click the `Restart` button. This will force the editor service to close down and restart. 29 | 30 | Please report any issues with this Intellij plugin to the 31 | [github repository](https://github.com/dprint/dprint-intellij/issues). 32 | 33 | 34 | ## Installation 35 | 36 | - Manually: 37 | 38 | Download the [latest release](https://github.com/dprint/dprint-intellij/releases/latest) and install it 39 | manually using 40 | Settings/Preferences > Plugins > ⚙️ > Install plugin from disk... 41 | 42 | --- 43 | 44 | ## Development 45 | 46 | This project is currently built using JDK 17. To install on a mac with homebrew run `brew install java` and set that 47 | be your project SDK. 48 | 49 | ### Dprint setup 50 | 51 | To test this plugin, you will require dprint installed. To install on a mac with homebrew run `brew install dprint`. 52 | When running the plugin via the `Run Plugin` configuration, add a default dprint config file by running `dprint init`. 53 | 54 | ### Intellij Setup 55 | 56 | - Set up linting settings, run Gradle > Tasks > help > ktlintGernateBaseline. 57 | This sets up intellij with appropriate formatting settings. 58 | 59 | ### Running 60 | 61 | There are 3 default run configs set up 62 | 63 | - Run Plugin - This starts up an instance of Intellij Ultimate with the plugin installed and enabled. 64 | - Run Tests - This runs linting and tests. 65 | - Run Verifications - This verifies the plugin is publishable. 66 | 67 | Depending on the version of IntellJ you are running for development, you will need to change the `platformType` property 68 | in `gradle.properties`. It is IU for IntelliJ Ultimate and IC for IntelliJ Community. 69 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask 2 | import org.jetbrains.changelog.Changelog 3 | import org.jetbrains.changelog.markdownToHTML 4 | import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType 5 | import org.jetbrains.intellij.platform.gradle.models.ProductRelease 6 | 7 | plugins { 8 | // Java support 9 | id("java") 10 | alias(libs.plugins.kotlin) // Kotlin support 11 | alias(libs.plugins.gradleIntelliJPlugin) // Gradle IntelliJ Plugin 12 | alias(libs.plugins.changelog) // Gradle Changelog Plugin 13 | alias(libs.plugins.ktlint) // ktlint formatter 14 | // Both used for updating gradle dep catalog 15 | alias(libs.plugins.versions) 16 | alias(libs.plugins.versionCatalogUpdate) 17 | } 18 | 19 | group = providers.gradleProperty("pluginGroup").get() 20 | version = providers.gradleProperty("pluginVersion").get() 21 | 22 | // Configure project's dependencies 23 | repositories { 24 | mavenCentral() 25 | intellijPlatform { 26 | defaultRepositories() 27 | } 28 | } 29 | dependencies { 30 | implementation(libs.commonsCollection4) 31 | testImplementation(libs.kotestAssertions) 32 | testImplementation(libs.kotestRunner) 33 | testImplementation(libs.mockk) 34 | 35 | intellijPlatform { 36 | val version = providers.gradleProperty("platformVersion") 37 | val type = providers.gradleProperty("platformType") 38 | 39 | create(type, version) 40 | 41 | intellijIdeaUltimate(version) 42 | instrumentationTools() 43 | pluginVerifier() 44 | 45 | bundledPlugin("JavaScript") 46 | } 47 | } 48 | 49 | kotlin { 50 | jvmToolchain { 51 | languageVersion = JavaLanguageVersion.of(17) 52 | vendor = JvmVendorSpec.AZUL 53 | } 54 | } 55 | 56 | intellijPlatform { 57 | buildSearchableOptions = false 58 | 59 | pluginConfiguration { 60 | name = providers.gradleProperty("pluginName") 61 | version = providers.gradleProperty("pluginVersion") 62 | 63 | // Extract the section from README.md and provide for the plugin's manifest 64 | description = 65 | providers.fileContents(layout.projectDirectory.file("README.md")).asText.map { 66 | val start = "" 67 | val end = "" 68 | 69 | with(it.lines()) { 70 | if (!containsAll(listOf(start, end))) { 71 | throw GradleException("Plugin description section not found in README.md:\n$start ... $end") 72 | } 73 | subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML) 74 | } 75 | } 76 | 77 | val changelog = project.changelog // local variable for configuration cache compatibility 78 | // Get the latest available change notes from the changelog file 79 | changeNotes = 80 | providers.gradleProperty("pluginVersion").map { pluginVersion -> 81 | with(changelog) { 82 | renderItem( 83 | (getOrNull(pluginVersion) ?: getUnreleased()) 84 | .withHeader(false) 85 | .withEmptySections(false), 86 | Changelog.OutputType.HTML, 87 | ) 88 | } 89 | } 90 | 91 | ideaVersion { 92 | sinceBuild = providers.gradleProperty("pluginSinceBuild") 93 | untilBuild = provider { null } 94 | } 95 | } 96 | 97 | publishing { 98 | token = providers.environmentVariable("PUBLISH_TOKEN") 99 | // pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 100 | // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: 101 | // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel 102 | channels = 103 | providers.gradleProperty("pluginVersion").map { 104 | listOf(it.split('-').getOrElse(1) { "default" }.split('.')[0]) 105 | } 106 | } 107 | 108 | pluginVerification { 109 | freeArgs = listOf("-mute", "TemplateWordInPluginId") 110 | ides { 111 | select { 112 | types = 113 | listOf( 114 | IntelliJPlatformType.IntellijIdeaUltimate, 115 | ) 116 | channels = listOf(ProductRelease.Channel.RELEASE, ProductRelease.Channel.EAP) 117 | sinceBuild = providers.gradleProperty("pluginSinceBuild") 118 | } 119 | } 120 | } 121 | } 122 | 123 | // Configure gradle-changelog-plugin plugin. 124 | // Read more: https://github.com/JetBrains/gradle-changelog-plugin 125 | changelog { 126 | version = providers.gradleProperty("pluginVersion") 127 | groups = listOf() 128 | keepUnreleasedSection = true 129 | unreleasedTerm = "[Unreleased]" 130 | } 131 | 132 | sourceSets { 133 | main { 134 | java { 135 | srcDir("src/main/kotlin") 136 | } 137 | } 138 | test { 139 | java { 140 | srcDir("src/test/kotlin") 141 | } 142 | } 143 | } 144 | 145 | fun isNonStable(version: String): Boolean { 146 | val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) } 147 | val regex = "^[0-9,.v-]+(-r)?$".toRegex() 148 | val isStable = stableKeyword || regex.matches(version) 149 | return isStable.not() 150 | } 151 | 152 | tasks { 153 | withType().configureEach { 154 | useJUnitPlatform() 155 | } 156 | 157 | withType { 158 | rejectVersionIf { 159 | isNonStable(candidate.version) 160 | } 161 | } 162 | 163 | runIde { 164 | jvmArgs("-Xmx4098m") 165 | } 166 | 167 | wrapper { 168 | gradleVersion = providers.gradleProperty("gradleVersion").get() 169 | } 170 | 171 | publishPlugin { 172 | dependsOn("patchChangelog") 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories 2 | # -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 3 | pluginGroup=com.dprint.intellij.plugin 4 | pluginName=dprint 5 | pluginVersion=0.8.3 6 | # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 7 | # for insight into build numbers and IntelliJ Platform versions. 8 | pluginSinceBuild=241 9 | platformType=IU 10 | platformVersion=2024.1 11 | platformDownloadSources=true 12 | # Opt-out flag for bundling Kotlin standard library. 13 | # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. 14 | kotlin.stdlib.default.dependency=false 15 | org.gradle.jvmargs=-XX\:MaxHeapSize\=4096m -Xmx4096m 16 | # Gradle Releases -> https://github.com/gradle/gradle/releases 17 | gradleVersion=8.5 18 | # https://github.com/gradle/gradle/issues/20416 19 | systemProp.org.gradle.kotlin.dsl.precompiled.accessors.strict=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | changelog = "2.2.1" 3 | commonsCollection4 = "4.4" 4 | gradleIntelliJPlugin = "2.1.0" 5 | kotestAssertions = "5.9.1" 6 | kotestRunner = "5.9.1" 7 | kotlin = "2.1.0" 8 | ktlint = "12.1.2" 9 | mockk = "1.13.13" 10 | versionCatalogUpdate = "0.8.5" 11 | versions = "0.51.0" 12 | 13 | [libraries] 14 | commonsCollection4 = { module = "org.apache.commons:commons-collections4", version.ref = "commonsCollection4" } 15 | kotestAssertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotestAssertions" } 16 | kotestRunner = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotestRunner" } 17 | mockk = { module = "io.mockk:mockk", version.ref = "mockk" } 18 | 19 | [plugins] 20 | # Changelog update gradle plugin - https://github.com/JetBrains/gradle-changelog-plugin 21 | changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } 22 | # IntelliJ gradle plugin - https://plugins.gradle.org/plugin/org.jetbrains.intellij 23 | gradleIntelliJPlugin = { id = "org.jetbrains.intellij.platform", version.ref = "gradleIntelliJPlugin" } 24 | # Kotlin gradle plugin - https://plugins.gradle.org/plugin/org.jetbrains.kotlin.jvm 25 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 26 | # ktlint formatter and linter - https://github.com/JLLeitschuh/ktlint-gradle 27 | ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } 28 | # Updates gradle plugin versions - https://github.com/littlerobots/version-catalog-update-plugin 29 | versionCatalogUpdate = { id = "nl.littlerobots.version-catalog-update", version.ref = "versionCatalogUpdate" } 30 | versions = { id = "com.github.ben-manes.versions", version.ref = "versions" } 31 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dprint/dprint-intellij/491b4333758ceb58fd2fa275b048c6a919cfbcaa/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command; 206 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 207 | # shell script including quotes and variable substitutions, so put them in 208 | # double quotes to make sure that they get re-expanded; and 209 | # * put everything else in single quotes, so that it's not re-expanded. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /qodana.yaml: -------------------------------------------------------------------------------- 1 | # Qodana configuration: 2 | # https://www.jetbrains.com/help/qodana/qodana-yaml.html 3 | 4 | version: 1.0 5 | linter: jetbrains/qodana-jvm-community:latest 6 | projectJDK: "17" 7 | profile: 8 | name: qodana.recommended 9 | exclude: 10 | - name: All 11 | paths: 12 | - .qodana -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0" 3 | } 4 | 5 | rootProject.name = "dprint" 6 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/actions/ClearCacheAction.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.actions 2 | 3 | import com.dprint.config.ProjectConfiguration 4 | import com.dprint.i18n.DprintBundle 5 | import com.dprint.services.editorservice.EditorServiceManager 6 | import com.dprint.utils.infoLogWithConsole 7 | import com.intellij.openapi.actionSystem.AnAction 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.components.service 10 | import com.intellij.openapi.diagnostic.logger 11 | 12 | private val LOGGER = logger() 13 | 14 | /** 15 | * This action clears the cache of canFormat results. Useful if config changes have been made. 16 | */ 17 | class ClearCacheAction : AnAction() { 18 | override fun actionPerformed(event: AnActionEvent) { 19 | event.project?.let { 20 | val projectConfig = it.service().state 21 | if (!projectConfig.enabled) return@let 22 | infoLogWithConsole(DprintBundle.message("clear.cache.action.run"), it, LOGGER) 23 | it.service().clearCanFormatCache() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/actions/ReformatAction.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.actions 2 | 3 | import com.dprint.config.ProjectConfiguration 4 | import com.dprint.i18n.DprintBundle 5 | import com.dprint.services.FormatterService 6 | import com.dprint.utils.infoLogWithConsole 7 | import com.intellij.openapi.actionSystem.AnAction 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.actionSystem.PlatformDataKeys 10 | import com.intellij.openapi.components.service 11 | import com.intellij.openapi.diagnostic.logger 12 | import com.intellij.openapi.editor.Document 13 | import com.intellij.openapi.project.Project 14 | import com.intellij.openapi.vfs.ReadonlyStatusHandler 15 | import com.intellij.openapi.vfs.VirtualFile 16 | import com.intellij.psi.PsiDocumentManager 17 | import com.intellij.psi.PsiManager 18 | 19 | private val LOGGER = logger() 20 | 21 | /** 22 | * This action is intended to be hooked up to a menu option or a key command. It handles events for two separate data 23 | * types, editor and virtual files. 24 | * 25 | * For editor data, it will pull the virtual file from the editor and for both it will send the virtual file to 26 | * DprintFormatterService to be formatted and saved. 27 | */ 28 | class ReformatAction : AnAction() { 29 | override fun actionPerformed(event: AnActionEvent) { 30 | event.project?.let { project -> 31 | val projectConfig = project.service().state 32 | if (!projectConfig.enabled) return@let 33 | 34 | val editor = event.getData(PlatformDataKeys.EDITOR) 35 | if (editor != null) { 36 | formatDocument(project, editor.document) 37 | } else { 38 | event.getData(PlatformDataKeys.VIRTUAL_FILE)?.let { virtualFile -> 39 | formatVirtualFile(project, virtualFile) 40 | } 41 | } 42 | } 43 | } 44 | 45 | private fun formatDocument( 46 | project: Project, 47 | document: Document, 48 | ) { 49 | val formatterService = project.service() 50 | val documentManager = PsiDocumentManager.getInstance(project) 51 | documentManager.getPsiFile(document)?.virtualFile?.let { virtualFile -> 52 | infoLogWithConsole(DprintBundle.message("reformat.action.run", virtualFile.path), project, LOGGER) 53 | formatterService.format(virtualFile, document) 54 | } 55 | } 56 | 57 | private fun formatVirtualFile( 58 | project: Project, 59 | virtualFile: VirtualFile, 60 | ) { 61 | val formatterService = project.service() 62 | infoLogWithConsole(DprintBundle.message("reformat.action.run", virtualFile.path), project, LOGGER) 63 | getDocument(project, virtualFile)?.let { 64 | formatterService.format(virtualFile, it) 65 | } 66 | } 67 | 68 | private fun isFileWriteable( 69 | project: Project, 70 | virtualFile: VirtualFile, 71 | ): Boolean { 72 | val readonlyStatusHandler = ReadonlyStatusHandler.getInstance(project) 73 | return !virtualFile.isDirectory && 74 | virtualFile.isValid && 75 | virtualFile.isInLocalFileSystem && 76 | !readonlyStatusHandler.ensureFilesWritable(listOf(virtualFile)).hasReadonlyFiles() 77 | } 78 | 79 | private fun getDocument( 80 | project: Project, 81 | virtualFile: VirtualFile, 82 | ): Document? { 83 | if (isFileWriteable(project, virtualFile)) { 84 | PsiManager.getInstance(project).findFile(virtualFile)?.let { 85 | return PsiDocumentManager.getInstance(project).getDocument(it) 86 | } 87 | } 88 | 89 | return null 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/actions/RestartAction.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.actions 2 | 3 | import com.dprint.config.ProjectConfiguration 4 | import com.dprint.i18n.DprintBundle 5 | import com.dprint.services.editorservice.EditorServiceManager 6 | import com.dprint.utils.infoLogWithConsole 7 | import com.intellij.openapi.actionSystem.AnAction 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.components.service 10 | import com.intellij.openapi.diagnostic.logger 11 | 12 | private val LOGGER = logger() 13 | 14 | /** 15 | * This action will restart the editor service when invoked 16 | */ 17 | class RestartAction : AnAction() { 18 | override fun actionPerformed(event: AnActionEvent) { 19 | event.project?.let { 20 | val enabled = it.service().state.enabled 21 | if (!enabled) return@let 22 | infoLogWithConsole(DprintBundle.message("restart.action.run"), it, LOGGER) 23 | it.service().restartEditorService() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/config/DprintConfigurable.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.config 2 | 3 | import com.dprint.i18n.DprintBundle 4 | import com.dprint.services.editorservice.EditorServiceManager 5 | import com.dprint.utils.validateConfigFile 6 | import com.dprint.utils.validateExecutablePath 7 | import com.intellij.ide.actionsOnSave.ActionOnSaveBackedByOwnConfigurable 8 | import com.intellij.ide.actionsOnSave.ActionOnSaveContext 9 | import com.intellij.ide.actionsOnSave.ActionOnSaveInfo 10 | import com.intellij.ide.actionsOnSave.ActionOnSaveInfoProvider 11 | import com.intellij.openapi.components.service 12 | import com.intellij.openapi.options.BoundSearchableConfigurable 13 | import com.intellij.openapi.project.Project 14 | import com.intellij.openapi.ui.DialogPanel 15 | import com.intellij.ui.dsl.builder.LabelPosition 16 | import com.intellij.ui.dsl.builder.bindSelected 17 | import com.intellij.ui.dsl.builder.bindText 18 | import com.intellij.ui.dsl.builder.panel 19 | import javax.swing.JCheckBox 20 | 21 | const val CONFIG_ID = "com.dprint.config" 22 | 23 | /** 24 | * Sets up the configuration panel for Dprint in the Tools section of preferences. 25 | */ 26 | class DprintConfigurable(private val project: Project) : BoundSearchableConfigurable( 27 | DprintBundle.message("config.name"), 28 | "reference.settings.dprint", 29 | CONFIG_ID, 30 | ) { 31 | lateinit var enabledCheckBox: JCheckBox 32 | lateinit var runOnSaveCheckbox: JCheckBox 33 | 34 | override fun createPanel(): DialogPanel { 35 | val projectConfig = project.service() 36 | val userConfig = project.service() 37 | val editorServiceManager = project.service() 38 | 39 | return panel { 40 | // Restart or destroy editor service on apply 41 | onApply { 42 | if (projectConfig.state.enabled) { 43 | editorServiceManager.restartEditorService() 44 | } else { 45 | editorServiceManager.destroyEditorService() 46 | } 47 | } 48 | 49 | // Enabled checkbox 50 | row { 51 | enabledCheckBox = 52 | checkBox(DprintBundle.message("config.enable")) 53 | .bindSelected( 54 | { projectConfig.state.enabled }, 55 | { projectConfig.state.enabled = it }, 56 | ) 57 | .comment(DprintBundle.message("config.enable.description")) 58 | .component 59 | } 60 | 61 | // Format on save checkbox 62 | row { 63 | runOnSaveCheckbox = 64 | checkBox(DprintBundle.message("config.run.on.save")) 65 | .bindSelected( 66 | { userConfig.state.runOnSave }, 67 | { userConfig.state.runOnSave = it }, 68 | ) 69 | .comment(DprintBundle.message("config.run.on.save.description")) 70 | .component 71 | } 72 | 73 | // Default IJ override checkbox 74 | row { 75 | checkBox(DprintBundle.message("config.override.intellij.formatter")) 76 | .bindSelected( 77 | { userConfig.state.overrideIntelliJFormatter }, 78 | { userConfig.state.overrideIntelliJFormatter = it }, 79 | ) 80 | .comment(DprintBundle.message("config.override.intellij.formatter.description")) 81 | } 82 | 83 | // Verbose logging checkbox 84 | row { 85 | checkBox(DprintBundle.message("config.verbose.logging")) 86 | .bindSelected( 87 | { userConfig.state.enableEditorServiceVerboseLogging }, 88 | { userConfig.state.enableEditorServiceVerboseLogging = it }, 89 | ) 90 | .comment(DprintBundle.message("config.verbose.logging.description")) 91 | } 92 | 93 | // dprint.json path input and file finder 94 | indent { 95 | row { 96 | textFieldWithBrowseButton() 97 | .bindText( 98 | { projectConfig.state.configLocation }, 99 | { projectConfig.state.configLocation = it }, 100 | ) 101 | .label(DprintBundle.message("config.dprint.config.path"), LabelPosition.TOP) 102 | .comment(DprintBundle.message("config.dprint.config.path.description")) 103 | .validationOnInput { 104 | if (it.text.isEmpty() || validateConfigFile(it.text)) { 105 | null 106 | } else { 107 | this.error(DprintBundle.message("config.dprint.config.invalid")) 108 | } 109 | } 110 | } 111 | } 112 | 113 | // dprint executable input and file finder 114 | indent { 115 | row { 116 | textFieldWithBrowseButton() 117 | .bindText( 118 | { projectConfig.state.executableLocation }, 119 | { projectConfig.state.executableLocation = it }, 120 | ) 121 | .label(DprintBundle.message("config.dprint.executable.path"), LabelPosition.TOP) 122 | .comment(DprintBundle.message("config.dprint.executable.path.description")) 123 | .validationOnInput { 124 | if (it.text.isEmpty() || validateExecutablePath(it.text)) { 125 | null 126 | } else { 127 | this.error(DprintBundle.message("config.dprint.executable.invalid")) 128 | } 129 | } 130 | } 131 | } 132 | 133 | indent { 134 | row { 135 | intTextField(IntRange(0, 100_000), 1_000) 136 | .bindText( 137 | { projectConfig.state.initialisationTimeout.toString() }, 138 | { projectConfig.state.initialisationTimeout = it.toLong() }, 139 | ) 140 | .label(DprintBundle.message("config.dprint.initialisation.timeout"), LabelPosition.TOP) 141 | .comment(DprintBundle.message("config.dprint.initialisation.timeout.description")) 142 | .validationOnInput { 143 | if (it.text.toLongOrNull() != null) { 144 | null 145 | } else { 146 | this.error(DprintBundle.message("config.dprint.initialisation.timeout.error")) 147 | } 148 | } 149 | } 150 | } 151 | 152 | indent { 153 | row { 154 | intTextField(IntRange(0, 100_000), 1_000) 155 | .bindText( 156 | { projectConfig.state.commandTimeout.toString() }, 157 | { projectConfig.state.commandTimeout = it.toLong() }, 158 | ) 159 | .label(DprintBundle.message("config.dprint.command.timeout"), LabelPosition.TOP) 160 | .comment(DprintBundle.message("config.dprint.command.timeout.description")) 161 | .validationOnInput { 162 | if (it.text.toLongOrNull() != null) { 163 | null 164 | } else { 165 | this.error(DprintBundle.message("config.dprint.command.timeout.error")) 166 | } 167 | } 168 | } 169 | } 170 | 171 | // Restart button 172 | indent { 173 | row { 174 | button(DprintBundle.message("config.reload")) { 175 | editorServiceManager.restartEditorService() 176 | }.comment(DprintBundle.message("config.reload.description")) 177 | } 178 | } 179 | } 180 | } 181 | 182 | // This is used so that we can also have our "run on save" checkbox in the 183 | // Tools -> Actions On Save menu 184 | class DprintActionOnSaveInfoProvider : ActionOnSaveInfoProvider() { 185 | override fun getActionOnSaveInfos( 186 | actionOnSaveContext: ActionOnSaveContext, 187 | ): MutableCollection { 188 | return mutableListOf(DprintActionOnSaveInfo(actionOnSaveContext)) 189 | } 190 | 191 | override fun getSearchableOptions(): MutableCollection { 192 | return mutableSetOf(DprintBundle.message("config.dprint.actions.on.save.run.dprint")) 193 | } 194 | } 195 | 196 | private class DprintActionOnSaveInfo(actionOnSaveContext: ActionOnSaveContext) : 197 | ActionOnSaveBackedByOwnConfigurable( 198 | actionOnSaveContext, 199 | CONFIG_ID, 200 | DprintConfigurable::class.java, 201 | ) { 202 | override fun getActionOnSaveName() = DprintBundle.message("config.dprint.actions.on.save.run.dprint") 203 | 204 | override fun isApplicableAccordingToStoredState(): Boolean { 205 | return project.service().state.enabled 206 | } 207 | 208 | override fun isApplicableAccordingToUiState(configurable: DprintConfigurable): Boolean { 209 | return configurable.enabledCheckBox.isSelected 210 | } 211 | 212 | override fun isActionOnSaveEnabledAccordingToStoredState(): Boolean { 213 | return project.service().state.runOnSave 214 | } 215 | 216 | override fun isActionOnSaveEnabledAccordingToUiState(configurable: DprintConfigurable) = 217 | configurable.runOnSaveCheckbox.isSelected 218 | 219 | override fun setActionOnSaveEnabled( 220 | configurable: DprintConfigurable, 221 | enabled: Boolean, 222 | ) { 223 | configurable.runOnSaveCheckbox.isSelected = enabled 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/config/ProjectConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.config 2 | 3 | import com.intellij.openapi.components.PersistentStateComponent 4 | import com.intellij.openapi.components.Service 5 | import com.intellij.openapi.components.State 6 | import com.intellij.openapi.components.Storage 7 | 8 | /** 9 | * Persists configuration between IDE sessions per project. Configuration is stored in .idea/dprintConfig.xml. 10 | */ 11 | @Service(Service.Level.PROJECT) 12 | @State(name = "DprintProjectConfiguration", storages = [Storage("dprintProjectConfig.xml")]) 13 | class ProjectConfiguration : PersistentStateComponent { 14 | class State { 15 | var enabled: Boolean = false 16 | var configLocation: String = "" 17 | var executableLocation: String = "" 18 | var initialisationTimeout: Long = 10_000 19 | var commandTimeout: Long = 5_000 20 | } 21 | 22 | private var internalState = State() 23 | 24 | override fun getState(): State { 25 | return internalState 26 | } 27 | 28 | override fun loadState(state: State) { 29 | internalState = state 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/config/UserConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.config 2 | 3 | import com.intellij.openapi.components.PersistentStateComponent 4 | import com.intellij.openapi.components.Service 5 | import com.intellij.openapi.components.State 6 | import com.intellij.openapi.components.Storage 7 | 8 | /** 9 | * Persists configuration between IDE sessions per project. Configuration is stored in .idea/dprintConfig.xml. 10 | */ 11 | @Service(Service.Level.PROJECT) 12 | @State(name = "DprintUserConfiguration", storages = [Storage("dprintUserConfig.xml")]) 13 | class UserConfiguration : PersistentStateComponent { 14 | class State { 15 | var runOnSave = false 16 | var overrideIntelliJFormatter = true 17 | var enableEditorServiceVerboseLogging = true 18 | } 19 | 20 | private var internalState = State() 21 | 22 | override fun getState(): State { 23 | return internalState 24 | } 25 | 26 | override fun loadState(state: State) { 27 | internalState = state 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/formatter/DprintDocumentMerger.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.formatter 2 | 3 | import com.intellij.formatting.service.DocumentMerger 4 | import com.intellij.openapi.editor.Document 5 | 6 | class DprintDocumentMerger : DocumentMerger { 7 | override fun updateDocument( 8 | document: Document, 9 | newText: String, 10 | ): Boolean { 11 | if (document.isWritable) { 12 | document.setText(newText) 13 | return true 14 | } 15 | 16 | return false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/formatter/DprintExternalFormatter.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.formatter 2 | 3 | import com.dprint.config.ProjectConfiguration 4 | import com.dprint.config.UserConfiguration 5 | import com.dprint.i18n.DprintBundle 6 | import com.dprint.services.editorservice.EditorServiceManager 7 | import com.dprint.utils.infoConsole 8 | import com.dprint.utils.infoLogWithConsole 9 | import com.dprint.utils.isFormattableFile 10 | import com.dprint.utils.warnLogWithConsole 11 | import com.intellij.formatting.service.AsyncDocumentFormattingService 12 | import com.intellij.formatting.service.AsyncFormattingRequest 13 | import com.intellij.formatting.service.FormattingService 14 | import com.intellij.openapi.components.service 15 | import com.intellij.openapi.diagnostic.logger 16 | import com.intellij.psi.PsiFile 17 | 18 | private val LOGGER = logger() 19 | private const val NAME = "dprintfmt" 20 | 21 | /** 22 | * This class is the recommended way to implement an external formatter in the IJ 23 | * framework. 24 | * 25 | * How it works is that extends AsyncDocumentFormattingService and IJ 26 | * will use the `canFormat` method to determine if this formatter should be used 27 | * for a given file. If yes, then this will be run and the IJ formatter will not. 28 | * If no, it passes through his formatter and checks the next registered formatter 29 | * until it eventually gets to the IJ formatter as a last resort. 30 | */ 31 | class DprintExternalFormatter : AsyncDocumentFormattingService() { 32 | override fun getFeatures(): MutableSet { 33 | return mutableSetOf(FormattingService.Feature.FORMAT_FRAGMENTS) 34 | } 35 | 36 | override fun canFormat(file: PsiFile): Boolean { 37 | val projectConfig = file.project.service().state 38 | val userConfig = file.project.service().state 39 | val editorServiceManager = file.project.service() 40 | 41 | if (!projectConfig.enabled) return false 42 | 43 | if (!userConfig.overrideIntelliJFormatter) { 44 | infoConsole(DprintBundle.message("external.formatter.not.configured.to.override"), file.project) 45 | } 46 | 47 | // If we don't have a cached can format response then we return true and let the formatting task figure that 48 | // out. Worse case scenario is that a file that cannot be formatted by dprint won't trigger the default IntelliJ 49 | // formatter. This is a minor issue and should be resolved if they run it again. We need to take this approach 50 | // as it appears that blocking the EDT here causes quite a few issues. Also, we ignore scratch files as a perf 51 | // optimisation because they are not part of the project and thus never in config. 52 | val virtualFile = file.virtualFile ?: file.originalFile.virtualFile 53 | val canFormat = 54 | if (virtualFile != null && isFormattableFile(file.project, virtualFile)) { 55 | editorServiceManager.canFormatCached(virtualFile.path) 56 | } else { 57 | false 58 | } 59 | 60 | if (canFormat == null) { 61 | warnLogWithConsole(DprintBundle.message("external.formatter.can.format.unknown"), file.project, LOGGER) 62 | return false 63 | } else if (canFormat) { 64 | infoConsole(DprintBundle.message("external.formatter.can.format", virtualFile.path), file.project) 65 | } else if (virtualFile?.path != null) { 66 | // If a virtual file path doesn't exist then it is an ephemeral file such as a scratch file. Dprint needs 67 | // real files to work. I have tried to log this in the past but it seems to be called frequently resulting 68 | // in log spam, so in the case the path doesn't exist, we just do nothing. 69 | infoConsole(DprintBundle.message("external.formatter.cannot.format", virtualFile.path), file.project) 70 | } 71 | 72 | return canFormat 73 | } 74 | 75 | override fun createFormattingTask(formattingRequest: AsyncFormattingRequest): FormattingTask? { 76 | val project = formattingRequest.context.project 77 | 78 | val editorServiceManager = project.service() 79 | val path = formattingRequest.ioFile?.path 80 | 81 | if (path == null) { 82 | infoLogWithConsole(DprintBundle.message("formatting.cannot.determine.file.path"), project, LOGGER) 83 | return null 84 | } 85 | 86 | if (!editorServiceManager.canRangeFormat() && isRangeFormat(formattingRequest)) { 87 | // When range formatting is available we need to add support here. 88 | infoLogWithConsole(DprintBundle.message("external.formatter.range.formatting"), project, LOGGER) 89 | return null 90 | } 91 | 92 | if (doAnyRangesIntersect(formattingRequest)) { 93 | infoLogWithConsole(DprintBundle.message("external.formatter.range.overlapping"), project, LOGGER) 94 | return null 95 | } 96 | 97 | infoLogWithConsole(DprintBundle.message("external.formatter.creating.task", path), project, LOGGER) 98 | 99 | return object : FormattingTask { 100 | val dprintTask = DprintFormattingTask(project, editorServiceManager, formattingRequest, path) 101 | 102 | override fun run() { 103 | return dprintTask.run() 104 | } 105 | 106 | override fun cancel(): Boolean { 107 | return dprintTask.cancel() 108 | } 109 | 110 | override fun isRunUnderProgress(): Boolean { 111 | return dprintTask.isRunUnderProgress() 112 | } 113 | } 114 | } 115 | 116 | /** 117 | * We make assumptions that ranges do not overlap each other in our formatting algorithm. 118 | */ 119 | private fun doAnyRangesIntersect(formattingRequest: AsyncFormattingRequest): Boolean { 120 | val ranges = formattingRequest.formattingRanges.sortedBy { textRange -> textRange.startOffset } 121 | for (i in ranges.indices) { 122 | if (i + 1 >= ranges.size) { 123 | continue 124 | } 125 | val current = ranges[i] 126 | val next = ranges[i + 1] 127 | 128 | if (current.intersects(next)) { 129 | return true 130 | } 131 | } 132 | return false 133 | } 134 | 135 | private fun isRangeFormat(formattingRequest: AsyncFormattingRequest): Boolean { 136 | return when { 137 | formattingRequest.formattingRanges.size > 1 -> true 138 | formattingRequest.formattingRanges.size == 1 -> { 139 | val range = formattingRequest.formattingRanges[0] 140 | return range.startOffset > 0 || range.endOffset < formattingRequest.documentText.length 141 | } 142 | 143 | else -> false 144 | } 145 | } 146 | 147 | override fun getNotificationGroupId(): String { 148 | return "Dprint" 149 | } 150 | 151 | override fun getName(): String { 152 | return NAME 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/formatter/DprintFormattingTask.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.formatter 2 | 3 | import com.dprint.i18n.DprintBundle 4 | import com.dprint.services.editorservice.EditorServiceManager 5 | import com.dprint.services.editorservice.FormatResult 6 | import com.dprint.services.editorservice.exceptions.ProcessUnavailableException 7 | import com.dprint.utils.errorLogWithConsole 8 | import com.dprint.utils.infoLogWithConsole 9 | import com.dprint.utils.warnLogWithConsole 10 | import com.intellij.formatting.service.AsyncFormattingRequest 11 | import com.intellij.openapi.diagnostic.logger 12 | import com.intellij.openapi.project.Project 13 | import com.intellij.openapi.util.TextRange 14 | import java.util.concurrent.CancellationException 15 | import java.util.concurrent.CompletableFuture 16 | import java.util.concurrent.ExecutionException 17 | import java.util.concurrent.TimeUnit 18 | import java.util.concurrent.TimeoutException 19 | 20 | private val LOGGER = logger() 21 | private const val FORMATTING_TIMEOUT = 10L 22 | 23 | class DprintFormattingTask( 24 | private val project: Project, 25 | private val editorServiceManager: EditorServiceManager, 26 | private val formattingRequest: AsyncFormattingRequest, 27 | private val path: String, 28 | ) { 29 | private var formattingIds = mutableListOf() 30 | private var isCancelled = false 31 | 32 | /** 33 | * Used when we want to cancel a format, so that we can cancel every future in the chain. 34 | */ 35 | private val allFormatFutures = mutableListOf>() 36 | 37 | fun run() { 38 | val content = formattingRequest.documentText 39 | val ranges = 40 | if (editorServiceManager.canRangeFormat()) { 41 | formattingRequest.formattingRanges 42 | } else { 43 | mutableListOf( 44 | TextRange(0, content.length), 45 | ) 46 | } 47 | 48 | infoLogWithConsole( 49 | DprintBundle.message("external.formatter.running.task", path), 50 | project, 51 | LOGGER, 52 | ) 53 | 54 | val initialResult = FormatResult(formattedContent = content) 55 | val baseFormatFuture = CompletableFuture.completedFuture(initialResult) 56 | allFormatFutures.add(baseFormatFuture) 57 | 58 | var nextFuture = baseFormatFuture 59 | for (range in ranges.subList(0, ranges.size)) { 60 | nextFuture.thenCompose { formatResult -> 61 | nextFuture = 62 | if (isCancelled) { 63 | // Revert to the initial contents 64 | CompletableFuture.completedFuture(initialResult) 65 | } else { 66 | applyNextRangeFormat( 67 | path, 68 | formatResult, 69 | getStartOfRange(formatResult.formattedContent, content, range), 70 | getEndOfRange(formatResult.formattedContent, content, range), 71 | ) 72 | } 73 | nextFuture 74 | } 75 | } 76 | 77 | // Timeouts are handled at the EditorServiceManager level and an empty result will be 78 | // returned if something goes wrong 79 | val result = getFuture(nextFuture) 80 | 81 | // If cancelled there is no need to utilise the formattingRequest finalising methods 82 | if (isCancelled) return 83 | 84 | // If the result is null we don't want to change the document text, so we just set it to be the original. 85 | // This should only happen if getting the future throws. 86 | if (result == null) { 87 | formattingRequest.onTextReady(content) 88 | return 89 | } 90 | 91 | val error = result.error 92 | if (error != null) { 93 | formattingRequest.onError(DprintBundle.message("formatting.error"), error) 94 | } else { 95 | // If the result is a no op it will be null, in which case we pass the original content back in 96 | formattingRequest.onTextReady(result.formattedContent ?: content) 97 | } 98 | } 99 | 100 | private fun getFuture(future: CompletableFuture): FormatResult? { 101 | return try { 102 | future.get(FORMATTING_TIMEOUT, TimeUnit.SECONDS) 103 | } catch (e: CancellationException) { 104 | errorLogWithConsole("External format process cancelled", e, project, LOGGER) 105 | null 106 | } catch (e: TimeoutException) { 107 | errorLogWithConsole("External format process timed out", e, project, LOGGER) 108 | formattingRequest.onError("Dprint external formatter", "Format process timed out") 109 | editorServiceManager.restartEditorService() 110 | null 111 | } catch (e: ExecutionException) { 112 | if (e.cause is ProcessUnavailableException) { 113 | warnLogWithConsole( 114 | DprintBundle.message("editor.service.process.is.dead"), 115 | e.cause, 116 | project, 117 | LOGGER, 118 | ) 119 | } 120 | errorLogWithConsole("External format process failed", e, project, LOGGER) 121 | formattingRequest.onError("Dprint external formatter", "Format process failed") 122 | editorServiceManager.restartEditorService() 123 | null 124 | } catch (e: InterruptedException) { 125 | errorLogWithConsole("External format process interrupted", e, project, LOGGER) 126 | formattingRequest.onError("Dprint external formatter", "Format process interrupted") 127 | editorServiceManager.restartEditorService() 128 | null 129 | } 130 | } 131 | 132 | private fun applyNextRangeFormat( 133 | path: String, 134 | previousFormatResult: FormatResult, 135 | startIndex: Int?, 136 | endIndex: Int?, 137 | ): CompletableFuture? { 138 | val contentToFormat = previousFormatResult.formattedContent 139 | if (contentToFormat == null || startIndex == null || endIndex == null) { 140 | errorLogWithConsole( 141 | DprintBundle.message( 142 | "external.formatter.illegal.state", 143 | startIndex ?: "null", 144 | endIndex ?: "null", 145 | contentToFormat ?: "null", 146 | ), 147 | project, 148 | LOGGER, 149 | ) 150 | return null 151 | } 152 | 153 | // Need to update the formatting id so the correct job would be cancelled 154 | val formattingId = editorServiceManager.maybeGetFormatId() 155 | formattingId?.let { 156 | formattingIds.add(it) 157 | } 158 | 159 | val nextFuture = CompletableFuture() 160 | allFormatFutures.add(nextFuture) 161 | val nextHandler: (FormatResult) -> Unit = { nextResult -> 162 | nextFuture.complete(nextResult) 163 | } 164 | editorServiceManager.format( 165 | formattingId, 166 | path, 167 | contentToFormat, 168 | startIndex, 169 | endIndex, 170 | nextHandler, 171 | ) 172 | 173 | return nextFuture 174 | } 175 | 176 | fun cancel(): Boolean { 177 | if (!editorServiceManager.canCancelFormat()) return false 178 | 179 | isCancelled = true 180 | for (id in formattingIds) { 181 | infoLogWithConsole( 182 | DprintBundle.message("external.formatter.cancelling.task", id), 183 | project, 184 | LOGGER, 185 | ) 186 | editorServiceManager.cancelFormat(id) 187 | } 188 | 189 | // Clean up state so process can complete 190 | allFormatFutures.stream().forEach { f -> f.cancel(true) } 191 | return true 192 | } 193 | 194 | fun isRunUnderProgress(): Boolean { 195 | return true 196 | } 197 | } 198 | 199 | private fun getStartOfRange( 200 | currentContent: String?, 201 | originalContent: String, 202 | range: TextRange, 203 | ): Int? { 204 | if (currentContent == null) { 205 | return null 206 | } 207 | // We need to account for dprint changing the length of the file as it formats. The assumption made is 208 | // that ranges do not overlap, so we can use the diff to the original content length to know where the 209 | // new range will shift after each range format. 210 | val rangeOffset = currentContent.length - originalContent.length 211 | val startOffset = range.startOffset + rangeOffset 212 | return when { 213 | startOffset > currentContent.length -> currentContent.length 214 | else -> startOffset 215 | } 216 | } 217 | 218 | private fun getEndOfRange( 219 | currentContent: String?, 220 | originalContent: String, 221 | range: TextRange, 222 | ): Int? { 223 | if (currentContent == null) { 224 | return null 225 | } 226 | // We need to account for dprint changing the length of the file as it formats. The assumption made is 227 | // that ranges do not overlap, so we can use the diff to the original content length to know where the 228 | // new range will shift after each range format. 229 | val rangeOffset = currentContent.length - originalContent.length 230 | val endOffset = range.endOffset + rangeOffset 231 | return when { 232 | endOffset < 0 -> 0 233 | else -> endOffset 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/i18n/DprintBundle.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.i18n 2 | 3 | import com.intellij.AbstractBundle 4 | import org.jetbrains.annotations.NonNls 5 | import org.jetbrains.annotations.PropertyKey 6 | 7 | @NonNls 8 | private const val DPRINT_BUNDLE = "messages.Bundle" 9 | 10 | object DprintBundle : AbstractBundle(DPRINT_BUNDLE) { 11 | @Suppress("SpreadOperator") 12 | @JvmStatic 13 | fun message( 14 | @PropertyKey(resourceBundle = DPRINT_BUNDLE) key: String, 15 | vararg params: Any, 16 | ) = getMessage(key, *params) 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/listeners/ConfigChangedAction.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.listeners 2 | 3 | import com.dprint.config.ProjectConfiguration 4 | import com.dprint.i18n.DprintBundle 5 | import com.dprint.services.editorservice.EditorServiceManager 6 | import com.dprint.utils.infoLogWithConsole 7 | import com.intellij.ide.actionsOnSave.impl.ActionsOnSaveFileDocumentManagerListener 8 | import com.intellij.openapi.components.service 9 | import com.intellij.openapi.diagnostic.logger 10 | import com.intellij.openapi.editor.Document 11 | import com.intellij.openapi.fileEditor.FileDocumentManager 12 | import com.intellij.openapi.project.Project 13 | 14 | private val LOGGER = logger() 15 | 16 | /** 17 | * This listener restarts the editor service if the config file is updated. 18 | */ 19 | class ConfigChangedAction : ActionsOnSaveFileDocumentManagerListener.ActionOnSave() { 20 | override fun isEnabledForProject(project: Project): Boolean { 21 | return project.service().state.enabled 22 | } 23 | 24 | override fun processDocuments( 25 | project: Project, 26 | documents: Array, 27 | ) { 28 | val editorServiceManager = project.service() 29 | val manager = FileDocumentManager.getInstance() 30 | for (document in documents) { 31 | manager.getFile(document)?.let { vfile -> 32 | if (vfile.path == editorServiceManager.getConfigPath()) { 33 | infoLogWithConsole(DprintBundle.message("config.changed.run"), project, LOGGER) 34 | editorServiceManager.restartEditorService() 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/listeners/FileOpenedListener.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.listeners 2 | 3 | import com.dprint.config.ProjectConfiguration 4 | import com.dprint.services.editorservice.EditorServiceManager 5 | import com.dprint.utils.isFormattableFile 6 | import com.intellij.openapi.components.service 7 | import com.intellij.openapi.fileEditor.FileEditorManager 8 | import com.intellij.openapi.fileEditor.FileEditorManagerListener 9 | import com.intellij.openapi.vfs.VirtualFile 10 | 11 | /** 12 | * This listener will fire a request to get the canFormat status of a file. The result will then be cached in the 13 | * EditorServiceManager so we don't need to wait for background threads in the main EDT thread. 14 | */ 15 | class FileOpenedListener : FileEditorManagerListener { 16 | override fun fileOpened( 17 | source: FileEditorManager, 18 | file: VirtualFile, 19 | ) { 20 | super.fileOpened(source, file) 21 | val projectConfig = source.project.service().state 22 | if (!projectConfig.enabled || 23 | !source.project.isOpen || 24 | !source.project.isInitialized || 25 | source.project.isDisposed 26 | ) { 27 | return 28 | } 29 | 30 | // We ignore scratch files as they are never part of config 31 | if (!isFormattableFile(source.project, file)) return 32 | 33 | val manager = source.project.service() 34 | manager.primeCanFormatCacheForFile(file) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/listeners/OnSaveAction.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.listeners 2 | 3 | import com.dprint.config.ProjectConfiguration 4 | import com.dprint.config.UserConfiguration 5 | import com.dprint.i18n.DprintBundle 6 | import com.dprint.services.FormatterService 7 | import com.dprint.utils.infoLogWithConsole 8 | import com.intellij.codeInsight.actions.ReformatCodeProcessor 9 | import com.intellij.ide.actionsOnSave.impl.ActionsOnSaveFileDocumentManagerListener 10 | import com.intellij.openapi.command.CommandProcessor 11 | import com.intellij.openapi.components.service 12 | import com.intellij.openapi.diagnostic.logger 13 | import com.intellij.openapi.editor.Document 14 | import com.intellij.openapi.fileEditor.FileDocumentManager 15 | import com.intellij.openapi.project.Project 16 | 17 | private val LOGGER = logger() 18 | 19 | /** 20 | * This listener sets up format on save functionality. 21 | */ 22 | class OnSaveAction : ActionsOnSaveFileDocumentManagerListener.ActionOnSave() { 23 | override fun isEnabledForProject(project: Project): Boolean { 24 | val projectConfig = project.service().state 25 | val userConfig = project.service().state 26 | return projectConfig.enabled && userConfig.runOnSave 27 | } 28 | 29 | override fun processDocuments( 30 | project: Project, 31 | documents: Array, 32 | ) { 33 | val currentCommandName = CommandProcessor.getInstance().currentCommandName 34 | if (currentCommandName == ReformatCodeProcessor.getCommandName()) { 35 | return 36 | } 37 | val formatterService = project.service() 38 | val manager = FileDocumentManager.getInstance() 39 | for (document in documents) { 40 | manager.getFile(document)?.let { vfile -> 41 | infoLogWithConsole(DprintBundle.message("save.action.run", vfile.path), project, LOGGER) 42 | formatterService.format(vfile, document) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/listeners/ProjectStartupListener.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.listeners 2 | 3 | import com.dprint.services.editorservice.EditorServiceManager 4 | import com.dprint.toolwindow.Console 5 | import com.intellij.openapi.components.service 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.openapi.startup.ProjectActivity 8 | 9 | class ProjectStartupListener : ProjectActivity { 10 | override suspend fun execute(project: Project) { 11 | val editorServiceManager = project.service() 12 | project.service() 13 | editorServiceManager.restartEditorService() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/messages/DprintAction.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.messages 2 | 3 | import com.dprint.utils.errorLogWithConsole 4 | import com.intellij.openapi.diagnostic.logger 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.util.messages.Topic 7 | 8 | interface DprintAction { 9 | companion object { 10 | val DPRINT_ACTION_TOPIC: Topic = Topic.create("dprint.action", DprintAction::class.java) 11 | 12 | private val LOGGER = logger() 13 | 14 | fun publishFormattingStarted( 15 | project: Project, 16 | filePath: String, 17 | ) { 18 | publishSafely(project) { 19 | project.messageBus.syncPublisher(DPRINT_ACTION_TOPIC) 20 | .formattingStarted(filePath) 21 | } 22 | } 23 | 24 | fun publishFormattingSucceeded( 25 | project: Project, 26 | filePath: String, 27 | timeElapsed: Long, 28 | ) { 29 | publishSafely(project) { 30 | project.messageBus.syncPublisher(DPRINT_ACTION_TOPIC) 31 | .formattingSucceeded(filePath, timeElapsed) 32 | } 33 | } 34 | 35 | fun publishFormattingFailed( 36 | project: Project, 37 | filePath: String, 38 | message: String?, 39 | ) { 40 | publishSafely(project) { 41 | project.messageBus.syncPublisher(DPRINT_ACTION_TOPIC).formattingFailed(filePath, message) 42 | } 43 | } 44 | 45 | private fun publishSafely( 46 | project: Project, 47 | runnable: () -> Unit, 48 | ) { 49 | try { 50 | runnable() 51 | } catch (e: Exception) { 52 | errorLogWithConsole("Failed to publish dprint action message", e, project, LOGGER) 53 | } 54 | } 55 | } 56 | 57 | fun formattingStarted(filePath: String) 58 | 59 | fun formattingSucceeded( 60 | filePath: String, 61 | timeElapsed: Long, 62 | ) 63 | 64 | fun formattingFailed( 65 | filePath: String, 66 | message: String?, 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/messages/DprintMessage.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.messages 2 | 3 | import com.intellij.util.messages.Topic 4 | 5 | /** 6 | * A message for the internal IntelliJ message bus that allows us to push logging information to a tool window 7 | */ 8 | interface DprintMessage { 9 | companion object { 10 | val DPRINT_MESSAGE_TOPIC = Topic("DPRINT_EVENT_MESSAGE", Listener::class.java) 11 | } 12 | 13 | interface Listener { 14 | fun info(message: String) 15 | 16 | fun warn(message: String) 17 | 18 | fun error(message: String) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/FormatterService.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services 2 | 3 | import com.dprint.i18n.DprintBundle 4 | import com.dprint.services.editorservice.EditorServiceManager 5 | import com.dprint.services.editorservice.FormatResult 6 | import com.dprint.utils.isFormattableFile 7 | import com.intellij.openapi.command.WriteCommandAction 8 | import com.intellij.openapi.components.Service 9 | import com.intellij.openapi.components.service 10 | import com.intellij.openapi.editor.Document 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.openapi.vfs.VirtualFile 13 | 14 | interface IFormatterService { 15 | /** 16 | * Attempts to format and save a Document using Dprint. 17 | */ 18 | fun format( 19 | virtualFile: VirtualFile, 20 | document: Document, 21 | ) 22 | } 23 | 24 | /** 25 | * A project service that handles reading virtual files, formatting their contents and writing the formatted result. 26 | */ 27 | @Service(Service.Level.PROJECT) 28 | class FormatterService(project: Project) : IFormatterService { 29 | private val impl = 30 | FormatterServiceImpl( 31 | project, 32 | project.service(), 33 | ) 34 | 35 | override fun format( 36 | virtualFile: VirtualFile, 37 | document: Document, 38 | ) { 39 | this.impl.format(virtualFile, document) 40 | } 41 | } 42 | 43 | class FormatterServiceImpl( 44 | private val project: Project, 45 | private val editorServiceManager: EditorServiceManager, 46 | ) : 47 | IFormatterService { 48 | override fun format( 49 | virtualFile: VirtualFile, 50 | document: Document, 51 | ) { 52 | val content = document.text 53 | val filePath = virtualFile.path 54 | if (content.isBlank() || !isFormattableFile(project, virtualFile)) return 55 | 56 | if (editorServiceManager.canFormatCached(filePath) == true) { 57 | val formatHandler: (FormatResult) -> Unit = { 58 | it.formattedContent?.let { 59 | WriteCommandAction.runWriteCommandAction(project) { 60 | document.setText(it) 61 | } 62 | } 63 | } 64 | 65 | editorServiceManager.format(filePath, content, formatHandler) 66 | } else { 67 | DprintBundle.message("formatting.cannot.format", filePath) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/EditorServiceManager.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice 2 | 3 | import com.dprint.config.ProjectConfiguration 4 | import com.dprint.i18n.DprintBundle 5 | import com.dprint.messages.DprintAction 6 | import com.dprint.messages.DprintMessage 7 | import com.dprint.services.editorservice.v4.EditorServiceV4 8 | import com.dprint.services.editorservice.v5.EditorServiceV5 9 | import com.dprint.utils.errorLogWithConsole 10 | import com.dprint.utils.getValidConfigPath 11 | import com.dprint.utils.getValidExecutablePath 12 | import com.dprint.utils.infoLogWithConsole 13 | import com.dprint.utils.isFormattableFile 14 | import com.dprint.utils.warnLogWithConsole 15 | import com.intellij.execution.configurations.GeneralCommandLine 16 | import com.intellij.execution.util.ExecUtil 17 | import com.intellij.notification.NotificationGroupManager 18 | import com.intellij.notification.NotificationType 19 | import com.intellij.openapi.components.Service 20 | import com.intellij.openapi.components.service 21 | import com.intellij.openapi.diagnostic.logger 22 | import com.intellij.openapi.fileEditor.FileEditorManager 23 | import com.intellij.openapi.project.Project 24 | import com.intellij.openapi.vfs.VirtualFile 25 | import kotlinx.serialization.json.Json 26 | import kotlinx.serialization.json.int 27 | import kotlinx.serialization.json.jsonObject 28 | import kotlinx.serialization.json.jsonPrimitive 29 | import org.apache.commons.collections4.map.LRUMap 30 | import java.io.File 31 | import kotlin.system.measureTimeMillis 32 | 33 | private val LOGGER = logger() 34 | private const val SCHEMA_V4 = 4 35 | private const val SCHEMA_V5 = 5 36 | 37 | @Service(Service.Level.PROJECT) 38 | class EditorServiceManager(private val project: Project) { 39 | private var editorService: IEditorService? = null 40 | private var configPath: String? = null 41 | private val editorServiceTaskQueue = EditorServiceTaskQueue(project) 42 | private var canFormatCache = LRUMap() 43 | 44 | private var hasAttemptedInitialisation = false 45 | 46 | private fun getSchemaVersion(configPath: String?): Int? { 47 | val executablePath = getValidExecutablePath(project) 48 | val timeout = project.service().state.initialisationTimeout 49 | 50 | val commandLine = 51 | GeneralCommandLine( 52 | executablePath, 53 | "editor-info", 54 | ) 55 | configPath?.let { 56 | val workingDir = File(it).parent 57 | commandLine.withWorkDirectory(workingDir) 58 | } 59 | 60 | val result = ExecUtil.execAndGetOutput(commandLine, timeout.toInt()) 61 | 62 | return try { 63 | val jsonText = result.stdout 64 | infoLogWithConsole(DprintBundle.message("config.dprint.editor.info", jsonText), project, LOGGER) 65 | Json.parseToJsonElement(jsonText).jsonObject["schemaVersion"]?.jsonPrimitive?.int 66 | } catch (e: RuntimeException) { 67 | val stdout = result.stdout.trim() 68 | val stderr = result.stderr.trim() 69 | val message = 70 | when { 71 | stdout.isEmpty() && stderr.isNotEmpty() -> 72 | DprintBundle.message( 73 | "error.failed.to.parse.json.schema.error", 74 | result.stderr.trim(), 75 | ) 76 | 77 | stdout.isNotEmpty() && stderr.isEmpty() -> 78 | DprintBundle.message( 79 | "error.failed.to.parse.json.schema.received", 80 | result.stdout.trim(), 81 | ) 82 | 83 | stdout.isNotEmpty() && stderr.isNotEmpty() -> 84 | DprintBundle.message( 85 | "error.failed.to.parse.json.schema.received.error", 86 | result.stdout.trim(), 87 | result.stderr.trim(), 88 | ) 89 | 90 | else -> DprintBundle.message("error.failed.to.parse.json.schema") 91 | } 92 | errorLogWithConsole( 93 | message, 94 | project, 95 | LOGGER, 96 | ) 97 | throw e 98 | } 99 | } 100 | 101 | /** 102 | * Gets a cached canFormat result. If a result doesn't exist this will return null and start a request to fill the 103 | * value in the cache. 104 | */ 105 | fun canFormatCached(path: String): Boolean? { 106 | val result = canFormatCache[path] 107 | 108 | if (result == null) { 109 | warnLogWithConsole( 110 | DprintBundle.message("editor.service.manager.no.cached.can.format", path), 111 | project, 112 | LOGGER, 113 | ) 114 | primeCanFormatCache(path) 115 | } 116 | 117 | return result 118 | } 119 | 120 | /** 121 | * Primes the canFormat result cache for the passed in virtual file. 122 | */ 123 | fun primeCanFormatCacheForFile(virtualFile: VirtualFile) { 124 | // Mainly used for project startup. The file opened listener runs before the ProjectStartUp listener 125 | if (!hasAttemptedInitialisation) { 126 | return 127 | } 128 | primeCanFormatCache(virtualFile.path) 129 | } 130 | 131 | private fun primeCanFormatCache(path: String) { 132 | val timeout = project.service().state.commandTimeout 133 | editorServiceTaskQueue.createTaskWithTimeout( 134 | TaskInfo(TaskType.PrimeCanFormat, path, null), 135 | DprintBundle.message("editor.service.manager.priming.can.format.cache", path), 136 | { 137 | if (editorService == null) { 138 | warnLogWithConsole(DprintBundle.message("editor.service.manager.not.initialised"), project, LOGGER) 139 | } 140 | 141 | editorService?.canFormat(path) { canFormat -> 142 | if (canFormat == null) { 143 | infoLogWithConsole("Unable to determine if $path can be formatted.", project, LOGGER) 144 | } else { 145 | canFormatCache[path] = canFormat 146 | infoLogWithConsole("$path ${if (canFormat) "can" else "cannot"} be formatted", project, LOGGER) 147 | } 148 | } 149 | }, 150 | timeout, 151 | ) 152 | } 153 | 154 | /** 155 | * Formats the given file in a background thread and runs the onFinished callback once complete. 156 | * See [com.dprint.services.editorservice.IEditorService.fmt] for more info on the parameters. 157 | */ 158 | fun format( 159 | path: String, 160 | content: String, 161 | onFinished: (FormatResult) -> Unit, 162 | ) { 163 | format(editorService?.maybeGetFormatId(), path, content, null, null, onFinished) 164 | } 165 | 166 | /** 167 | * Formats the given file in a background thread and runs the onFinished callback once complete. 168 | * See [com.dprint.services.editorservice.IEditorService.fmt] for more info on the parameters. 169 | */ 170 | fun format( 171 | formatId: Int?, 172 | path: String, 173 | content: String, 174 | startIndex: Int?, 175 | endIndex: Int?, 176 | onFinished: (FormatResult) -> Unit, 177 | ) { 178 | val timeout = project.service().state.commandTimeout 179 | editorServiceTaskQueue.createTaskWithTimeout( 180 | TaskInfo(TaskType.Format, path, formatId), 181 | DprintBundle.message("editor.service.manager.creating.formatting.task", path), 182 | { 183 | if (editorService == null) { 184 | DprintAction.publishFormattingFailed(project, path, "Editor service is not initialised.") 185 | warnLogWithConsole(DprintBundle.message("editor.service.manager.not.initialised"), project, LOGGER) 186 | } 187 | DprintAction.publishFormattingStarted(project, path) 188 | val timeMs = 189 | measureTimeMillis { editorService?.fmt(formatId, path, content, startIndex, endIndex, onFinished) } 190 | DprintAction.publishFormattingSucceeded(project, path, timeMs) 191 | }, 192 | timeout, 193 | { 194 | onFinished(FormatResult()) 195 | DprintAction.publishFormattingFailed(project, path, it.message) 196 | }, 197 | ) 198 | } 199 | 200 | private fun initialiseFreshEditorService(): Boolean { 201 | hasAttemptedInitialisation = true 202 | configPath = getValidConfigPath(project) 203 | val schemaVersion = getSchemaVersion(configPath) 204 | infoLogWithConsole( 205 | DprintBundle.message( 206 | "editor.service.manager.received.schema.version", 207 | schemaVersion ?: "none", 208 | ), 209 | project, 210 | LOGGER, 211 | ) 212 | when { 213 | schemaVersion == null -> 214 | project.messageBus.syncPublisher(DprintMessage.DPRINT_MESSAGE_TOPIC).info( 215 | DprintBundle.message("config.dprint.schemaVersion.not.found"), 216 | ) 217 | 218 | schemaVersion < SCHEMA_V4 -> 219 | project.messageBus.syncPublisher(DprintMessage.DPRINT_MESSAGE_TOPIC) 220 | .info( 221 | DprintBundle.message("config.dprint.schemaVersion.older"), 222 | ) 223 | 224 | schemaVersion == SCHEMA_V4 -> editorService = project.service() 225 | schemaVersion == SCHEMA_V5 -> editorService = project.service() 226 | schemaVersion > SCHEMA_V5 -> 227 | infoLogWithConsole( 228 | DprintBundle.message("config.dprint.schemaVersion.newer"), 229 | project, 230 | LOGGER, 231 | ) 232 | } 233 | 234 | if (editorService == null) { 235 | return false 236 | } 237 | 238 | editorService?.initialiseEditorService() 239 | return true 240 | } 241 | 242 | fun restartEditorService() { 243 | if (!project.service().state.enabled) { 244 | return 245 | } 246 | 247 | // Slightly larger incase the json schema step times out 248 | val timeout = project.service().state.initialisationTimeout 249 | editorServiceTaskQueue.createTaskWithTimeout( 250 | TaskInfo(TaskType.Initialisation, null, null), 251 | DprintBundle.message("editor.service.manager.initialising.editor.service"), 252 | { 253 | clearCanFormatCache() 254 | val initialised = initialiseFreshEditorService() 255 | if (initialised) { 256 | for (virtualFile in FileEditorManager.getInstance(project).openFiles) { 257 | if (isFormattableFile(project, virtualFile)) { 258 | primeCanFormatCacheForFile(virtualFile) 259 | } 260 | } 261 | } 262 | }, 263 | timeout, 264 | { 265 | this.notifyFailedToStart() 266 | }, 267 | ) 268 | } 269 | 270 | private fun notifyFailedToStart() { 271 | NotificationGroupManager 272 | .getInstance() 273 | .getNotificationGroup("Dprint") 274 | .createNotification( 275 | DprintBundle.message( 276 | "editor.service.manager.initialising.editor.service.failed.title", 277 | ), 278 | DprintBundle.message( 279 | "editor.service.manager.initialising.editor.service.failed.content", 280 | ), 281 | NotificationType.ERROR, 282 | ) 283 | .notify(project) 284 | } 285 | 286 | fun destroyEditorService() { 287 | editorService?.destroyEditorService() 288 | } 289 | 290 | fun getConfigPath(): String? { 291 | return configPath 292 | } 293 | 294 | fun canRangeFormat(): Boolean { 295 | return editorService?.canRangeFormat() == true 296 | } 297 | 298 | fun maybeGetFormatId(): Int? { 299 | return editorService?.maybeGetFormatId() 300 | } 301 | 302 | fun cancelFormat(formatId: Int) { 303 | val timeout = project.service().state.commandTimeout 304 | editorServiceTaskQueue.createTaskWithTimeout( 305 | TaskInfo(TaskType.Cancel, null, formatId), 306 | "Cancelling format $formatId", 307 | { editorService?.cancelFormat(formatId) }, 308 | timeout, 309 | ) 310 | } 311 | 312 | fun canCancelFormat(): Boolean { 313 | return editorService?.canCancelFormat() == true 314 | } 315 | 316 | fun clearCanFormatCache() { 317 | canFormatCache.clear() 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/EditorServiceTaskQueue.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice 2 | 3 | import com.dprint.i18n.DprintBundle 4 | import com.dprint.services.editorservice.exceptions.ProcessUnavailableException 5 | import com.dprint.utils.errorLogWithConsole 6 | import com.dprint.utils.infoLogWithConsole 7 | import com.dprint.utils.warnLogWithConsole 8 | import com.intellij.openapi.diagnostic.logger 9 | import com.intellij.openapi.progress.BackgroundTaskQueue 10 | import com.intellij.openapi.progress.ProgressIndicator 11 | import com.intellij.openapi.progress.Task 12 | import com.intellij.openapi.project.Project 13 | import java.lang.Exception 14 | import java.util.concurrent.CancellationException 15 | import java.util.concurrent.CompletableFuture 16 | import java.util.concurrent.ExecutionException 17 | import java.util.concurrent.TimeUnit 18 | import java.util.concurrent.TimeoutException 19 | 20 | enum class TaskType { 21 | Initialisation, 22 | PrimeCanFormat, 23 | Format, 24 | Cancel, 25 | } 26 | 27 | data class TaskInfo(val taskType: TaskType, val path: String?, val formatId: Int?) 28 | 29 | private val LOGGER = logger() 30 | 31 | class EditorServiceTaskQueue(private val project: Project) { 32 | private val taskQueue = BackgroundTaskQueue(project, "Dprint manager task queue") 33 | private val activeTasks = HashSet() 34 | 35 | fun createTaskWithTimeout( 36 | taskInfo: TaskInfo, 37 | title: String, 38 | operation: () -> Unit, 39 | timeout: Long, 40 | ) { 41 | createTaskWithTimeout(taskInfo, title, operation, timeout, null) 42 | } 43 | 44 | fun createTaskWithTimeout( 45 | taskInfo: TaskInfo, 46 | title: String, 47 | operation: () -> Unit, 48 | timeout: Long, 49 | onFailure: ((Throwable) -> Unit)?, 50 | ) { 51 | if (activeTasks.contains(taskInfo)) { 52 | infoLogWithConsole("Task is already queued so this will be dropped: $taskInfo", project, LOGGER) 53 | return 54 | } 55 | activeTasks.add(taskInfo) 56 | val task = 57 | object : Task.Backgroundable(project, title, true) { 58 | val future = CompletableFuture() 59 | 60 | override fun run(indicator: ProgressIndicator) { 61 | indicator.text = title 62 | infoLogWithConsole(indicator.text, project, LOGGER) 63 | try { 64 | future.completeAsync(operation) 65 | future.get(timeout, TimeUnit.MILLISECONDS) 66 | } catch (e: TimeoutException) { 67 | onFailure?.invoke(e) 68 | errorLogWithConsole("Dprint timeout: $title", e, project, LOGGER) 69 | } catch (e: ExecutionException) { 70 | onFailure?.invoke(e) 71 | if (e.cause is ProcessUnavailableException) { 72 | warnLogWithConsole( 73 | DprintBundle.message("editor.service.process.is.dead"), 74 | e.cause, 75 | project, 76 | LOGGER, 77 | ) 78 | } 79 | errorLogWithConsole("Dprint execution exception: $title", e, project, LOGGER) 80 | } catch (e: InterruptedException) { 81 | onFailure?.invoke(e) 82 | errorLogWithConsole("Dprint interruption: $title", e, project, LOGGER) 83 | } catch (e: CancellationException) { 84 | onFailure?.invoke(e) 85 | errorLogWithConsole("Dprint cancellation: $title", e, project, LOGGER) 86 | } catch (e: Exception) { 87 | onFailure?.invoke(e) 88 | activeTasks.remove(taskInfo) 89 | throw e 90 | } finally { 91 | activeTasks.remove(taskInfo) 92 | } 93 | } 94 | 95 | override fun onCancel() { 96 | super.onCancel() 97 | future.cancel(true) 98 | } 99 | } 100 | taskQueue.run(task) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/FormatResult.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice 2 | 3 | /** 4 | * The resulting state of running the Dprint formatter. 5 | * 6 | * If both parameters are null, it represents a no-op from the format operation. 7 | */ 8 | data class FormatResult(val formattedContent: String? = null, val error: String? = null) 9 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/IEditorService.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice 2 | 3 | import com.dprint.services.editorservice.exceptions.HandlerNotImplementedException 4 | import com.intellij.openapi.Disposable 5 | 6 | interface IEditorService : Disposable { 7 | /** 8 | * If enabled, initialises the dprint editor-service so it is ready to format 9 | */ 10 | fun initialiseEditorService() 11 | 12 | /** 13 | * Shuts down the editor service and destroys the process. 14 | */ 15 | fun destroyEditorService() 16 | 17 | /** 18 | * Returns whether dprint can format the given file path based on the config used in the editor service. 19 | */ 20 | fun canFormat( 21 | filePath: String, 22 | onFinished: (Boolean?) -> Unit, 23 | ) 24 | 25 | fun canRangeFormat(): Boolean 26 | 27 | /** 28 | * This runs dprint using the editor service with the supplied file path and content as stdin. 29 | * @param filePath The path of the file being formatted. This is needed so the correct dprint configuration file 30 | * located. 31 | * @param content The content of the file as a string. This is formatted via Dprint and returned via the result. 32 | * @param onFinished A callback that is called when the formatting job has finished. The only param to this callback 33 | * will be the result of the formatting job. The class providing this should handle timeouts themselves. 34 | * @return A result object containing the formatted content is successful or an error. 35 | */ 36 | fun fmt( 37 | filePath: String, 38 | content: String, 39 | onFinished: (FormatResult) -> Unit, 40 | ): Int? { 41 | return fmt(maybeGetFormatId(), filePath, content, null, null, onFinished) 42 | } 43 | 44 | /** 45 | * This runs dprint using the editor service with the supplied file path and content as stdin. 46 | * @param formatId The id of the message that is passed to the underlying editor service. This is exposed at this 47 | * level, so we can cancel requests if need be. 48 | * @param filePath The path of the file being formatted. This is needed so the correct dprint configuration file 49 | * located. 50 | * @param content The content of the file as a string. This is formatted via Dprint and returned via the result. 51 | * @param startIndex The starting index of a range format, null if the format is not for a range in the file. 52 | * @param endIndex The ending index of a range format, null if the format is not for a range in the file. 53 | * @param onFinished A callback that is called when the formatting job has finished. The only param to this callback 54 | * will be the result of the formatting job. The class providing this should handle timeouts themselves. 55 | * @return A result object containing the formatted content is successful or an error. 56 | */ 57 | fun fmt( 58 | formatId: Int?, 59 | filePath: String, 60 | content: String, 61 | startIndex: Int?, 62 | endIndex: Int?, 63 | onFinished: (FormatResult) -> Unit, 64 | ): Int? 65 | 66 | /** 67 | * Whether the editor service implementation supports cancellation of formats. 68 | */ 69 | fun canCancelFormat(): Boolean 70 | 71 | /** 72 | * Gets a formatting message id if the editor service supports messages with id's, this starts at schema version 5. 73 | */ 74 | fun maybeGetFormatId(): Int? 75 | 76 | /** 77 | * Cancels the format for a given id if the service supports it. Will throw HandlerNotImplementedException if the 78 | * service doesn't. 79 | */ 80 | fun cancelFormat(formatId: Int) { 81 | throw HandlerNotImplementedException("Cancel format has not been implemented") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/exceptions/HandlerNotImplementedException.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.exceptions 2 | 3 | class HandlerNotImplementedException(message: String) : Exception(message) 4 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/exceptions/ProcessUnavailableException.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.exceptions 2 | 3 | class ProcessUnavailableException(message: String) : Exception(message) 4 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/exceptions/UnsupportedMessagePartException.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.exceptions 2 | 3 | class UnsupportedMessagePartException(message: String) : Exception(message) 4 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/process/EditorProcess.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.process 2 | 3 | import com.dprint.config.UserConfiguration 4 | import com.dprint.i18n.DprintBundle 5 | import com.dprint.messages.DprintMessage 6 | import com.dprint.services.editorservice.exceptions.ProcessUnavailableException 7 | import com.dprint.utils.getValidConfigPath 8 | import com.dprint.utils.getValidExecutablePath 9 | import com.dprint.utils.infoLogWithConsole 10 | import com.intellij.execution.configurations.GeneralCommandLine 11 | import com.intellij.openapi.diagnostic.logger 12 | import com.intellij.openapi.project.Project 13 | import java.io.File 14 | import java.nio.ByteBuffer 15 | 16 | private const val BUFFER_SIZE = 1024 17 | private const val ZERO = 0 18 | private const val U32_BYTE_SIZE = 4 19 | 20 | private val LOGGER = logger() 21 | 22 | // Dprint uses unsigned bytes of 4x255 for the success message and that translates 23 | // to 4x-1 in the jvm's signed bytes. 24 | private val SUCCESS_MESSAGE = byteArrayOf(-1, -1, -1, -1) 25 | 26 | class EditorProcess(private val project: Project, private val userConfiguration: UserConfiguration) { 27 | private var process: Process? = null 28 | private var stderrListener: StdErrListener? = null 29 | 30 | fun initialize() { 31 | val executablePath = getValidExecutablePath(project) 32 | val configPath = getValidConfigPath(project) 33 | 34 | if (this.process != null) { 35 | destroy() 36 | } 37 | 38 | when { 39 | configPath.isNullOrBlank() -> { 40 | project.messageBus.syncPublisher(DprintMessage.DPRINT_MESSAGE_TOPIC) 41 | .info(DprintBundle.message("error.config.path")) 42 | } 43 | 44 | executablePath.isNullOrBlank() -> { 45 | project.messageBus.syncPublisher(DprintMessage.DPRINT_MESSAGE_TOPIC) 46 | .info(DprintBundle.message("error.executable.path")) 47 | } 48 | 49 | else -> { 50 | process = createEditorService(executablePath, configPath) 51 | process?.let { actualProcess -> 52 | actualProcess.onExit().thenApply { 53 | destroy() 54 | } 55 | createStderrListener(actualProcess) 56 | } 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * Shuts down the editor service and destroys the process. 63 | */ 64 | fun destroy() { 65 | stderrListener?.dispose() 66 | process?.destroy() 67 | process = null 68 | } 69 | 70 | private fun createStderrListener(actualProcess: Process): StdErrListener { 71 | val stdErrListener = StdErrListener(project, actualProcess) 72 | stdErrListener.listen() 73 | return stdErrListener 74 | } 75 | 76 | private fun createEditorService( 77 | executablePath: String, 78 | configPath: String, 79 | ): Process { 80 | val ijPid = ProcessHandle.current().pid() 81 | 82 | val args = 83 | mutableListOf( 84 | executablePath, 85 | "editor-service", 86 | "--config", 87 | configPath, 88 | "--parent-pid", 89 | ijPid.toString(), 90 | ) 91 | 92 | if (userConfiguration.state.enableEditorServiceVerboseLogging) args.add("--verbose") 93 | 94 | val commandLine = GeneralCommandLine(args) 95 | val workingDir = File(configPath).parent 96 | 97 | when { 98 | workingDir != null -> { 99 | commandLine.withWorkDirectory(workingDir) 100 | infoLogWithConsole( 101 | DprintBundle.message("editor.service.starting", executablePath, configPath, workingDir), 102 | project, 103 | LOGGER, 104 | ) 105 | } 106 | 107 | else -> 108 | infoLogWithConsole( 109 | DprintBundle.message("editor.service.starting.working.dir", executablePath, configPath), 110 | project, 111 | LOGGER, 112 | ) 113 | } 114 | 115 | val rtnProcess = commandLine.createProcess() 116 | rtnProcess.onExit().thenApply { exitedProcess -> 117 | infoLogWithConsole( 118 | DprintBundle.message("process.shut.down", exitedProcess.pid()), 119 | project, 120 | LOGGER, 121 | ) 122 | } 123 | return rtnProcess 124 | } 125 | 126 | private fun getProcess(): Process { 127 | val boundProcess = process 128 | 129 | if (boundProcess?.isAlive == true) { 130 | return boundProcess 131 | } 132 | throw ProcessUnavailableException( 133 | DprintBundle.message( 134 | "editor.process.cannot.get.editor.service.process", 135 | ), 136 | ) 137 | } 138 | 139 | fun writeSuccess() { 140 | LOGGER.debug(DprintBundle.message("formatting.sending.success.to.editor.service")) 141 | val stdin = getProcess().outputStream 142 | stdin.write(SUCCESS_MESSAGE) 143 | stdin.flush() 144 | } 145 | 146 | fun writeInt(i: Int) { 147 | val stdin = getProcess().outputStream 148 | 149 | LOGGER.debug(DprintBundle.message("formatting.sending.to.editor.service", i)) 150 | 151 | val buffer = ByteBuffer.allocate(U32_BYTE_SIZE) 152 | buffer.putInt(i) 153 | stdin.write(buffer.array()) 154 | stdin.flush() 155 | } 156 | 157 | fun writeString(string: String) { 158 | val stdin = getProcess().outputStream 159 | val byteArray = string.encodeToByteArray() 160 | var pointer = 0 161 | 162 | writeInt(byteArray.size) 163 | stdin.flush() 164 | 165 | while (pointer < byteArray.size) { 166 | if (pointer != 0) { 167 | readInt() 168 | } 169 | val end = if (byteArray.size - pointer < BUFFER_SIZE) byteArray.size else pointer + BUFFER_SIZE 170 | val range = IntRange(pointer, end - 1) 171 | val chunk = byteArray.slice(range).toByteArray() 172 | stdin.write(chunk) 173 | stdin.flush() 174 | pointer = end 175 | } 176 | } 177 | 178 | fun readAndAssertSuccess() { 179 | val stdout = process?.inputStream 180 | if (stdout != null) { 181 | val bytes = stdout.readNBytes(U32_BYTE_SIZE) 182 | for (i in 0 until U32_BYTE_SIZE) { 183 | assert(bytes[i] == SUCCESS_MESSAGE[i]) 184 | } 185 | LOGGER.debug(DprintBundle.message("formatting.received.success")) 186 | } else { 187 | LOGGER.debug(DprintBundle.message("editor.process.cannot.get.editor.service.process")) 188 | initialize() 189 | } 190 | } 191 | 192 | fun readInt(): Int { 193 | val stdout = getProcess().inputStream 194 | val result = ByteBuffer.wrap(stdout.readNBytes(U32_BYTE_SIZE)).int 195 | LOGGER.debug(DprintBundle.message("formatting.received.value", result)) 196 | return result 197 | } 198 | 199 | fun readString(): String { 200 | val stdout = getProcess().inputStream 201 | val totalBytes = readInt() 202 | var result = ByteArray(0) 203 | 204 | var index = 0 205 | 206 | while (index < totalBytes) { 207 | if (index != 0) { 208 | writeInt(ZERO) 209 | } 210 | 211 | val numBytes = if (totalBytes - index < BUFFER_SIZE) totalBytes - index else BUFFER_SIZE 212 | val bytes = stdout.readNBytes(numBytes) 213 | result += bytes 214 | index += numBytes 215 | } 216 | 217 | val decodedResult = result.decodeToString() 218 | LOGGER.debug(DprintBundle.message("formatting.received.value", decodedResult)) 219 | return decodedResult 220 | } 221 | 222 | fun writeBuffer(byteArray: ByteArray) { 223 | val stdin = getProcess().outputStream 224 | stdin.write(byteArray) 225 | stdin.flush() 226 | } 227 | 228 | fun readBuffer(totalBytes: Int): ByteArray { 229 | val stdout = getProcess().inputStream 230 | return stdout.readNBytes(totalBytes) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/process/StdErrListener.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.process 2 | 3 | import com.dprint.utils.errorLogWithConsole 4 | import com.intellij.openapi.diagnostic.logger 5 | import com.intellij.openapi.project.Project 6 | import java.nio.BufferUnderflowException 7 | import kotlin.concurrent.thread 8 | 9 | private val LOGGER = logger() 10 | 11 | class StdErrListener(private val project: Project, private val process: Process) { 12 | private var listenerThread: Thread? = null 13 | private var disposing = false 14 | 15 | fun listen() { 16 | disposing = false 17 | listenerThread = 18 | thread(start = true) { 19 | while (true) { 20 | if (Thread.interrupted()) { 21 | return@thread 22 | } 23 | 24 | try { 25 | process.errorStream?.bufferedReader()?.readLine()?.let { error -> 26 | errorLogWithConsole("Dprint daemon ${process.pid()}: $error", project, LOGGER) 27 | } 28 | } catch (e: InterruptedException) { 29 | if (!disposing) LOGGER.info(e) 30 | return@thread 31 | } catch (e: BufferUnderflowException) { 32 | // Happens when the editor service is shut down while this thread is waiting to read output 33 | if (!disposing) LOGGER.info(e) 34 | return@thread 35 | } catch (e: Exception) { 36 | if (!disposing) LOGGER.info(e) 37 | return@thread 38 | } 39 | } 40 | } 41 | } 42 | 43 | fun dispose() { 44 | disposing = true 45 | listenerThread?.interrupt() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/v4/EditorServiceV4.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.v4 2 | 3 | import com.dprint.config.ProjectConfiguration 4 | import com.dprint.config.UserConfiguration 5 | import com.dprint.i18n.DprintBundle 6 | import com.dprint.services.editorservice.FormatResult 7 | import com.dprint.services.editorservice.IEditorService 8 | import com.dprint.services.editorservice.process.EditorProcess 9 | import com.dprint.utils.infoLogWithConsole 10 | import com.dprint.utils.warnLogWithConsole 11 | import com.intellij.openapi.components.Service 12 | import com.intellij.openapi.components.service 13 | import com.intellij.openapi.diagnostic.logger 14 | import com.intellij.openapi.project.Project 15 | 16 | private const val CHECK_COMMAND = 1 17 | private const val FORMAT_COMMAND = 2 18 | 19 | private val LOGGER = logger() 20 | 21 | @Service(Service.Level.PROJECT) 22 | class EditorServiceV4(private val project: Project) : IEditorService { 23 | private var editorProcess = EditorProcess(project, project.service()) 24 | 25 | override fun initialiseEditorService() { 26 | // If not enabled we don't start the editor service 27 | if (!project.service().state.enabled) return 28 | editorProcess.initialize() 29 | infoLogWithConsole( 30 | DprintBundle.message("editor.service.initialize", getName()), 31 | project, 32 | LOGGER, 33 | ) 34 | } 35 | 36 | override fun dispose() { 37 | destroyEditorService() 38 | } 39 | 40 | override fun destroyEditorService() { 41 | infoLogWithConsole(DprintBundle.message("editor.service.destroy", getName()), project, LOGGER) 42 | editorProcess.destroy() 43 | } 44 | 45 | override fun canFormat( 46 | filePath: String, 47 | onFinished: (Boolean?) -> Unit, 48 | ) { 49 | infoLogWithConsole(DprintBundle.message("formatting.checking.can.format", filePath), project, LOGGER) 50 | 51 | editorProcess.writeInt(CHECK_COMMAND) 52 | editorProcess.writeString(filePath) 53 | editorProcess.writeSuccess() 54 | 55 | // https://github.com/dprint/dprint/blob/main/docs/editor-extension-development.md 56 | // this command sequence returns 1 if the file can be formatted 57 | val status: Int = editorProcess.readInt() 58 | editorProcess.readAndAssertSuccess() 59 | 60 | val result = status == 1 61 | when (result) { 62 | true -> infoLogWithConsole(DprintBundle.message("formatting.can.format", filePath), project, LOGGER) 63 | false -> infoLogWithConsole(DprintBundle.message("formatting.cannot.format", filePath), project, LOGGER) 64 | } 65 | onFinished(result) 66 | } 67 | 68 | override fun canRangeFormat(): Boolean { 69 | return false 70 | } 71 | 72 | override fun fmt( 73 | filePath: String, 74 | content: String, 75 | onFinished: (FormatResult) -> Unit, 76 | ): Int? { 77 | var result = FormatResult() 78 | 79 | infoLogWithConsole(DprintBundle.message("formatting.file", filePath), project, LOGGER) 80 | editorProcess.writeInt(FORMAT_COMMAND) 81 | editorProcess.writeString(filePath) 82 | editorProcess.writeString(content) 83 | editorProcess.writeSuccess() 84 | 85 | when (editorProcess.readInt()) { 86 | 0 -> { 87 | infoLogWithConsole( 88 | DprintBundle.message("editor.service.format.not.needed", filePath), 89 | project, 90 | LOGGER, 91 | ) 92 | } // no-op as content didn't change 93 | 1 -> { 94 | result = FormatResult(formattedContent = editorProcess.readString()) 95 | infoLogWithConsole( 96 | DprintBundle.message("editor.service.format.succeeded", filePath), 97 | project, 98 | LOGGER, 99 | ) 100 | } 101 | 102 | 2 -> { 103 | val error = editorProcess.readString() 104 | result = FormatResult(error = error) 105 | warnLogWithConsole( 106 | DprintBundle.message("editor.service.format.failed", filePath, error), 107 | project, 108 | LOGGER, 109 | ) 110 | } 111 | } 112 | 113 | editorProcess.readAndAssertSuccess() 114 | 115 | onFinished(result) 116 | 117 | // We cannot cancel in V4 so return null 118 | return null 119 | } 120 | 121 | override fun fmt( 122 | formatId: Int?, 123 | filePath: String, 124 | content: String, 125 | startIndex: Int?, 126 | endIndex: Int?, 127 | onFinished: (FormatResult) -> Unit, 128 | ): Int? { 129 | return fmt(filePath, content, onFinished) 130 | } 131 | 132 | override fun canCancelFormat(): Boolean { 133 | return false 134 | } 135 | 136 | override fun maybeGetFormatId(): Int? { 137 | return null 138 | } 139 | 140 | private fun getName(): String { 141 | return this::class.java.simpleName 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/v5/EditorServiceV5.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.v5 2 | 3 | import com.dprint.config.UserConfiguration 4 | import com.dprint.i18n.DprintBundle 5 | import com.dprint.services.editorservice.FormatResult 6 | import com.dprint.services.editorservice.IEditorService 7 | import com.dprint.services.editorservice.process.EditorProcess 8 | import com.dprint.utils.errorLogWithConsole 9 | import com.dprint.utils.infoLogWithConsole 10 | import com.dprint.utils.warnLogWithConsole 11 | import com.intellij.openapi.components.Service 12 | import com.intellij.openapi.components.service 13 | import com.intellij.openapi.diagnostic.logger 14 | import com.intellij.openapi.project.Project 15 | import kotlinx.coroutines.TimeoutCancellationException 16 | import kotlinx.coroutines.launch 17 | import kotlinx.coroutines.runBlocking 18 | import kotlinx.coroutines.withTimeout 19 | 20 | private val LOGGER = logger() 21 | private const val SHUTDOWN_TIMEOUT = 1000L 22 | 23 | @Service(Service.Level.PROJECT) 24 | class EditorServiceV5(val project: Project) : IEditorService { 25 | private val impl = 26 | EditorServiceV5Impl(project, EditorProcess(project, project.service()), PendingMessages()) 27 | 28 | override fun initialiseEditorService() { 29 | impl.initialiseEditorService() 30 | } 31 | 32 | override fun destroyEditorService() { 33 | impl.destroyEditorService() 34 | } 35 | 36 | override fun canFormat( 37 | filePath: String, 38 | onFinished: (Boolean?) -> Unit, 39 | ) { 40 | impl.canFormat(filePath, onFinished) 41 | } 42 | 43 | override fun canRangeFormat(): Boolean { 44 | return impl.canRangeFormat() 45 | } 46 | 47 | override fun fmt( 48 | formatId: Int?, 49 | filePath: String, 50 | content: String, 51 | startIndex: Int?, 52 | endIndex: Int?, 53 | onFinished: (FormatResult) -> Unit, 54 | ): Int { 55 | return impl.fmt(formatId, filePath, content, startIndex, endIndex, onFinished) 56 | } 57 | 58 | override fun canCancelFormat(): Boolean { 59 | return impl.canCancelFormat() 60 | } 61 | 62 | override fun maybeGetFormatId(): Int { 63 | return impl.maybeGetFormatId() 64 | } 65 | 66 | override fun dispose() { 67 | impl.dispose() 68 | } 69 | } 70 | 71 | class EditorServiceV5Impl( 72 | private val project: Project, 73 | private val editorProcess: EditorProcess, 74 | private val pendingMessages: PendingMessages, 75 | ) : IEditorService { 76 | private var stdoutListener: StdoutListener? = null 77 | 78 | private fun createStdoutListener(): StdoutListener { 79 | val stdoutListener = StdoutListener(editorProcess, pendingMessages) 80 | stdoutListener.listen() 81 | return stdoutListener 82 | } 83 | 84 | override fun initialiseEditorService() { 85 | infoLogWithConsole( 86 | DprintBundle.message("editor.service.initialize", getName()), 87 | project, 88 | LOGGER, 89 | ) 90 | dropMessages() 91 | if (stdoutListener != null) { 92 | stdoutListener?.dispose() 93 | stdoutListener = null 94 | } 95 | 96 | editorProcess.initialize() 97 | stdoutListener = createStdoutListener() 98 | } 99 | 100 | override fun dispose() { 101 | destroyEditorService() 102 | } 103 | 104 | override fun destroyEditorService() { 105 | infoLogWithConsole(DprintBundle.message("editor.service.destroy", getName()), project, LOGGER) 106 | val message = createNewMessage(MessageType.ShutDownProcess) 107 | stdoutListener?.disposing = true 108 | try { 109 | runBlocking { 110 | withTimeout(SHUTDOWN_TIMEOUT) { 111 | launch { 112 | editorProcess.writeBuffer(message.build()) 113 | } 114 | } 115 | } 116 | } catch (e: TimeoutCancellationException) { 117 | errorLogWithConsole(DprintBundle.message("editor.service.shutting.down.timed.out"), e, project, LOGGER) 118 | } finally { 119 | stdoutListener?.dispose() 120 | dropMessages() 121 | editorProcess.destroy() 122 | } 123 | } 124 | 125 | override fun canFormat( 126 | filePath: String, 127 | onFinished: (Boolean?) -> Unit, 128 | ) { 129 | handleStaleMessages() 130 | 131 | infoLogWithConsole(DprintBundle.message("formatting.checking.can.format", filePath), project, LOGGER) 132 | val message = createNewMessage(MessageType.CanFormat) 133 | message.addString(filePath) 134 | 135 | val handler: (PendingMessages.Result) -> Unit = { 136 | handleCanFormatResult(it, onFinished, filePath) 137 | } 138 | 139 | pendingMessages.store(message.id, handler) 140 | editorProcess.writeBuffer(message.build()) 141 | } 142 | 143 | private fun handleCanFormatResult( 144 | result: PendingMessages.Result, 145 | onFinished: (Boolean?) -> Unit, 146 | filePath: String, 147 | ) { 148 | when { 149 | (result.type == MessageType.CanFormatResponse && result.data is Boolean) -> { 150 | onFinished(result.data) 151 | } 152 | (result.type == MessageType.ErrorResponse && result.data is String) -> { 153 | infoLogWithConsole( 154 | DprintBundle.message("editor.service.format.check.failed", filePath, result.data), 155 | project, 156 | LOGGER, 157 | ) 158 | onFinished(null) 159 | } 160 | (result.type === MessageType.Dropped) -> { 161 | // do nothing 162 | onFinished(null) 163 | } 164 | else -> { 165 | infoLogWithConsole( 166 | DprintBundle.message("editor.service.unsupported.message.type", result.type), 167 | project, 168 | LOGGER, 169 | ) 170 | onFinished(null) 171 | } 172 | } 173 | } 174 | 175 | /** 176 | * If we find stale messages we assume there is an issue with the underlying process and try restart. In the event 177 | * that doesn't work, it is likely there is a problem with the underlying daemon and the IJ process that runs on top 178 | * of it is not aware of its unhealthy state. 179 | */ 180 | private fun handleStaleMessages() { 181 | if (pendingMessages.hasStaleMessages()) { 182 | infoLogWithConsole(DprintBundle.message("editor.service.stale.tasks"), project, LOGGER) 183 | this.initialiseEditorService() 184 | } 185 | } 186 | 187 | override fun canRangeFormat(): Boolean { 188 | // TODO before we can enable this we need to ensure that the formatting indexes passed into fmt are converted 189 | // from string index to byte index correctly 190 | return false 191 | } 192 | 193 | override fun fmt( 194 | formatId: Int?, 195 | filePath: String, 196 | content: String, 197 | startIndex: Int?, 198 | endIndex: Int?, 199 | onFinished: (FormatResult) -> Unit, 200 | ): Int { 201 | infoLogWithConsole(DprintBundle.message("formatting.file", filePath), project, LOGGER) 202 | val message = createFormatMessage(formatId, filePath, startIndex, endIndex, content) 203 | val handler: (PendingMessages.Result) -> Unit = { 204 | val formatResult: FormatResult = mapResultToFormatResult(it, filePath) 205 | onFinished(formatResult) 206 | } 207 | pendingMessages.store(message.id, handler) 208 | editorProcess.writeBuffer(message.build()) 209 | 210 | infoLogWithConsole( 211 | DprintBundle.message("editor.service.created.formatting.task", filePath, message.id), 212 | project, 213 | LOGGER, 214 | ) 215 | 216 | return message.id 217 | } 218 | 219 | private fun createFormatMessage( 220 | formatId: Int?, 221 | filePath: String, 222 | startIndex: Int?, 223 | endIndex: Int?, 224 | content: String, 225 | ): OutgoingMessage { 226 | val outgoingMessage = OutgoingMessage(formatId ?: getNextMessageId(), MessageType.FormatFile) 227 | outgoingMessage.addString(filePath) 228 | // TODO We need to properly handle string index to byte index here 229 | outgoingMessage.addInt(startIndex ?: 0) // for range formatting add starting index 230 | outgoingMessage.addInt(endIndex ?: content.encodeToByteArray().size) // add ending index 231 | outgoingMessage.addInt(0) // Override config 232 | outgoingMessage.addString(content) 233 | return outgoingMessage 234 | } 235 | 236 | private fun mapResultToFormatResult( 237 | result: PendingMessages.Result, 238 | filePath: String, 239 | ): FormatResult { 240 | return when { 241 | (result.type == MessageType.FormatFileResponse && result.data is String?) -> { 242 | val successMessage = 243 | when (result.data) { 244 | null -> DprintBundle.message("editor.service.format.not.needed", filePath) 245 | else -> DprintBundle.message("editor.service.format.succeeded", filePath) 246 | } 247 | infoLogWithConsole(successMessage, project, LOGGER) 248 | FormatResult(formattedContent = result.data) 249 | } 250 | (result.type == MessageType.ErrorResponse && result.data is String) -> { 251 | warnLogWithConsole( 252 | DprintBundle.message("editor.service.format.failed", filePath, result.data), 253 | project, 254 | LOGGER, 255 | ) 256 | FormatResult(error = result.data) 257 | } 258 | (result.type != MessageType.Dropped) -> { 259 | val errorMessage = DprintBundle.message("editor.service.unsupported.message.type", result.type) 260 | warnLogWithConsole( 261 | DprintBundle.message("editor.service.format.failed", filePath, errorMessage), 262 | project, 263 | LOGGER, 264 | ) 265 | FormatResult(error = errorMessage) 266 | } else -> { 267 | FormatResult() 268 | } 269 | } 270 | } 271 | 272 | override fun canCancelFormat(): Boolean { 273 | return true 274 | } 275 | 276 | override fun maybeGetFormatId(): Int { 277 | return getNextMessageId() 278 | } 279 | 280 | override fun cancelFormat(formatId: Int) { 281 | val message = createNewMessage(MessageType.CancelFormat) 282 | infoLogWithConsole(DprintBundle.message("editor.service.cancel.format", formatId), project, LOGGER) 283 | message.addInt(formatId) 284 | editorProcess.writeBuffer(message.build()) 285 | pendingMessages.take(formatId) 286 | } 287 | 288 | private fun dropMessages() { 289 | for (message in pendingMessages.drain()) { 290 | infoLogWithConsole(DprintBundle.message("editor.service.clearing.message", message.first), project, LOGGER) 291 | message.second(PendingMessages.Result(MessageType.Dropped, null)) 292 | } 293 | } 294 | 295 | private fun getName(): String { 296 | return this::class.java.simpleName 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/v5/IncomingMessage.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.v5 2 | 3 | import java.nio.ByteBuffer 4 | 5 | private const val U32_BYTE_SIZE = 4 6 | 7 | class IncomingMessage(private val buffer: ByteArray) { 8 | private var index = 0 9 | 10 | fun readInt(): Int { 11 | val int = ByteBuffer.wrap(buffer, index, U32_BYTE_SIZE).int 12 | index += U32_BYTE_SIZE 13 | return int 14 | } 15 | 16 | fun readSizedString(): String { 17 | val length = readInt() 18 | val content = buffer.copyOfRange(index, index + length).decodeToString() 19 | index += length 20 | return content 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/v5/MessageType.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.v5 2 | 3 | enum class MessageType(val intValue: Int) { 4 | /** 5 | * Used when pending messages are drained due to a shutdow so that the handlers can be completed with no action. 6 | */ 7 | Dropped(-1), 8 | SuccessResponse(0), 9 | ErrorResponse(1), 10 | ShutDownProcess(2), 11 | Active(3), 12 | CanFormat(4), 13 | CanFormatResponse(5), 14 | FormatFile(6), 15 | FormatFileResponse(7), 16 | CancelFormat(8), 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/v5/OutgoingMessage.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.v5 2 | 3 | import com.dprint.i18n.DprintBundle 4 | import com.dprint.services.editorservice.exceptions.UnsupportedMessagePartException 5 | import java.nio.ByteBuffer 6 | import java.util.concurrent.atomic.AtomicInteger 7 | 8 | private var messageId = AtomicInteger(0) 9 | 10 | fun createNewMessage(type: MessageType): OutgoingMessage { 11 | return OutgoingMessage(getNextMessageId(), type) 12 | } 13 | 14 | fun getNextMessageId(): Int { 15 | return messageId.incrementAndGet() 16 | } 17 | 18 | class OutgoingMessage(val id: Int, private val type: MessageType) { 19 | // Dprint uses unsigned bytes of 4x255 for the success message and that translates 20 | // to 4x-1 in the jvm's signed bytes. 21 | private val successMessage = byteArrayOf(-1, -1, -1, -1) 22 | private val u32ByteSize = 4 23 | private var parts = mutableListOf() 24 | 25 | fun addString(str: String) { 26 | parts.add(str.encodeToByteArray()) 27 | } 28 | 29 | fun addInt(int: Int) { 30 | parts.add(int) 31 | } 32 | 33 | private fun intToFourByteArray(int: Int): ByteArray { 34 | val buffer = ByteBuffer.allocate(u32ByteSize) 35 | buffer.putInt(int) 36 | return buffer.array() 37 | } 38 | 39 | fun build(): ByteArray { 40 | var bodyLength = 0 41 | for (part in parts) { 42 | when (part) { 43 | is Int -> bodyLength += u32ByteSize 44 | is ByteArray -> bodyLength += (part.size + u32ByteSize) 45 | } 46 | } 47 | val byteLength = bodyLength + u32ByteSize * u32ByteSize 48 | val buffer = ByteBuffer.allocate(byteLength) 49 | 50 | buffer.put(intToFourByteArray(id)) 51 | buffer.put(intToFourByteArray(type.intValue)) 52 | buffer.put(intToFourByteArray(bodyLength)) 53 | 54 | for (part in parts) { 55 | when (part) { 56 | is ByteArray -> { 57 | buffer.put(intToFourByteArray(part.size)) 58 | buffer.put(part) 59 | } 60 | 61 | is Int -> { 62 | buffer.put(intToFourByteArray(part)) 63 | } 64 | 65 | else -> { 66 | throw UnsupportedMessagePartException( 67 | DprintBundle.message("editor.service.unsupported.message.type", part::class.java.simpleName), 68 | ) 69 | } 70 | } 71 | } 72 | 73 | buffer.put(successMessage) 74 | 75 | if (buffer.hasRemaining()) { 76 | val message = 77 | DprintBundle.message( 78 | "editor.service.incorrect.message.size", 79 | byteLength, 80 | byteLength - buffer.remaining(), 81 | ) 82 | throw UnsupportedMessagePartException(message) 83 | } 84 | return buffer.array() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/v5/PendingMessages.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.v5 2 | 3 | import java.util.concurrent.ConcurrentHashMap 4 | 5 | typealias Handler = (PendingMessages.Result) -> Unit 6 | 7 | data class MessageInfo(val handler: Handler, val timeStored: Long) 8 | 9 | const val STALE_LENGTH_MS = 10_000 10 | 11 | class PendingMessages { 12 | private val concurrentHashMap = ConcurrentHashMap() 13 | 14 | /** 15 | * @param type The message type for the result. If null 16 | */ 17 | class Result(val type: MessageType, val data: Any?) 18 | 19 | fun store( 20 | id: Int, 21 | handler: Handler, 22 | ) { 23 | concurrentHashMap[id] = MessageInfo(handler, System.currentTimeMillis()) 24 | } 25 | 26 | fun take(id: Int): Handler? { 27 | val info = concurrentHashMap[id] 28 | info?.let { 29 | concurrentHashMap.remove(id) 30 | } 31 | return info?.handler 32 | } 33 | 34 | fun drain(): List> { 35 | val allEntries = concurrentHashMap.entries.map { Pair(it.key, it.value.handler) } 36 | concurrentHashMap.clear() 37 | return allEntries 38 | } 39 | 40 | fun hasStaleMessages(): Boolean { 41 | val now = System.currentTimeMillis() 42 | return concurrentHashMap.values.any { now - it.timeStored > STALE_LENGTH_MS } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/services/editorservice/v5/StdoutListener.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.v5 2 | 3 | import com.dprint.i18n.DprintBundle 4 | import com.dprint.services.editorservice.process.EditorProcess 5 | import com.intellij.openapi.diagnostic.logger 6 | import java.nio.BufferUnderflowException 7 | import kotlin.concurrent.thread 8 | 9 | private const val SLEEP_TIME = 500L 10 | 11 | private val LOGGER = logger() 12 | 13 | class StdoutListener(private val editorProcess: EditorProcess, private val pendingMessages: PendingMessages) { 14 | private var listenerThread: Thread? = null 15 | var disposing = false 16 | 17 | fun listen() { 18 | disposing = false 19 | listenerThread = 20 | thread(start = true) { 21 | LOGGER.info(DprintBundle.message("editor.service.started.stdout.listener")) 22 | while (true) { 23 | if (Thread.interrupted()) { 24 | return@thread 25 | } 26 | try { 27 | handleStdout() 28 | } catch (e: InterruptedException) { 29 | if (!disposing) LOGGER.info(e) 30 | return@thread 31 | } catch (e: BufferUnderflowException) { 32 | // Happens when the editor service is shut down while this thread is waiting to read output 33 | if (!disposing) LOGGER.info(e) 34 | return@thread 35 | } catch (e: Exception) { 36 | if (!disposing) LOGGER.error(DprintBundle.message("editor.service.read.failed"), e) 37 | Thread.sleep(SLEEP_TIME) 38 | } 39 | } 40 | } 41 | } 42 | 43 | fun dispose() { 44 | listenerThread?.interrupt() 45 | } 46 | 47 | private fun handleStdout() { 48 | val messageId = editorProcess.readInt() 49 | val messageType = editorProcess.readInt() 50 | val bodyLength = editorProcess.readInt() 51 | val body = IncomingMessage(editorProcess.readBuffer(bodyLength)) 52 | editorProcess.readAndAssertSuccess() 53 | 54 | when (messageType) { 55 | MessageType.SuccessResponse.intValue -> { 56 | val responseId = body.readInt() 57 | val result = PendingMessages.Result(MessageType.SuccessResponse, null) 58 | pendingMessages.take(responseId)?.let { it(result) } 59 | } 60 | 61 | MessageType.ErrorResponse.intValue -> { 62 | val responseId = body.readInt() 63 | val errorMessage = body.readSizedString() 64 | LOGGER.info(DprintBundle.message("editor.service.received.error.response", errorMessage)) 65 | val result = PendingMessages.Result(MessageType.ErrorResponse, errorMessage) 66 | pendingMessages.take(responseId)?.let { it(result) } 67 | } 68 | 69 | MessageType.Active.intValue -> { 70 | sendSuccess(messageId) 71 | } 72 | 73 | MessageType.CanFormatResponse.intValue -> { 74 | val responseId = body.readInt() 75 | val canFormatResult = body.readInt() 76 | val result = PendingMessages.Result(MessageType.CanFormatResponse, canFormatResult == 1) 77 | pendingMessages.take(responseId)?.let { it(result) } 78 | } 79 | 80 | MessageType.FormatFileResponse.intValue -> { 81 | val responseId = body.readInt() 82 | val hasChanged = body.readInt() 83 | val text = 84 | when (hasChanged == 1) { 85 | true -> body.readSizedString() 86 | false -> null 87 | } 88 | val result = PendingMessages.Result(MessageType.FormatFileResponse, text) 89 | pendingMessages.take(responseId)?.let { it(result) } 90 | } 91 | 92 | else -> { 93 | val errorMessage = DprintBundle.message("editor.service.unsupported.message.type", messageType) 94 | LOGGER.info(errorMessage) 95 | sendFailure(messageId, errorMessage) 96 | } 97 | } 98 | } 99 | 100 | private fun sendSuccess(messageId: Int) { 101 | val message = createNewMessage(MessageType.SuccessResponse) 102 | message.addInt(messageId) 103 | sendResponse(message) 104 | } 105 | 106 | private fun sendFailure( 107 | messageId: Int, 108 | errorMessage: String, 109 | ) { 110 | val message = createNewMessage(MessageType.ErrorResponse) 111 | message.addInt(messageId) 112 | message.addString(errorMessage) 113 | sendResponse(message) 114 | } 115 | 116 | private fun sendResponse(outgoingMessage: OutgoingMessage) { 117 | editorProcess.writeBuffer(outgoingMessage.build()) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/toolwindow/Console.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.toolwindow 2 | 3 | import com.dprint.messages.DprintMessage 4 | import com.intellij.execution.impl.ConsoleViewImpl 5 | import com.intellij.execution.ui.ConsoleViewContentType 6 | import com.intellij.openapi.components.Service 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.psi.search.GlobalSearchScope 9 | import java.time.LocalDateTime 10 | import java.time.format.DateTimeFormatter 11 | 12 | @Service(Service.Level.PROJECT) 13 | class Console(val project: Project) { 14 | val consoleView = ConsoleViewImpl(project, GlobalSearchScope.allScope(project), false, false) 15 | 16 | init { 17 | with(project.messageBus.connect()) { 18 | subscribe( 19 | DprintMessage.DPRINT_MESSAGE_TOPIC, 20 | object : DprintMessage.Listener { 21 | override fun info(message: String) { 22 | consoleView.print(decorateText(message), ConsoleViewContentType.LOG_INFO_OUTPUT) 23 | } 24 | 25 | override fun warn(message: String) { 26 | consoleView.print(decorateText(message), ConsoleViewContentType.LOG_WARNING_OUTPUT) 27 | } 28 | 29 | override fun error(message: String) { 30 | consoleView.print(decorateText(message), ConsoleViewContentType.LOG_ERROR_OUTPUT) 31 | } 32 | }, 33 | ) 34 | } 35 | } 36 | 37 | private fun decorateText(text: String): String { 38 | return "${DateTimeFormatter.ofPattern("yyyy MM dd HH:mm:ss").format(LocalDateTime.now())}: ${text}\n" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/toolwindow/ConsoleToolWindowFactory.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.toolwindow 2 | 3 | import com.intellij.openapi.components.service 4 | import com.intellij.openapi.project.DumbAware 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.openapi.ui.SimpleToolWindowPanel 7 | import com.intellij.openapi.wm.ToolWindow 8 | import com.intellij.openapi.wm.ToolWindowFactory 9 | import com.intellij.ui.content.ContentFactory 10 | 11 | class ConsoleToolWindowFactory : ToolWindowFactory, DumbAware { 12 | override fun createToolWindowContent( 13 | project: Project, 14 | toolWindow: ToolWindow, 15 | ) { 16 | val console = project.service() 17 | val contentFactory = ContentFactory.getInstance() 18 | val panel = SimpleToolWindowPanel(true, false) 19 | panel.setContent(console.consoleView.component) 20 | val content = contentFactory.createContent(panel, "", false) 21 | toolWindow.contentManager.addContent(content) 22 | } 23 | 24 | override suspend fun isApplicableAsync(project: Project): Boolean { 25 | return true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.utils 2 | 3 | import com.dprint.config.ProjectConfiguration 4 | import com.dprint.i18n.DprintBundle 5 | import com.intellij.diff.util.DiffUtil 6 | import com.intellij.execution.configurations.GeneralCommandLine 7 | import com.intellij.execution.util.ExecUtil 8 | import com.intellij.ide.scratch.ScratchUtil 9 | import com.intellij.openapi.components.service 10 | import com.intellij.openapi.diagnostic.logger 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.openapi.vfs.VirtualFile 13 | import java.io.File 14 | 15 | // Need this for the IntelliJ logger 16 | private object FileUtils 17 | 18 | private val LOGGER = logger() 19 | private val DEFAULT_CONFIG_NAMES = 20 | listOf( 21 | "dprint.json", 22 | ".dprint.json", 23 | "dprint.jsonc", 24 | ".dprint.jsonc", 25 | ) 26 | 27 | /* 28 | * Utils for checking that dprint is configured correctly outside intellij. 29 | */ 30 | 31 | /** 32 | * Validates that a path is a valid json file 33 | */ 34 | fun validateConfigFile(path: String): Boolean { 35 | val file = File(path) 36 | return file.exists() && (file.extension == "json" || file.extension == "jsonc") 37 | } 38 | 39 | /** 40 | * Gets a valid config file path 41 | */ 42 | fun getValidConfigPath(project: Project): String? { 43 | val config = project.service() 44 | val configuredPath = config.state.configLocation 45 | 46 | when { 47 | validateConfigFile(configuredPath) -> return configuredPath 48 | configuredPath.isNotBlank() -> 49 | infoLogWithConsole( 50 | DprintBundle.message("notification.invalid.config.path"), 51 | project, 52 | LOGGER, 53 | ) 54 | } 55 | 56 | val basePath = project.basePath 57 | val allDirs = mutableListOf() 58 | 59 | // get all parent directories 60 | var currentDir = basePath 61 | while (currentDir != null) { 62 | allDirs.add(currentDir) 63 | currentDir = File(currentDir).parent 64 | } 65 | 66 | // look for the first valid dprint config file by looking in the project base directory and 67 | // moving up its parents until one is found 68 | for (dir in allDirs) { 69 | for (fileName in DEFAULT_CONFIG_NAMES) { 70 | val file = File(dir, fileName) 71 | when { 72 | file.exists() -> return file.path 73 | file.exists() -> 74 | warnLogWithConsole( 75 | DprintBundle.message("notification.invalid.default.config", file.path), 76 | project, 77 | LOGGER, 78 | ) 79 | } 80 | } 81 | } 82 | 83 | infoLogWithConsole(DprintBundle.message("notification.config.not.found"), project, LOGGER) 84 | 85 | return null 86 | } 87 | 88 | /** 89 | * Helper function to find out if a given virtual file is formattable. Some files, 90 | * such as scratch files and diff views will never be formattable by dprint, so 91 | * we use this to identify them early and thus save the trip to the dprint daemon. 92 | */ 93 | fun isFormattableFile( 94 | project: Project, 95 | virtualFile: VirtualFile, 96 | ): Boolean { 97 | val isScratch = ScratchUtil.isScratch(virtualFile) 98 | if (isScratch) { 99 | infoLogWithConsole(DprintBundle.message("formatting.scratch.files", virtualFile.path), project, LOGGER) 100 | } 101 | 102 | return virtualFile.isWritable && 103 | virtualFile.isInLocalFileSystem && 104 | !isScratch && 105 | !DiffUtil.isFileWithoutContent(virtualFile) 106 | } 107 | 108 | /** 109 | * Validates a path ends with 'dprint' or 'dprint.exe' and is executable 110 | */ 111 | fun validateExecutablePath(path: String): Boolean { 112 | return path.endsWith(getExecutableFile()) && File(path).canExecute() 113 | } 114 | 115 | /** 116 | * Attempts to get the dprint executable location by checking to see if it is discoverable. 117 | */ 118 | private fun getLocationFromThePath(): String? { 119 | val args = listOf(if (System.getProperty("os.name").lowercase().contains("win")) "where" else "which", "dprint") 120 | val commandLine = GeneralCommandLine(args) 121 | val output = ExecUtil.execAndGetOutput(commandLine) 122 | 123 | if (output.checkSuccess(LOGGER)) { 124 | val maybePath = output.stdout.trim() 125 | if (File(maybePath).canExecute()) { 126 | return maybePath 127 | } 128 | } 129 | 130 | return null 131 | } 132 | 133 | /** 134 | * Attempts to get the dprint executable location by checking node modules 135 | */ 136 | private fun getLocationFromTheNodeModules(basePath: String?): String? { 137 | basePath?.let { 138 | val path = "$it/node_modules/dprint/${getExecutableFile()}" 139 | if (validateExecutablePath(path)) return path 140 | } 141 | return null 142 | } 143 | 144 | private fun getExecutableFile(): String { 145 | return when (System.getProperty("os.name").lowercase().contains("win")) { 146 | true -> "dprint.exe" 147 | false -> "dprint" 148 | } 149 | } 150 | 151 | /** 152 | * Gets a valid dprint executable path. It will try to use the configured path and will fall 153 | * back to a path that is discoverable via the command line 154 | * 155 | * TODO Cache this per session so we only need to get the location from the path once 156 | */ 157 | fun getValidExecutablePath(project: Project): String? { 158 | val config = project.service() 159 | val configuredExecutablePath = config.state.executableLocation 160 | 161 | when { 162 | validateExecutablePath(configuredExecutablePath) -> return configuredExecutablePath 163 | configuredExecutablePath.isNotBlank() -> 164 | errorLogWithConsole( 165 | DprintBundle.message("notification.invalid.executable.path"), 166 | project, 167 | LOGGER, 168 | ) 169 | } 170 | 171 | getLocationFromTheNodeModules(project.basePath)?.let { 172 | return it 173 | } 174 | getLocationFromThePath()?.let { 175 | return it 176 | } 177 | 178 | errorLogWithConsole(DprintBundle.message("notification.executable.not.found"), project, LOGGER) 179 | 180 | return null 181 | } 182 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dprint/utils/LogUtils.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.utils 2 | 3 | import com.dprint.messages.DprintMessage 4 | import com.intellij.openapi.diagnostic.Logger 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.util.messages.MessageBus 7 | 8 | data class MessageWithThrowable(val message: String, val throwable: Throwable?) { 9 | override fun toString(): String { 10 | if (throwable != null) { 11 | return "$message\n\t$throwable" 12 | } 13 | return message 14 | } 15 | } 16 | 17 | fun infoConsole( 18 | message: String, 19 | project: Project, 20 | ) { 21 | maybeGetMessageBus(project)?.syncPublisher(DprintMessage.DPRINT_MESSAGE_TOPIC)?.info(message) 22 | } 23 | 24 | fun infoLogWithConsole( 25 | message: String, 26 | project: Project, 27 | logger: Logger, 28 | ) { 29 | logger.info(message) 30 | infoConsole(message, project) 31 | } 32 | 33 | fun warnLogWithConsole( 34 | message: String, 35 | project: Project, 36 | logger: Logger, 37 | ) { 38 | // Always use info for system level logging as it throws notifications into the UI 39 | logger.info(message) 40 | maybeGetMessageBus(project)?.syncPublisher(DprintMessage.DPRINT_MESSAGE_TOPIC)?.warn(message) 41 | } 42 | 43 | fun warnLogWithConsole( 44 | message: String, 45 | throwable: Throwable?, 46 | project: Project, 47 | logger: Logger, 48 | ) { 49 | // Always use info for system level logging as it throws notifications into the UI 50 | logger.warn(message, throwable) 51 | maybeGetMessageBus( 52 | project, 53 | )?.syncPublisher(DprintMessage.DPRINT_MESSAGE_TOPIC)?.warn(MessageWithThrowable(message, throwable).toString()) 54 | } 55 | 56 | fun errorLogWithConsole( 57 | message: String, 58 | project: Project, 59 | logger: Logger, 60 | ) { 61 | errorLogWithConsole(message, null, project, logger) 62 | } 63 | 64 | fun errorLogWithConsole( 65 | message: String, 66 | throwable: Throwable?, 67 | project: Project, 68 | logger: Logger, 69 | ) { 70 | // Always use info for system level logging as it throws notifications into the UI 71 | logger.error(message, throwable) 72 | maybeGetMessageBus( 73 | project, 74 | )?.syncPublisher(DprintMessage.DPRINT_MESSAGE_TOPIC)?.error(MessageWithThrowable(message, throwable).toString()) 75 | } 76 | 77 | private fun maybeGetMessageBus(project: Project): MessageBus? { 78 | if (project.isDisposed) { 79 | return null 80 | } 81 | 82 | return project.messageBus 83 | } 84 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | com.dprint.intellij.plugin 3 | Dprint 4 | dprint 5 | 6 | com.intellij.modules.platform 7 | 8 | messages.Bundle 9 | 10 | 11 | 12 | 18 | 21 | 23 | 24 | 25 | 26 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/main/resources/messages/Bundle.properties: -------------------------------------------------------------------------------- 1 | name=dprint 2 | action.com.dprint.actions.ClearCacheAction.description=Clear the dprint plugin cache of files that can be formatted 3 | action.com.dprint.actions.ClearCacheAction.text=Clear Dprint Plugin Cache 4 | action.com.dprint.actions.ReformatAction.description=Reformat the currently open file with dprint 5 | action.com.dprint.actions.ReformatAction.text=Reformat With Dprint 6 | action.com.dprint.actions.RestartAction.description=Restart the dprint editor-service daemon 7 | action.com.dprint.actions.RestartAction.text=Restart Dprint 8 | config.dprint.actions.on.save.run.dprint=Run dprint 9 | config.changed.run="Restarting due to config change." 10 | config.dprint.config.invalid=Invalid config file 11 | config.dprint.config.path=Config file path 12 | config.dprint.config.path.description=The absolute path for dprint.json. If left blank, the plugin will\ 13 | try to find dprint.json in the project root. 14 | config.dprint.command.timeout=Command timeout (ms) 15 | config.dprint.command.timeout.description=The timeout in ms for a single dprint command to run. 16 | config.dprint.command.timeout.error=Invalid number format 17 | config.dprint.initialisation.timeout=Initialisation timeout (ms) 18 | config.dprint.initialisation.timeout.description=The timeout in ms for the dprint daemon to start up. 19 | config.dprint.initialisation.timeout.error=Invalid number format 20 | config.dprint.editor.info=Received editor info: {0} 21 | config.dprint.executable.invalid=Invalid executable 22 | config.dprint.executable.path=Executable path 23 | config.dprint.executable.path.description=The absolute path for the dprint executable. If left blank, the plugin will \ 24 | try to find an executable on the path or in node_modules. 25 | config.dprint.schemaVersion.newer=Please upgrade your editor extension to be compatible with the installed version of \ 26 | dprint 27 | config.dprint.schemaVersion.not.found=Unable to determine a schema version 28 | config.dprint.schemaVersion.older=Your installed version of dprint is out of date. Apologies, but please update to the \ 29 | latest version 30 | config.enable=Enable dprint 31 | config.enable.description=Enables or disabled dprint for the project. Overrides enablement of other formatting settings. 32 | config.override.intellij.formatter=Default formatter override 33 | config.override.intellij.formatter.description=If enabled, dprint will replace the default IntelliJ formatter if the \ 34 | file can be formatted by dprint. If the file cannot be formatted by dprint the IntelliJ formatter will run as per usual. 35 | config.name=Dprint 36 | config.reload=Restart 37 | config.reload.description=Restarts the dprint daemon process. Can also be triggered via the Restart dprint action. 38 | config.run.on.save=Run dprint on save 39 | config.run.on.save.description=When a file is saved dprint will determine if the file can be formatted and will format \ 40 | it if so. This is not the same as enabling the default formatter override and running the IntelliJ formatter on save. 41 | config.verbose.logging=Verbose daemon logging 42 | config.verbose.logging.description=Enables verbose logging for the underlying dprint daemon. Logging for this will be \ 43 | delivered in the dprint console and in the IntelliJ logs. 44 | editor.process.cannot.get.editor.service.process=Cannot get a running editor service 45 | editor.service.process.is.dead=Cannot communicate with dprint daemon, please restart the dprint IJ plugin 46 | editor.service.cancel.format=Cancelling format {0} 47 | editor.service.clearing.message=Clearing message {0} 48 | editor.service.created.formatting.task=Created formatting task for {0} with id {1} 49 | editor.service.destroy=Destroying {0} 50 | editor.service.format.check.failed=dprint failed to check of the file {0} can be formatted due to:\n{1} 51 | editor.service.format.failed=dprint failed to format the file {0} due to:\n{1} 52 | editor.service.format.not.needed=No need to format {0} 53 | editor.service.format.succeeded=Successfully formatted {0} 54 | editor.service.incorrect.message.size=Incorrect message size, expected {0} and got {1} 55 | editor.service.initialize=Initializing {0} 56 | editor.service.manager.creating.formatting.task=Creating formatting task for {0} 57 | editor.service.manager.initialising.editor.service=Initialising editor service 58 | editor.service.manager.initialising.editor.service.failed.title=Dprint failed to initialise 59 | editor.service.manager.initialising.editor.service.failed.content=Please check the IDE errors and the dprint console \ 60 | tool window to diagnose the issue. It is likely that dprint took too long to resolve your editor schema. Try running \ 61 | `dprint editor-info` to start to diagnose. 62 | editor.service.manager.not.initialised=Editor Service is not initialised. Please check your environment and restart \ 63 | the dprint plugin. 64 | editor.service.manager.no.cached.can.format=Did not find cached can format result for {0} 65 | editor.service.manager.priming.can.format.cache=Priming can format cache for {0} 66 | editor.service.manager.received.schema.version=Received schema version {0} 67 | editor.service.read.failed=Failed to read stdout of the editor service 68 | editor.service.received.error.response=Received failure message: {0}. 69 | editor.service.shutting.down.timed.out=Timed out shutting down editor process 70 | editor.service.stale.tasks=Found stale tasks which are older than 10,000ms. Attempting to restart editor service. If \ 71 | problem persists there is likely an issue with the underlying dprint daemon. 72 | editor.service.started.stdout.listener=Started stdout listener 73 | editor.service.starting.working.dir=Starting editor service with executable {0}, config {1}. No working dir found. 74 | editor.service.starting=Starting editor service with executable {0}, config {1} and working directory {2}. 75 | editor.service.unsupported.message.type=Received unsupported message type {0}. 76 | error.config.path=Unable to retrieve a valid dprint configuration path. 77 | error.executable.path=Unable to retrieve a valid dprint executable path. 78 | error.failed.to.parse.json.schema=Failed to parse JSON schema. 79 | error.failed.to.parse.json.schema.received=Failed to parse JSON schema.\n\tReceived: {0}. 80 | error.failed.to.parse.json.schema.error=Failed to parse JSON schema.\n\tError: {0}. 81 | error.failed.to.parse.json.schema.received.error=Failed to parse JSON schema.\n\tReceived: {0}.\n\tError: {1}. 82 | external.formatter.can.format=Dprint can format {0}, overriding default IntelliJ formatter. 83 | external.formatter.can.format.unknown=Unable to determine if dprint can format the file, falling back to the IntelliJ formatter. 84 | external.formatter.cancelling.task=Cancelling CodeStyle formatting task {0} 85 | external.formatter.cannot.format=Dprint cannot format {0}, IntelliJ formatter will be used. 86 | external.formatter.creating.task=Creating IntelliJ CodeStyle Formatting Task for {0}. 87 | external.formatter.not.configured.to.override=Dprint is not configured to override the IntelliJ formatter. 88 | external.formatter.range.formatting=Range formatting is not currently implemented, maybe soon. 89 | external.formatter.range.overlapping=Formatting ranges overlap and dprint cannot format these. Consider formatting the \ 90 | whole file. 91 | external.formatter.illegal.state=Range format attempted without content, start or end index. Start={0}, End={1}, \ 92 | Content={2} 93 | external.formatter.running.task=Running CodeStyle formatting task {0} 94 | formatting.can.format={0} can be formatted 95 | formatting.cannot.determine.file.path=Cannot determine file path to format. 96 | formatting.cannot.format=Cannot format {0}. 97 | formatting.checking.can.format=Checking if {0} can be formatted. 98 | formatting.error=Formatting error 99 | formatting.file=Formatting {0} 100 | formatting.received.success=Received success bytes. 101 | formatting.received.value=Received value: {0} 102 | formatting.scratch.files=Cannot format scratch files, file: {0} 103 | formatting.sending.success.to.editor.service=Sending success to editor service. 104 | formatting.sending.to.editor.service=Sending to editor service: {0}. 105 | notification.config.not.found=Could not detect a config file 106 | notification.executable.not.found=Could not find a dprint executable 107 | notification.group.name=dprint 108 | notification.invalid.config.path=The configured dprint config file is invalid 109 | notification.invalid.default.config=Found invalid config file {0} 110 | notification.invalid.executable.path=The configured dprint executable is invalid 111 | process.shut.down=Dprint process {0} has shut down 112 | clear.cache.action.run=Running the clear canFormat cache action 113 | reformat.action.run=Running reformat action on {0}. 114 | restart.action.run=Running restart action 115 | save.action.run=Running save action on {0}. 116 | -------------------------------------------------------------------------------- /src/test/kotlin/com/dprint/formatter/DprintFormattingTaskTest.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.formatter 2 | 3 | import com.dprint.services.editorservice.EditorServiceManager 4 | import com.dprint.services.editorservice.FormatResult 5 | import com.dprint.utils.errorLogWithConsole 6 | import com.dprint.utils.infoLogWithConsole 7 | import com.intellij.formatting.service.AsyncFormattingRequest 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.util.TextRange 10 | import io.kotest.core.spec.style.FunSpec 11 | import io.mockk.clearAllMocks 12 | import io.mockk.every 13 | import io.mockk.mockk 14 | import io.mockk.mockkStatic 15 | import io.mockk.slot 16 | import io.mockk.verify 17 | import java.util.concurrent.CancellationException 18 | import java.util.concurrent.CompletableFuture 19 | 20 | class DprintFormattingTaskTest : FunSpec({ 21 | val path = "/some/path" 22 | 23 | mockkStatic(::infoLogWithConsole) 24 | 25 | val project = mockk() 26 | val editorServiceManager = mockk() 27 | val formattingRequest = mockk(relaxed = true) 28 | lateinit var dprintFormattingTask: DprintFormattingTask 29 | 30 | beforeEach { 31 | every { infoLogWithConsole(any(), project, any()) } returns Unit 32 | every { editorServiceManager.maybeGetFormatId() } returnsMany mutableListOf(1, 2, 3) 33 | 34 | dprintFormattingTask = DprintFormattingTask(project, editorServiceManager, formattingRequest, path) 35 | } 36 | 37 | afterEach { clearAllMocks() } 38 | 39 | test("it calls editorServiceManager.format correctly when range formatting is disabled") { 40 | val testContent = "val test = \"test\"" 41 | val successContent = "val test = \"test\"" 42 | val formatResult = FormatResult(formattedContent = successContent) 43 | val onFinished = slot<(FormatResult) -> Unit>() 44 | 45 | every { formattingRequest.documentText } returns testContent 46 | every { formattingRequest.formattingRanges } returns mutableListOf(TextRange(0, testContent.length)) 47 | every { editorServiceManager.canRangeFormat() } returns false 48 | // range indexes should be null as range format is disabled 49 | every { 50 | editorServiceManager.format( 51 | 1, path, testContent, 0, testContent.length, capture(onFinished), 52 | ) 53 | } answers { 54 | onFinished.captured.invoke(formatResult) 55 | } 56 | 57 | dprintFormattingTask.run() 58 | 59 | verify(exactly = 1) { editorServiceManager.format(1, path, testContent, 0, testContent.length, any()) } 60 | verify { formattingRequest.onTextReady(successContent) } 61 | } 62 | 63 | test("it calls editorServiceManager.format correctly when range formatting has a single range") { 64 | val testContent = "val test = \"test\"" 65 | val successContent = "val test = \"test\"" 66 | val formatResult = FormatResult(formattedContent = successContent) 67 | val onFinished = slot<(FormatResult) -> Unit>() 68 | 69 | every { formattingRequest.documentText } returns testContent 70 | every { formattingRequest.formattingRanges } returns mutableListOf(TextRange(0, testContent.length - 1)) 71 | every { editorServiceManager.canRangeFormat() } returns true 72 | // range indexes should be null as range format is disabled 73 | every { 74 | editorServiceManager.format( 75 | 1, path, testContent, 0, testContent.length - 1, capture(onFinished), 76 | ) 77 | } answers { 78 | onFinished.captured.invoke(formatResult) 79 | } 80 | 81 | dprintFormattingTask.run() 82 | 83 | verify(exactly = 1) { editorServiceManager.format(1, path, testContent, 0, testContent.length - 1, any()) } 84 | verify { formattingRequest.onTextReady(successContent) } 85 | } 86 | 87 | test("it calls editorServiceManager.format correctly when range formatting has multiple ranges") { 88 | val unformattedPart1 = "val test" 89 | val unformattedPart2 = " = \"test\"" 90 | val testContent = unformattedPart1 + unformattedPart2 91 | 92 | val formattedPart1 = "val test" 93 | val formattedPart2 = " = \"test\"" 94 | val successContentPart1 = formattedPart1 + unformattedPart2 95 | val successContentPart2 = formattedPart1 + formattedPart2 96 | 97 | val formatResult1 = FormatResult(formattedContent = successContentPart1) 98 | val onFinished1 = slot<(FormatResult) -> Unit>() 99 | 100 | val formatResult2 = FormatResult(formattedContent = successContentPart2) 101 | val onFinished2 = slot<(FormatResult) -> Unit>() 102 | 103 | every { formattingRequest.documentText } returns testContent 104 | every { 105 | formattingRequest.formattingRanges 106 | } returns 107 | mutableListOf( 108 | TextRange(0, unformattedPart1.length), 109 | TextRange(unformattedPart1.length, unformattedPart1.length + unformattedPart2.length), 110 | ) 111 | every { editorServiceManager.canRangeFormat() } returns true 112 | // range indexes should be null as range format is disabled 113 | every { 114 | editorServiceManager.format( 115 | 1, path, testContent, any(), any(), capture(onFinished1), 116 | ) 117 | } answers { 118 | onFinished1.captured.invoke(formatResult1) 119 | } 120 | 121 | every { 122 | editorServiceManager.format( 123 | 2, path, successContentPart1, any(), any(), capture(onFinished2), 124 | ) 125 | } answers { 126 | onFinished2.captured.invoke(formatResult2) 127 | } 128 | 129 | dprintFormattingTask.run() 130 | 131 | // Verify the correct range lengths are recalculated 132 | verify(exactly = 1) { editorServiceManager.format(1, path, testContent, 0, unformattedPart1.length, any()) } 133 | verify( 134 | exactly = 1, 135 | ) { 136 | editorServiceManager.format( 137 | 2, 138 | path, 139 | successContentPart1, 140 | formattedPart1.length, 141 | formattedPart1.length + unformattedPart2.length, 142 | any(), 143 | ) 144 | } 145 | verify { formattingRequest.onTextReady(successContentPart2) } 146 | } 147 | 148 | test("it calls editorServiceManager.cancel with the format id when cancelled") { 149 | val testContent = "val test = \"test\"" 150 | val formattedContent = "val test = \"test\"" 151 | val formatResult = FormatResult(formattedContent = formattedContent) 152 | val onFinished = slot<(FormatResult) -> Unit>() 153 | 154 | mockkStatic("com.dprint.utils.LogUtilsKt") 155 | every { infoLogWithConsole(any(), project, any()) } returns Unit 156 | every { errorLogWithConsole(any(), any(), project, any()) } returns Unit 157 | every { formattingRequest.documentText } returns testContent 158 | every { formattingRequest.formattingRanges } returns mutableListOf(TextRange(0, testContent.length)) 159 | every { editorServiceManager.canRangeFormat() } returns false 160 | every { editorServiceManager.canCancelFormat() } returns true 161 | every { editorServiceManager.cancelFormat(1) } returns Unit 162 | // range indexes should be null as range format is disabled 163 | every { 164 | editorServiceManager.format( 165 | any(), path, testContent, 0, testContent.length, capture(onFinished), 166 | ) 167 | } answers { 168 | CompletableFuture.runAsync { 169 | dprintFormattingTask.cancel() 170 | Thread.sleep(5000) 171 | onFinished.captured.invoke(formatResult) 172 | } 173 | } 174 | 175 | dprintFormattingTask.run() 176 | 177 | verify(exactly = 1) { editorServiceManager.format(1, path, testContent, 0, testContent.length, any()) } 178 | verify(exactly = 1) { editorServiceManager.cancelFormat(1) } 179 | verify(exactly = 1) { errorLogWithConsole(any(), any(CancellationException::class), project, any()) } 180 | verify(exactly = 0) { formattingRequest.onTextReady(any()) } 181 | } 182 | 183 | test("it calls formattingRequest.onError when the format returns a failure state") { 184 | val testContent = "val test = \"test\"" 185 | val testFailure = "Test failure" 186 | val formatResult = FormatResult(error = testFailure) 187 | val onFinished = slot<(FormatResult) -> Unit>() 188 | 189 | every { formattingRequest.documentText } returns testContent 190 | every { formattingRequest.formattingRanges } returns mutableListOf(TextRange(0, testContent.length)) 191 | every { editorServiceManager.canRangeFormat() } returns false 192 | // range indexes should be null as range format is disabled 193 | every { 194 | editorServiceManager.format( 195 | 1, path, testContent, 0, testContent.length, capture(onFinished), 196 | ) 197 | } answers { 198 | onFinished.captured.invoke(formatResult) 199 | } 200 | 201 | dprintFormattingTask.run() 202 | 203 | verify(exactly = 1) { editorServiceManager.format(1, path, testContent, 0, testContent.length, any()) } 204 | verify { formattingRequest.onError(any(), testFailure) } 205 | } 206 | }) 207 | -------------------------------------------------------------------------------- /src/test/kotlin/com/dprint/services/FormatterServiceImplTest.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services 2 | 3 | import com.dprint.services.editorservice.EditorServiceManager 4 | import com.dprint.utils.isFormattableFile 5 | import com.intellij.openapi.editor.Document 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.openapi.vfs.VirtualFile 8 | import io.kotest.core.spec.style.FunSpec 9 | import io.mockk.clearAllMocks 10 | import io.mockk.every 11 | import io.mockk.mockk 12 | import io.mockk.mockkStatic 13 | import io.mockk.verify 14 | 15 | class FormatterServiceImplTest : FunSpec({ 16 | val testPath = "/test/path" 17 | val testText = "val test = \"test\"" 18 | 19 | mockkStatic(::isFormattableFile) 20 | 21 | val virtualFile = mockk() 22 | val document = mockk() 23 | val project = mockk() 24 | val editorServiceManager = mockk(relaxed = true) 25 | 26 | val formatterService = FormatterServiceImpl(project, editorServiceManager) 27 | 28 | beforeEach { 29 | every { virtualFile.path } returns testPath 30 | every { document.text } returns testText 31 | } 32 | 33 | afterEach { 34 | clearAllMocks() 35 | } 36 | 37 | test("It doesn't format if cached can format result is false") { 38 | every { isFormattableFile(project, virtualFile) } returns true 39 | every { editorServiceManager.canFormatCached(testPath) } returns false 40 | 41 | formatterService.format(virtualFile, document) 42 | 43 | verify(exactly = 0) { editorServiceManager.format(testPath, testPath, any()) } 44 | } 45 | 46 | test("It doesn't format if cached can format result is null") { 47 | every { isFormattableFile(project, virtualFile) } returns true 48 | every { editorServiceManager.canFormatCached(testPath) } returns null 49 | 50 | formatterService.format(virtualFile, document) 51 | 52 | verify(exactly = 0) { editorServiceManager.format(testPath, testPath, any()) } 53 | } 54 | 55 | test("It formats if cached can format result is true") { 56 | every { isFormattableFile(project, virtualFile) } returns true 57 | every { editorServiceManager.canFormatCached(testPath) } returns true 58 | 59 | formatterService.format(virtualFile, document) 60 | 61 | verify(exactly = 1) { editorServiceManager.format(testPath, testText, any()) } 62 | } 63 | }) 64 | -------------------------------------------------------------------------------- /src/test/kotlin/com/dprint/services/editorservice/process/EditorProcessTest.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.process 2 | 3 | import com.dprint.config.UserConfiguration 4 | import com.dprint.utils.getValidConfigPath 5 | import com.dprint.utils.getValidExecutablePath 6 | import com.dprint.utils.infoLogWithConsole 7 | import com.intellij.execution.configurations.GeneralCommandLine 8 | import com.intellij.openapi.project.Project 9 | import io.kotest.core.spec.style.FunSpec 10 | import io.mockk.EqMatcher 11 | import io.mockk.clearAllMocks 12 | import io.mockk.every 13 | import io.mockk.mockk 14 | import io.mockk.mockkConstructor 15 | import io.mockk.mockkStatic 16 | import io.mockk.verify 17 | import java.io.File 18 | import java.util.concurrent.CompletableFuture 19 | 20 | class EditorProcessTest : FunSpec({ 21 | mockkStatic(ProcessHandle::current) 22 | mockkStatic(::infoLogWithConsole) 23 | mockkStatic("com.dprint.utils.FileUtilsKt") 24 | 25 | val project = mockk() 26 | val processHandle = mockk() 27 | val process = mockk() 28 | val userConfig = mockk() 29 | 30 | val editorProcess = EditorProcess(project, userConfig) 31 | 32 | beforeEach { 33 | every { infoLogWithConsole(any(), project, any()) } returns Unit 34 | } 35 | 36 | afterEach { 37 | clearAllMocks() 38 | } 39 | 40 | test("it creates a process with the correct args") { 41 | val execPath = "/bin/dprint" 42 | val configPath = "./dprint.json" 43 | val workingDir = "/working/dir" 44 | val parentProcessId = 1L 45 | 46 | mockkConstructor(GeneralCommandLine::class) 47 | mockkConstructor(File::class) 48 | mockkConstructor(StdErrListener::class) 49 | 50 | every { getValidExecutablePath(project) } returns execPath 51 | every { getValidConfigPath(project) } returns configPath 52 | 53 | every { ProcessHandle.current() } returns processHandle 54 | every { processHandle.pid() } returns parentProcessId 55 | every { userConfig.state } returns UserConfiguration.State() 56 | every { constructedWith(EqMatcher(configPath)).parent } returns workingDir 57 | every { process.pid() } returns 2L 58 | every { process.onExit() } returns CompletableFuture.completedFuture(process) 59 | every { anyConstructed().listen() } returns Unit 60 | 61 | val expectedArgs = 62 | listOf( 63 | execPath, 64 | "editor-service", 65 | "--config", 66 | configPath, 67 | "--parent-pid", 68 | parentProcessId.toString(), 69 | "--verbose", 70 | ) 71 | 72 | // This essentially tests the correct args are passed in. 73 | every { constructedWith(EqMatcher(expectedArgs)).createProcess() } returns process 74 | 75 | editorProcess.initialize() 76 | 77 | verify( 78 | exactly = 1, 79 | ) { constructedWith(EqMatcher(expectedArgs)).withWorkDirectory(workingDir) } 80 | verify(exactly = 1) { constructedWith(EqMatcher(expectedArgs)).createProcess() } 81 | } 82 | }) 83 | -------------------------------------------------------------------------------- /src/test/kotlin/com/dprint/services/editorservice/v5/EditorServiceV5ImplTest.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.v5 2 | 3 | import com.dprint.services.editorservice.FormatResult 4 | import com.dprint.services.editorservice.process.EditorProcess 5 | import com.dprint.utils.infoLogWithConsole 6 | import com.dprint.utils.warnLogWithConsole 7 | import com.intellij.openapi.project.Project 8 | import io.kotest.core.spec.style.FunSpec 9 | import io.mockk.clearAllMocks 10 | import io.mockk.every 11 | import io.mockk.mockk 12 | import io.mockk.mockkStatic 13 | import io.mockk.slot 14 | import io.mockk.verify 15 | 16 | class EditorServiceV5ImplTest : FunSpec({ 17 | 18 | mockkStatic(::infoLogWithConsole) 19 | mockkStatic(::createNewMessage) 20 | 21 | val project = mockk() 22 | val editorProcess = mockk() 23 | val pendingMessages = mockk() 24 | 25 | val editorServiceV5 = EditorServiceV5Impl(project, editorProcess, pendingMessages) 26 | 27 | beforeEach { 28 | every { infoLogWithConsole(any(), project, any()) } returns Unit 29 | every { createNewMessage(any()) } answers { 30 | OutgoingMessage(1, firstArg()) 31 | } 32 | every { pendingMessages.hasStaleMessages() } returns false 33 | } 34 | 35 | afterEach { 36 | clearAllMocks() 37 | } 38 | 39 | test("canFormat sends the correct message and stores a handler") { 40 | val testFile = "/test/File.kt" 41 | val onFinished = mockk<(Boolean?) -> Unit>() 42 | 43 | every { editorProcess.writeBuffer(any()) } returns Unit 44 | every { pendingMessages.store(any(), any()) } returns Unit 45 | every { onFinished(any()) } returns Unit 46 | 47 | editorServiceV5.canFormat(testFile, onFinished) 48 | 49 | val expectedOutgoingMessage = OutgoingMessage(1, MessageType.CanFormat) 50 | expectedOutgoingMessage.addString(testFile) 51 | 52 | verify(exactly = 1) { pendingMessages.store(1, any()) } 53 | verify(exactly = 1) { editorProcess.writeBuffer(expectedOutgoingMessage.build()) } 54 | } 55 | 56 | test("canFormat's handler invokes onFinished with the result on success") { 57 | val testFile = "/test/File.kt" 58 | val onFinished = mockk<(Boolean?) -> Unit>() 59 | val capturedHandler = slot<(PendingMessages.Result) -> Unit>() 60 | 61 | every { editorProcess.writeBuffer(any()) } returns Unit 62 | every { pendingMessages.store(any(), capture(capturedHandler)) } returns Unit 63 | every { onFinished(any()) } returns Unit 64 | 65 | editorServiceV5.canFormat(testFile, onFinished) 66 | capturedHandler.captured(PendingMessages.Result(MessageType.CanFormatResponse, true)) 67 | 68 | verify(exactly = 1) { onFinished(true) } 69 | } 70 | 71 | test("canFormat's handler invokes onFinished with null on error") { 72 | val testFile = "/test/File.kt" 73 | val onFinished = mockk<(Boolean?) -> Unit>() 74 | val capturedHandler = slot<(PendingMessages.Result) -> Unit>() 75 | 76 | every { editorProcess.writeBuffer(any()) } returns Unit 77 | every { pendingMessages.store(any(), capture(capturedHandler)) } returns Unit 78 | every { onFinished(any()) } returns Unit 79 | 80 | editorServiceV5.canFormat(testFile, onFinished) 81 | capturedHandler.captured(PendingMessages.Result(MessageType.ErrorResponse, "error")) 82 | 83 | verify(exactly = 1) { onFinished(null) } 84 | } 85 | 86 | test("fmt sends the correct message and stores a handler") { 87 | val testFile = "/test/File.kt" 88 | val testContent = "val test = \"test\"" 89 | val onFinished = mockk<(FormatResult) -> Unit>() 90 | 91 | every { editorProcess.writeBuffer(any()) } returns Unit 92 | every { pendingMessages.store(any(), any()) } returns Unit 93 | every { onFinished(any()) } returns Unit 94 | 95 | editorServiceV5.fmt(1, testFile, testContent, null, null, onFinished) 96 | 97 | val expectedOutgoingMessage = OutgoingMessage(1, MessageType.FormatFile) 98 | // path 99 | expectedOutgoingMessage.addString(testFile) 100 | // start position 101 | expectedOutgoingMessage.addInt(0) 102 | // content length 103 | expectedOutgoingMessage.addInt(testContent.toByteArray().size) 104 | // don't override config 105 | expectedOutgoingMessage.addInt(0) 106 | // content 107 | expectedOutgoingMessage.addString(testContent) 108 | 109 | verify(exactly = 1) { pendingMessages.store(1, any()) } 110 | verify(exactly = 1) { editorProcess.writeBuffer(expectedOutgoingMessage.build()) } 111 | } 112 | 113 | test("fmt's handler invokes onFinished with the new content on success") { 114 | val testFile = "/test/File.kt" 115 | val testContent = "val test = \"test\"" 116 | val formattedContent = "val test = \"test\"" 117 | val onFinished = mockk<(FormatResult) -> Unit>() 118 | val capturedHandler = slot<(PendingMessages.Result) -> Unit>() 119 | 120 | every { editorProcess.writeBuffer(any()) } returns Unit 121 | every { pendingMessages.store(any(), capture(capturedHandler)) } returns Unit 122 | every { onFinished(any()) } returns Unit 123 | 124 | editorServiceV5.fmt(1, testFile, testContent, null, null, onFinished) 125 | capturedHandler.captured(PendingMessages.Result(MessageType.FormatFileResponse, formattedContent)) 126 | 127 | verify(exactly = 1) { onFinished(FormatResult(formattedContent = formattedContent)) } 128 | } 129 | 130 | test("fmt's handler invokes onFinished with the error on failure") { 131 | val testFile = "/test/File.kt" 132 | val testContent = "val test = \"test\"" 133 | val testError = "test error" 134 | val onFinished = mockk<(FormatResult) -> Unit>() 135 | val capturedHandler = slot<(PendingMessages.Result) -> Unit>() 136 | 137 | mockkStatic("com.dprint.utils.LogUtilsKt") 138 | 139 | every { infoLogWithConsole(any(), project, any()) } returns Unit 140 | every { warnLogWithConsole(any(), project, any()) } returns Unit 141 | every { editorProcess.writeBuffer(any()) } returns Unit 142 | every { pendingMessages.store(any(), capture(capturedHandler)) } returns Unit 143 | every { onFinished(any()) } returns Unit 144 | 145 | editorServiceV5.fmt(1, testFile, testContent, null, null, onFinished) 146 | capturedHandler.captured(PendingMessages.Result(MessageType.ErrorResponse, testError)) 147 | 148 | verify(exactly = 1) { onFinished(FormatResult(error = testError)) } 149 | } 150 | 151 | test("Cancel format creates the correct message") { 152 | val testId = 7 153 | 154 | every { editorProcess.writeBuffer(any()) } returns Unit 155 | every { pendingMessages.take(any()) } returns null 156 | 157 | editorServiceV5.cancelFormat(testId) 158 | 159 | val expectedOutgoingMessage = OutgoingMessage(1, MessageType.CancelFormat) 160 | expectedOutgoingMessage.addInt(testId) 161 | 162 | verify { pendingMessages.take(testId) } 163 | verify { editorProcess.writeBuffer(expectedOutgoingMessage.build()) } 164 | } 165 | }) 166 | -------------------------------------------------------------------------------- /src/test/kotlin/com/dprint/services/editorservice/v5/IncomingMessageTest.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.v5 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.matchers.shouldBe 5 | import java.nio.ByteBuffer 6 | 7 | class IncomingMessageTest : FunSpec({ 8 | test("It decodes an int") { 9 | val int = 7 10 | val buffer = ByteBuffer.allocate(4) 11 | buffer.putInt(7) 12 | val incomingMessage = IncomingMessage(buffer.array()) 13 | incomingMessage.readInt() shouldBe int 14 | } 15 | 16 | test("It decodes a string") { 17 | val text = "blah!" 18 | val textAsByteArray = text.encodeToByteArray() 19 | val sizeBuffer = ByteBuffer.allocate(4) 20 | sizeBuffer.putInt(textAsByteArray.size) 21 | val buffer = ByteBuffer.allocate(4 + textAsByteArray.size) 22 | // Need to call array here so the 0's get copied into the new buffer 23 | buffer.put(sizeBuffer.array()) 24 | buffer.put(textAsByteArray) 25 | val incomingMessage = IncomingMessage(buffer.array()) 26 | incomingMessage.readSizedString() shouldBe text 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /src/test/kotlin/com/dprint/services/editorservice/v5/OutgoingMessageTest.kt: -------------------------------------------------------------------------------- 1 | package com.dprint.services.editorservice.v5 2 | 3 | import com.intellij.util.io.toByteArray 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.matchers.shouldBe 6 | import java.nio.ByteBuffer 7 | 8 | val SUCCESS_MESSAGE = byteArrayOf(-1, -1, -1, -1) 9 | 10 | internal class OutgoingMessageTest : FunSpec({ 11 | test("It builds a string message") { 12 | val id = 1 13 | val type = MessageType.Active 14 | val text = "blah!" 15 | val textAsBytes = text.toByteArray() 16 | val outgoingMessage = OutgoingMessage(id, type) 17 | outgoingMessage.addString(text) 18 | 19 | // 4 is for the size of the part, it has a single part 20 | val bodyLength = 4 + text.length 21 | // id + message type + body size + part size + part content + success message 22 | val expectedSize = 4 * 3 + 4 + text.length + SUCCESS_MESSAGE.size 23 | val expected = ByteBuffer.allocate(expectedSize) 24 | expected.put(createIntBytes(id)) 25 | expected.put(createIntBytes(type.intValue)) 26 | expected.put(createIntBytes(bodyLength)) 27 | expected.put(createIntBytes(text.length)) 28 | expected.put(textAsBytes) 29 | expected.put(SUCCESS_MESSAGE) 30 | 31 | outgoingMessage.build() shouldBe expected.array() 32 | } 33 | 34 | test("It builds an int message") { 35 | val id = 1 36 | val type = MessageType.Active 37 | val int = 2 38 | val outgoingMessage = OutgoingMessage(id, type) 39 | outgoingMessage.addInt(int) 40 | 41 | // id + message type + body size + part content + success message 42 | val expectedSize = 4 * 3 + 4 + SUCCESS_MESSAGE.size 43 | val expected = ByteBuffer.allocate(expectedSize) 44 | expected.put(createIntBytes(id)) 45 | expected.put(createIntBytes(type.intValue)) 46 | expected.put(createIntBytes(4)) // body length 47 | expected.put(createIntBytes(int)) 48 | expected.put(SUCCESS_MESSAGE) 49 | 50 | outgoingMessage.build() shouldBe expected.array() 51 | } 52 | 53 | test("It builds a combined message") { 54 | val id = 1 55 | val type = MessageType.Active 56 | val int = 2 57 | val text = "blah!" 58 | val textAsBytes = text.toByteArray() 59 | val outgoingMessage = OutgoingMessage(id, type) 60 | outgoingMessage.addInt(int) 61 | outgoingMessage.addString(text) 62 | 63 | // body length 64 | val bodyLength = 4 + 4 + text.length 65 | // id + message type + body size + int part + string part size + string part content + success message 66 | val expectedSize = 4 * 3 + 4 + 4 + text.length + SUCCESS_MESSAGE.size 67 | val expected = ByteBuffer.allocate(expectedSize) 68 | expected.put(createIntBytes(id)) 69 | expected.put(createIntBytes(type.intValue)) 70 | expected.put(createIntBytes(bodyLength)) 71 | expected.put(createIntBytes(int)) 72 | expected.put(createIntBytes(text.length)) 73 | expected.put(textAsBytes) 74 | expected.put(SUCCESS_MESSAGE) 75 | 76 | outgoingMessage.build() shouldBe expected.array() 77 | } 78 | }) 79 | 80 | private fun createIntBytes(int: Int): ByteArray { 81 | val buffer = ByteBuffer.allocate(4) 82 | buffer.putInt(int) 83 | return buffer.toByteArray() 84 | } 85 | --------------------------------------------------------------------------------