├── .nvmrc ├── .github ├── CODEOWNERS ├── release-drafter.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── workflows │ ├── release-drafter.yml │ ├── codeql-analysis.yml │ └── main.yml └── dependabot.yml ├── .npmrc ├── test-fixtures ├── gradle-project │ ├── .nvmrc │ ├── .prettierignore │ ├── .npmrc │ ├── settings.gradle │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── language-types │ │ │ │ ├── example.c │ │ │ │ ├── example.cpp │ │ │ │ ├── example.css │ │ │ │ ├── example.m │ │ │ │ ├── example.mm │ │ │ │ ├── example.py │ │ │ │ ├── example.js │ │ │ │ ├── example.jsx │ │ │ │ ├── example.less │ │ │ │ ├── example.md │ │ │ │ ├── example.scss │ │ │ │ ├── example.sql │ │ │ │ ├── example.ts │ │ │ │ ├── example.tsx │ │ │ │ ├── example.groovy │ │ │ │ ├── example.json │ │ │ │ └── example.gradle │ │ │ ├── unsupported │ │ │ └── example.unsupported │ │ │ ├── typescript │ │ │ ├── App.ts │ │ │ └── App.ts.formatted.txt │ │ │ ├── java │ │ │ └── gradle │ │ │ │ └── project │ │ │ │ ├── App.kt.formatted.txt │ │ │ │ ├── AppInvalid.java.txt │ │ │ │ ├── App.java │ │ │ │ ├── Hello.java │ │ │ │ └── App.java.formatted.txt │ │ │ └── groovy │ │ │ └── gradle │ │ │ └── project │ │ │ ├── App.groovy │ │ │ └── App.groovy.formatted.txt │ ├── .gitignore │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── .gitattributes │ ├── package.json │ ├── build.gradle │ ├── README.md │ ├── package-lock.json │ ├── .vscode │ │ └── settings.json │ ├── gradlew.bat │ └── gradlew ├── gradle-multi-project │ ├── lib │ │ └── build.gradle │ ├── README.md │ ├── build.gradle │ ├── settings.gradle │ ├── .gitignore │ ├── app │ │ ├── src │ │ │ └── main │ │ │ │ └── java │ │ │ │ └── gradle │ │ │ │ └── project │ │ │ │ ├── App.java │ │ │ │ ├── Hello.java │ │ │ │ └── App.java.formatted.txt │ │ └── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── .gitattributes │ ├── .vscode │ │ └── settings.json │ ├── gradlew.bat │ └── gradlew └── vscode-user │ └── User │ └── settings.json ├── .vscode ├── extensions.json ├── tasks.json ├── settings.json └── launch.json ├── icon.png ├── ARCHITECTURE.md ├── .prettierignore ├── CHANGELOG.md ├── src ├── Command.ts ├── AsyncQueue.ts ├── Disposables.ts ├── SpotlessRunner.ts ├── AsyncWait.ts ├── Deferred.ts ├── util.ts ├── test │ ├── integration │ │ ├── gradle-project │ │ │ ├── index.ts │ │ │ ├── dependencyChecker.test.ts │ │ │ ├── diagnostics.test.ts │ │ │ └── formatting.test.ts │ │ └── gradle-multi-project │ │ │ ├── index.ts │ │ │ └── formatting.test.ts │ ├── runTests.ts │ └── testUtil.ts ├── logger.ts ├── constants.ts ├── config.ts ├── FixAllCodeActionCommand.ts ├── DocumentFormattingEditProvider.ts ├── documentSelector.ts ├── FixAllCodeActionProvider.ts ├── FeatureManager.ts ├── extension.ts ├── DependencyChecker.ts ├── Spotless.ts └── SpotlessDiagnostics.ts ├── images └── spotless-gradle-screencast.gif ├── .prettierrc.json ├── .gitattributes ├── .vscodeignore ├── .editorconfig ├── .snyk ├── tsconfig.json ├── .gitignore ├── .eslintrc.json ├── webpack.config.js ├── LICENSE.md ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @badsyntax 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ -------------------------------------------------------------------------------- /test-fixtures/gradle-project/.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/lib/build.gradle: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badsyntax/vscode-spotless-gradle/HEAD/icon.png -------------------------------------------------------------------------------- /test-fixtures/gradle-project/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'gradle-project' 2 | -------------------------------------------------------------------------------- /test-fixtures/vscode-user/User/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "update.mode": "none" 3 | } 4 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.c: -------------------------------------------------------------------------------- 1 | // example.c 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.cpp: -------------------------------------------------------------------------------- 1 | // example.cpp 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.css: -------------------------------------------------------------------------------- 1 | /* example.css */ -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.m: -------------------------------------------------------------------------------- 1 | // example.m 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.mm: -------------------------------------------------------------------------------- 1 | // example.mm 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.py: -------------------------------------------------------------------------------- 1 | # example.py 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/unsupported/example.unsupported: -------------------------------------------------------------------------------- 1 | Unsupported file 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.js: -------------------------------------------------------------------------------- 1 | /* example.js */ 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.jsx: -------------------------------------------------------------------------------- 1 | /* example.jsx */ 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.less: -------------------------------------------------------------------------------- 1 | /* example.less */ 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.scss: -------------------------------------------------------------------------------- 1 | /* example.scss */ 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.sql: -------------------------------------------------------------------------------- 1 | /* example.sql */ 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.ts: -------------------------------------------------------------------------------- 1 | /* example.ts */ 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.tsx: -------------------------------------------------------------------------------- 1 | /* example.tsx */ 2 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.groovy: -------------------------------------------------------------------------------- 1 | /* example.groovy */ 2 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture Overview 2 | 3 | ![vscode-spotless-gradle](./images/architecture.svg) 4 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "example.json": null 3 | } 4 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/typescript/App.ts: -------------------------------------------------------------------------------- 1 | export class App {constructor() {console.log("app");}} 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode-test 3 | out/ 4 | package-lock.json 5 | *nls.*.json 6 | dist/ 7 | test-fixtures/ 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Release notes can be found here: https://github.com/badsyntax/vscode-spotless-gradle/releases 4 | -------------------------------------------------------------------------------- /src/Command.ts: -------------------------------------------------------------------------------- 1 | export interface Command { 2 | readonly id: string | string[]; 3 | execute(...args: unknown[]): void; 4 | } 5 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/README.md: -------------------------------------------------------------------------------- 1 | # Example Gradle Multi Project 2 | 3 | An example Gradle project used in the tests. 4 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'gradle-multi-project' 2 | 3 | include 'app' 4 | include 'lib' 5 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/java/gradle/project/App.kt.formatted.txt: -------------------------------------------------------------------------------- 1 | fun main() { 2 | println("Hello from Kotlin!") 3 | } 4 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: '$NEXT_PATCH_VERSION' 2 | tag-template: '$NEXT_PATCH_VERSION' 3 | template: | 4 | $CHANGES 5 | -------------------------------------------------------------------------------- /images/spotless-gradle-screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badsyntax/vscode-spotless-gradle/HEAD/images/spotless-gradle-screencast.gif -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/java/gradle/project/AppInvalid.java.txt: -------------------------------------------------------------------------------- 1 | package gradle.project; 2 | 3 | public class AppInvalid { 4 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/typescript/App.ts.formatted.txt: -------------------------------------------------------------------------------- 1 | export class App { 2 | constructor() { 3 | console.log("app"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/resources/language-types/example.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | repositories { 6 | jcenter() 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: badsyntax 7 | --- 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "printWidth": 80, 5 | "overrides": [{ "files": "*.svg", "options": { "parser": "html" } }] 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/groovy/gradle/project/App.groovy: -------------------------------------------------------------------------------- 1 | package gradle.project 2 | public class App {static void main(String... args) {println 'Groovy world!'}} 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: badsyntax 7 | --- 8 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/gradle.properties: -------------------------------------------------------------------------------- 1 | # Test that spotless can format in VS Code via spotlessIdeHook with configuration-cache enabled 2 | org.gradle.unsafe.configuration-cache=true 3 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badsyntax/vscode-spotless-gradle/HEAD/test-fixtures/gradle-project/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/java/gradle/project/App.java: -------------------------------------------------------------------------------- 1 | package gradle.project; 2 | 3 | public class App {public static void main(String[] args) {System.out.println("app");}} 4 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/java/gradle/project/Hello.java: -------------------------------------------------------------------------------- 1 | package gradle.project; 2 | 3 | public class Hello {public static void main(String[] args) {System.out.println("hello");}} 4 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/app/src/main/java/gradle/project/App.java: -------------------------------------------------------------------------------- 1 | package gradle.project; 2 | 3 | public class App {public static void main(String[] args) {System.out.println("app");}} 4 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/gradle.properties: -------------------------------------------------------------------------------- 1 | # Test that spotless can format in VS Code via spotlessIdeHook with configuration-cache enabled 2 | org.gradle.unsafe.configuration-cache=true 3 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/app/src/main/java/gradle/project/Hello.java: -------------------------------------------------------------------------------- 1 | package gradle.project; 2 | 3 | public class Hello {public static void main(String[] args) {System.out.println("hello");}} 4 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badsyntax/vscode-spotless-gradle/HEAD/test-fixtures/gradle-multi-project/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | * text=auto eol=lf 5 | 6 | # These are explicitly windows files and should use crlf 7 | *.bat text eol=crlf 8 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/groovy/gradle/project/App.groovy.formatted.txt: -------------------------------------------------------------------------------- 1 | package gradle.project 2 | public class App { 3 | static void main(String... args) { 4 | println 'Groovy world!' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/src/main/java/gradle/project/App.java.formatted.txt: -------------------------------------------------------------------------------- 1 | package gradle.project; 2 | 3 | public class App { 4 | public static void main(String[] args) { 5 | System.out.println("app"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/app/src/main/java/gradle/project/App.java.formatted.txt: -------------------------------------------------------------------------------- 1 | package gradle.project; 2 | 3 | public class App { 4 | public static void main(String[] args) { 5 | System.out.println("app"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test-fixtures/ 3 | node_modules/ 4 | images/ 5 | .vscode/ 6 | .vscode-test/ 7 | .github/ 8 | .gradle/ 9 | tsconfig.json 10 | webpack.config.js 11 | .gitignore 12 | .eslintrc.json 13 | .prettierignore 14 | .prettierrc.json 15 | .nvmrc 16 | .snyk 17 | **/*.map 18 | **/*.ts 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.{json,ts}] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /src/AsyncQueue.ts: -------------------------------------------------------------------------------- 1 | export class AsyncQueue { 2 | private promise: Promise | undefined; 3 | protected async queue(func: () => Promise): Promise { 4 | if (this.promise) { 5 | await this.promise; 6 | } 7 | this.promise = func(); 8 | return this.promise.finally(() => { 9 | this.promise = undefined; 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | update_release_draft: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Draft release 12 | uses: release-drafter/release-drafter@v5 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.RELEASE_DRAFTER_TOKEN_GITHUB }} 15 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.14.1 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-LODASH-567746: 7 | - vsce > lodash: 8 | patched: '2020-05-11T18:12:26.375Z' 9 | - vsce > cheerio > lodash: 10 | patched: '2020-05-11T18:12:26.375Z' 11 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "watch", 7 | "problemMatcher": "$tsc-watch", 8 | "isBackground": true, 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/Disposables.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export class Disposables { 4 | private disposables: vscode.Disposable[] = []; 5 | 6 | public add(...disposables: vscode.Disposable[]): void { 7 | this.disposables.push(...disposables); 8 | } 9 | 10 | public dispose(): void { 11 | for (const disposable of this.disposables) { 12 | disposable.dispose(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": ["es6", "es2018.promise", "es2019.array", "dom"], 7 | "sourceMap": true, 8 | "strict": true, 9 | "rootDir": "src", 10 | "skipLibCheck": true, 11 | "noUnusedLocals": true, 12 | "experimentalDecorators": true 13 | }, 14 | "include": ["src/**/*.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'application' 4 | id 'com.diffplug.spotless' version '5.6.1' 5 | } 6 | 7 | repositories { 8 | jcenter() 9 | } 10 | 11 | application { 12 | mainClassName = 'gradle.project.App' 13 | } 14 | 15 | spotless { 16 | java { 17 | googleJavaFormat() 18 | removeUnusedImports() 19 | trimTrailingWhitespace() 20 | targetExclude "build/**" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.spotlessGradle": true 5 | }, 6 | "java.format.enabled": false, 7 | "spotlessGradle.diagnostics.enable": false, 8 | "spotlessGradle.format.enable": true, 9 | "[java]": { 10 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle", 11 | "spotlessGradle.diagnostics.enable": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gradle-project", 3 | "version": "1.0.0", 4 | "description": "An example Gradle project used in the tests.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "prettier": "^2.1.2" 14 | }, 15 | "eslintConfig": { 16 | "ignorePatterns": "*" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Output directories 3 | build/ 4 | target/ 5 | bin/ 6 | out/ 7 | 8 | # Mac OS X files 9 | .DS_Store 10 | 11 | # Eclipse files 12 | .settings 13 | .classpath 14 | .project 15 | 16 | # vim files 17 | .*.sw[a-z] 18 | *.un~ 19 | 20 | # Java class files 21 | *.class 22 | 23 | # Gradle 24 | .gradle 25 | 26 | # direnv 27 | .envrc 28 | 29 | # node 30 | node_modules 31 | 32 | # vscode-tst 33 | .vscode-test 34 | 35 | # vscode packaged extension 36 | *.vsix 37 | 38 | # webpack bundle 39 | dist 40 | 41 | # local packages 42 | *.tgz 43 | -------------------------------------------------------------------------------- /src/SpotlessRunner.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { AsyncQueue } from './AsyncQueue'; 3 | import { Spotless } from './Spotless'; 4 | 5 | export class SpotlessRunner extends AsyncQueue { 6 | constructor(private readonly spotless: Spotless) { 7 | super(); 8 | } 9 | 10 | async run( 11 | document: vscode.TextDocument, 12 | cancellationToken?: vscode.CancellationToken 13 | ): Promise { 14 | return this.queue(() => this.spotless.apply(document, cancellationToken)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/AsyncWait.ts: -------------------------------------------------------------------------------- 1 | export class AsyncWait { 2 | private promise: Promise | undefined; 3 | private stale = false; 4 | protected async waitAndRun(func: () => Promise): Promise { 5 | if (this.promise) { 6 | this.stale = true; 7 | } else { 8 | this.promise = func(); 9 | const result = await this.promise; 10 | this.promise = undefined; 11 | if (this.stale) { 12 | this.stale = false; 13 | return this.waitAndRun(func); 14 | } 15 | return result; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "out": false // set this to true to hide the "out" folder with the compiled JS files 4 | }, 5 | "search.exclude": { 6 | "out": true // set this to false to include "out" folder in search results 7 | }, 8 | "typescript.tsc.autoDetect": "off", 9 | "eslint.validate": ["javascript", "typescript"], 10 | "editor.formatOnSave": true, 11 | "editor.codeActionsOnSave": { 12 | "source.fixAll.eslint": true, 13 | "source.organizeImports": false 14 | }, 15 | "cSpell.words": [ 16 | "linting", 17 | "Pspotless", 18 | "richardwillis", 19 | "screencast", 20 | "spotless" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/Deferred.ts: -------------------------------------------------------------------------------- 1 | export class Deferred { 2 | public promise: Promise; 3 | 4 | private _resolve?: (value: T | PromiseLike) => void; 5 | private _reject?: (reason?: Error) => void; 6 | 7 | constructor() { 8 | this.promise = new Promise( 9 | ( 10 | resolve: (value: T | PromiseLike) => void, 11 | reject: (reason?: Error) => void 12 | ) => { 13 | this._resolve = resolve; 14 | this._reject = reject; 15 | } 16 | ); 17 | } 18 | 19 | resolve(value: T | PromiseLike): void { 20 | this._resolve!(value); 21 | } 22 | 23 | reject(reason?: Error): void { 24 | this._reject!(reason); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: gradle 9 | directory: '/test-fixtures/gradle-project' 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | ignore: 14 | - dependency-name: com.diffplug.spotless 15 | versions: 16 | - 5.10.0 17 | - 5.10.1 18 | - 5.10.2 19 | - 5.11.0 20 | - 5.11.1 21 | - 5.12.0 22 | - 5.12.1 23 | - 5.9.0 24 | - dependency-name: org.codehaus.groovy:groovy-all 25 | versions: 26 | - 3.0.7 27 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | 4 | export function sanitizePath(fsPath: string): string { 5 | if (process.platform === 'win32') { 6 | // vscode.Uri.fsPath will lower-case the drive letters 7 | // https://github.com/microsoft/vscode/blob/dc348340fd1a6c583cb63a1e7e6b4fd657e01e01/src/vs/vscode.d.ts#L1338 8 | return fsPath[0].toUpperCase() + fsPath.substr(1); 9 | } 10 | return fsPath; 11 | } 12 | 13 | export function getWorkspaceFolder(uri: vscode.Uri): vscode.WorkspaceFolder { 14 | const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); 15 | if (!workspaceFolder) { 16 | throw new Error( 17 | `Unable to find workspace folder for ${path.basename(uri.fsPath)}` 18 | ); 19 | } 20 | return workspaceFolder; 21 | } 22 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'application' 4 | id 'groovy' 5 | id 'com.diffplug.spotless' version '6.2.2' 6 | } 7 | 8 | repositories { 9 | jcenter() 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | implementation 'org.codehaus.groovy:groovy-all:3.0.9' 15 | } 16 | 17 | application { 18 | mainClassName = 'gradle.project.App' 19 | } 20 | 21 | jar { 22 | duplicatesStrategy(DuplicatesStrategy.EXCLUDE) 23 | } 24 | 25 | spotless { 26 | java { 27 | googleJavaFormat() 28 | removeUnusedImports() 29 | trimTrailingWhitespace() 30 | targetExclude "build/**" 31 | } 32 | groovy { 33 | excludeJava() 34 | greclipse() 35 | } 36 | groovyGradle { 37 | target '*.gradle' 38 | greclipse() 39 | } 40 | typescript { 41 | target 'src/**/*.ts' 42 | prettier() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier", 6 | "plugin:prettier/recommended", 7 | "plugin:sonarjs/recommended" 8 | ], 9 | "plugins": ["sonarjs"], 10 | "parserOptions": { "ecmaVersion": 2018, "sourceType": "module" }, 11 | "rules": { 12 | "@typescript-eslint/no-use-before-define": "off", 13 | "@typescript-eslint/no-non-null-assertion": "off", 14 | "no-unused-vars": "off", 15 | "@typescript-eslint/no-unused-vars": ["error"], 16 | "@typescript-eslint/explicit-function-return-type": "off" 17 | }, 18 | "overrides": [ 19 | { 20 | "files": ["*.ts", "*.tsx"], 21 | "rules": { 22 | "@typescript-eslint/explicit-function-return-type": ["error"] 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/README.md: -------------------------------------------------------------------------------- 1 | # Example Gradle Project 2 | 3 | An example Gradle project used in the tests. 4 | 5 | ## Formatting massive files 6 | 7 | An example massive Java file (`MassiveFile.java`) is provided to show how well the `vscode-gradle`, `vscode-spotless-gradle` and `gradle.spotless` libraries work on massive files. 8 | 9 | The file is pretty much the biggest the libraries can handle. If the file was any bigger, we could see the following issues: 10 | 11 | - The file contents (request message) could exceed the grpc-client & http2 maximum message sizes (`grpc-js` throws a `Call cancelled` error) 12 | - The formatted contents (response message) could be bigger than the maximum message size defined by the gRPC service (`io.grpc.Server` throws `gRPC message exceeds maximum size 4194304`) 13 | - The `spotlessApply` task could run out of memory (`java.lang.OutOfMemoryError: Java heap space`) 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | //@ts-check 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', 10 | entry: './src/extension.ts', 11 | output: { 12 | path: path.resolve(__dirname, 'dist'), 13 | filename: 'extension.js', 14 | libraryTarget: 'commonjs2', 15 | devtoolModuleFilenameTemplate: '../[resource-path]', 16 | }, 17 | devtool: 'source-map', 18 | externals: { 19 | vscode: 'commonjs vscode', 20 | }, 21 | resolve: { 22 | extensions: ['.ts', '.js'], 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.ts$/, 28 | exclude: /node_modules/, 29 | use: [ 30 | { 31 | loader: 'ts-loader', 32 | }, 33 | ], 34 | }, 35 | ], 36 | }, 37 | }; 38 | 39 | module.exports = config; 40 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeQL' 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | language: ['javascript'] 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v2 22 | with: 23 | fetch-depth: 2 24 | - run: git checkout HEAD^2 25 | if: ${{ github.event_name == 'pull_request' }} 26 | - name: Initialize CodeQL 27 | uses: github/codeql-action/init@v1 28 | with: 29 | languages: ${{ matrix.language }} 30 | - name: Use Node 12.16.2 31 | uses: actions/setup-node@v2.5.1 32 | with: 33 | node-version: 12.16.2 34 | - name: Build 35 | run: | 36 | npm ci 37 | npm run compile:all 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v1 40 | -------------------------------------------------------------------------------- /src/test/integration/gradle-project/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | const mocha = new Mocha({ 7 | ui: 'bdd', 8 | color: true, 9 | timeout: 60000, // the time it takes for vscode-gradle to load the gradle project, then run the tests 10 | bail: true, 11 | }); 12 | 13 | const testsRoot = path.resolve(__dirname); 14 | 15 | return new Promise((c, e) => { 16 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 17 | if (err) { 18 | return e(err); 19 | } 20 | 21 | // Add files to the test suite 22 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 23 | 24 | try { 25 | // Run the mocha test 26 | mocha.run((failures) => { 27 | if (failures > 0) { 28 | e(new Error(`${failures} tests failed.`)); 29 | } else { 30 | c(); 31 | } 32 | }); 33 | } catch (err) { 34 | console.error(err); 35 | e(err); 36 | } 37 | }); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/test/integration/gradle-multi-project/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | const mocha = new Mocha({ 7 | ui: 'bdd', 8 | color: true, 9 | timeout: 60000, // the time it takes for vscode-gradle to load the gradle project, then run the tests 10 | bail: true, 11 | }); 12 | 13 | const testsRoot = path.resolve(__dirname); 14 | 15 | return new Promise((c, e) => { 16 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 17 | if (err) { 18 | return e(err); 19 | } 20 | 21 | // Add files to the test suite 22 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 23 | 24 | try { 25 | // Run the mocha test 26 | mocha.run((failures) => { 27 | if (failures > 0) { 28 | e(new Error(`${failures} tests failed.`)); 29 | } else { 30 | c(); 31 | } 32 | }); 33 | } catch (err) { 34 | console.error(err); 35 | e(err); 36 | } 37 | }); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Richard Willis 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 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gradle-project", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "gradle-project", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "prettier": "^2.1.2" 13 | } 14 | }, 15 | "node_modules/prettier": { 16 | "version": "2.5.1", 17 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", 18 | "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", 19 | "dev": true, 20 | "bin": { 21 | "prettier": "bin-prettier.js" 22 | }, 23 | "engines": { 24 | "node": ">=10.13.0" 25 | } 26 | } 27 | }, 28 | "dependencies": { 29 | "prettier": { 30 | "version": "2.5.1", 31 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", 32 | "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", 33 | "dev": true 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | type logType = 'info' | 'warning' | 'error' | 'debug'; 4 | 5 | export class Logger { 6 | private channel?: vscode.OutputChannel; 7 | 8 | private log(message: string, type: logType): void { 9 | if (!this.channel) { 10 | throw new Error('No extension output channel defined.'); 11 | } 12 | const logMessage = this.format(message, type); 13 | this.channel.appendLine(logMessage); 14 | } 15 | 16 | public format(message: string, type: logType): string { 17 | return `[${type}] ${message}`; 18 | } 19 | 20 | public info(...messages: string[]): void { 21 | this.log(messages.join(' '), 'info'); 22 | } 23 | 24 | public warning(...messages: string[]): void { 25 | this.log(messages.join(' '), 'warning'); 26 | } 27 | 28 | public error(...messages: string[]): void { 29 | this.log(messages.join(' '), 'error'); 30 | } 31 | 32 | public debug(...messages: string[]): void { 33 | this.log(messages.join(' '), 'debug'); 34 | } 35 | 36 | public getChannel(): vscode.OutputChannel | undefined { 37 | return this.channel; 38 | } 39 | 40 | public setLoggingChannel(channel: vscode.OutputChannel): void { 41 | if (this.channel) { 42 | throw new Error('Output channel already defined.'); 43 | } 44 | this.channel = channel; 45 | } 46 | } 47 | 48 | export const logger = new Logger(); 49 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ALL_SUPPORTED_LANGUAGES = [ 2 | 'java', 3 | 'kotlinscript', 4 | 'kotlin', 5 | 'scala', 6 | 'sql', 7 | 'groovy', 8 | 'gradle', 9 | 'javascript', 10 | 'javascriptreact', 11 | 'typescript', 12 | 'typescriptreact', 13 | 'css', 14 | 'scss', 15 | 'less', 16 | 'vue', 17 | 'graphql', 18 | 'json', 19 | 'yaml', 20 | 'markdown', 21 | 'python', 22 | 'c', 23 | 'cpp', 24 | 'csharp', 25 | 'objective-c', 26 | 'objective-cpp', 27 | ]; 28 | export const GRADLE_FOR_JAVA_EXTENSION_ID = 'vscjava.vscode-gradle'; 29 | export const SPOTLESS_GRADLE_EXTENSION_ID = 30 | 'richardwillis.vscode-spotless-gradle'; 31 | 32 | export const OUTPUT_CHANNEL_ID = 'Spotless Gradle'; 33 | export const DIAGNOSTICS_ID = 'Spotless'; 34 | export const DIAGNOSTICS_SOURCE_ID = 'gradle'; 35 | 36 | export const SPOTLESS_STATUS_IS_CLEAN = 'IS CLEAN'; 37 | export const SPOTLESS_STATUS_DID_NOT_CONVERGE = 'DID NOT CONVERGE'; 38 | export const SPOTLESS_STATUS_IS_DIRTY = 'IS DIRTY'; 39 | 40 | export const SPOTLESS_STATUSES = [ 41 | SPOTLESS_STATUS_DID_NOT_CONVERGE, 42 | SPOTLESS_STATUS_IS_CLEAN, 43 | SPOTLESS_STATUS_IS_DIRTY, 44 | ]; 45 | 46 | export const INSTALL_COMPATIBLE_EXTENSION_VERSIONS = 47 | 'Install Compatible Versions'; 48 | 49 | export const CONFIG_NAMESPACE = 'spotlessGradle'; 50 | export const CONFIG_FORMAT_ENABLE = 'format.enable'; 51 | export const CONFIG_DIAGNOSTICS_ENABLE = 'diagnostics.enable'; 52 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { 3 | CONFIG_DIAGNOSTICS_ENABLE, 4 | CONFIG_FORMAT_ENABLE, 5 | CONFIG_NAMESPACE, 6 | } from './constants'; 7 | 8 | export function getConfigFormatEnable( 9 | workspaceFolder: vscode.WorkspaceFolder 10 | ): boolean { 11 | return vscode.workspace 12 | .getConfiguration(CONFIG_NAMESPACE, workspaceFolder.uri) 13 | .get(CONFIG_FORMAT_ENABLE, false); 14 | } 15 | 16 | export function getConfigLangOverrideFormatEnable( 17 | workspaceFolder: vscode.WorkspaceFolder, 18 | language: string, 19 | defaultValue: boolean 20 | ): boolean | undefined { 21 | return ( 22 | vscode.workspace.getConfiguration(`[${language}]`, workspaceFolder.uri)[ 23 | `${CONFIG_NAMESPACE}.${CONFIG_FORMAT_ENABLE}` 24 | ] ?? defaultValue 25 | ); 26 | } 27 | 28 | export function getConfigDiagnosticsEnable( 29 | workspaceFolder: vscode.WorkspaceFolder 30 | ): boolean { 31 | return vscode.workspace 32 | .getConfiguration(CONFIG_NAMESPACE, workspaceFolder.uri) 33 | .get(CONFIG_DIAGNOSTICS_ENABLE, false); 34 | } 35 | 36 | export function getConfigLangOverrideDiagnosticsEnable( 37 | workspaceFolder: vscode.WorkspaceFolder, 38 | language: string, 39 | defaultValue: boolean 40 | ): boolean | undefined { 41 | const diagnostics = vscode.workspace.getConfiguration( 42 | `[${language}]`, 43 | workspaceFolder.uri 44 | )[`${CONFIG_NAMESPACE}.${CONFIG_DIAGNOSTICS_ENABLE}`]; 45 | return diagnostics ?? defaultValue; 46 | } 47 | -------------------------------------------------------------------------------- /src/FixAllCodeActionCommand.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Command } from './Command'; 3 | import { logger } from './logger'; 4 | import { SpotlessRunner } from './SpotlessRunner'; 5 | 6 | export class FixAllCodeActionsCommand implements Command { 7 | public static readonly Id = 'vscode-spotless-gradle.fixAllCodeActions'; 8 | public readonly id = FixAllCodeActionsCommand.Id; 9 | 10 | constructor( 11 | private readonly context: vscode.ExtensionContext, 12 | private readonly spotlessRunner: SpotlessRunner 13 | ) {} 14 | 15 | public register(): void { 16 | this.context.subscriptions.push( 17 | vscode.commands.registerCommand(this.id, this.execute) 18 | ); 19 | } 20 | 21 | public execute = async ( 22 | document: vscode.TextDocument, 23 | cancellationToken?: vscode.CancellationToken 24 | ): Promise => { 25 | try { 26 | const spotlessChanges = await this.getSpotlessChanges( 27 | document, 28 | cancellationToken 29 | ); 30 | if (!spotlessChanges) { 31 | return; 32 | } 33 | const range = new vscode.Range( 34 | document.positionAt(0), 35 | document.positionAt(document.getText().length) 36 | ); 37 | const workspaceEdit = new vscode.WorkspaceEdit(); 38 | workspaceEdit.replace(document.uri, range, spotlessChanges); 39 | await vscode.workspace.applyEdit(workspaceEdit); 40 | } catch (e) { 41 | logger.error(`Unable to apply workspace edits: ${(e as Error).message}`); 42 | } 43 | }; 44 | 45 | private getSpotlessChanges( 46 | document: vscode.TextDocument, 47 | cancellationToken?: vscode.CancellationToken 48 | ): Promise { 49 | return this.spotlessRunner.run(document, cancellationToken); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DocumentFormattingEditProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { logger } from './logger'; 4 | import { SpotlessRunner } from './SpotlessRunner'; 5 | 6 | const noChanges: vscode.TextEdit[] = []; 7 | 8 | export class DocumentFormattingEditProvider 9 | implements vscode.DocumentFormattingEditProvider, vscode.Disposable 10 | { 11 | private documentFormattingEditProvider: vscode.Disposable | undefined; 12 | 13 | constructor( 14 | private readonly spotlessRunner: SpotlessRunner, 15 | private documentSelector: vscode.DocumentSelector 16 | ) {} 17 | 18 | public register(): void { 19 | this.documentFormattingEditProvider = 20 | vscode.languages.registerDocumentFormattingEditProvider( 21 | this.documentSelector, 22 | this 23 | ); 24 | } 25 | 26 | public dispose(): void { 27 | this.documentFormattingEditProvider?.dispose(); 28 | } 29 | 30 | public setDocumentSelector(documentSelector: vscode.DocumentSelector): void { 31 | this.documentSelector = documentSelector; 32 | this.dispose(); 33 | this.register(); 34 | } 35 | 36 | async provideDocumentFormattingEdits( 37 | document: vscode.TextDocument, 38 | _options: vscode.FormattingOptions, 39 | cancellationToken: vscode.CancellationToken 40 | ): Promise { 41 | try { 42 | const spotlessChanges = await this.spotlessRunner.run( 43 | document, 44 | cancellationToken 45 | ); 46 | if (!spotlessChanges) { 47 | return noChanges; 48 | } 49 | const range = new vscode.Range( 50 | document.positionAt(0), 51 | document.positionAt(document.getText().length) 52 | ); 53 | return [new vscode.TextEdit(range, spotlessChanges)]; 54 | } catch (e) { 55 | logger.error(`Unable to apply formatting: ${(e as Error).message}`); 56 | return noChanges; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/documentSelector.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { 3 | getConfigFormatEnable, 4 | getConfigLangOverrideFormatEnable, 5 | getConfigDiagnosticsEnable, 6 | getConfigLangOverrideDiagnosticsEnable, 7 | } from './config'; 8 | import { ALL_SUPPORTED_LANGUAGES } from './constants'; 9 | 10 | export function getDocumentSelector( 11 | knownLanguages: string[], 12 | spotlessLanguages: string[] 13 | ): Array { 14 | const languages = Array.from(new Set(spotlessLanguages)).filter((language) => 15 | knownLanguages.includes(language) 16 | ); 17 | return languages.map((language) => ({ 18 | language, 19 | scheme: 'file', 20 | })); 21 | } 22 | 23 | export function getFormatDocumentSelector( 24 | knownLanguages: string[] 25 | ): Array { 26 | const spotlessLanguages = (vscode.workspace.workspaceFolders || []) 27 | .map((workspaceFolder) => { 28 | const globalFormatEnable = getConfigFormatEnable(workspaceFolder); 29 | return ALL_SUPPORTED_LANGUAGES.filter((language) => { 30 | return getConfigLangOverrideFormatEnable( 31 | workspaceFolder, 32 | language, 33 | globalFormatEnable 34 | ); 35 | }); 36 | }) 37 | .flat(); 38 | return getDocumentSelector(knownLanguages, spotlessLanguages); 39 | } 40 | 41 | export function getDiagnosticsDocumentSelector( 42 | knownLanguages: string[] 43 | ): Array { 44 | const spotlessLanguages = (vscode.workspace.workspaceFolders || []) 45 | .map((workspaceFolder) => { 46 | const globalDiagnosticsEnable = 47 | getConfigDiagnosticsEnable(workspaceFolder); 48 | return ALL_SUPPORTED_LANGUAGES.filter((language) => { 49 | return getConfigLangOverrideDiagnosticsEnable( 50 | workspaceFolder, 51 | language, 52 | globalDiagnosticsEnable 53 | ); 54 | }); 55 | }) 56 | .flat(); 57 | return getDocumentSelector(knownLanguages, spotlessLanguages); 58 | } 59 | -------------------------------------------------------------------------------- /src/FixAllCodeActionProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { FixAllCodeActionsCommand } from './FixAllCodeActionCommand'; 3 | 4 | const noChanges: vscode.CodeAction[] = []; 5 | 6 | export class FixAllCodeActionProvider 7 | implements vscode.CodeActionProvider, vscode.Disposable 8 | { 9 | public static readonly fixAllCodeActionKind = 10 | vscode.CodeActionKind.SourceFixAll.append('spotlessGradle'); 11 | 12 | public static metadata: vscode.CodeActionProviderMetadata = { 13 | providedCodeActionKinds: [FixAllCodeActionProvider.fixAllCodeActionKind], 14 | }; 15 | 16 | private codeActionsProvider: vscode.Disposable | undefined; 17 | 18 | constructor(private documentSelector: vscode.DocumentSelector) {} 19 | 20 | public register(): void { 21 | this.codeActionsProvider = vscode.languages.registerCodeActionsProvider( 22 | this.documentSelector, 23 | this, 24 | FixAllCodeActionProvider.metadata 25 | ); 26 | } 27 | 28 | public dispose(): void { 29 | this.codeActionsProvider?.dispose(); 30 | } 31 | 32 | public setDocumentSelector(documentSelector: vscode.DocumentSelector): void { 33 | this.documentSelector = documentSelector; 34 | this.dispose(); 35 | this.register(); 36 | } 37 | 38 | public provideCodeActions( 39 | document: vscode.TextDocument, 40 | _range: vscode.Range | vscode.Selection, 41 | context: vscode.CodeActionContext, 42 | cancellationToken: vscode.CancellationToken 43 | ): vscode.CodeAction[] { 44 | if (!context.only) { 45 | return noChanges; 46 | } 47 | if ( 48 | !context.only.contains(FixAllCodeActionProvider.fixAllCodeActionKind) && 49 | !FixAllCodeActionProvider.fixAllCodeActionKind.contains(context.only) 50 | ) { 51 | return noChanges; 52 | } 53 | const title = 'Format code using Spotless'; 54 | const action = new vscode.CodeAction( 55 | title, 56 | FixAllCodeActionProvider.fixAllCodeActionKind 57 | ); 58 | action.command = { 59 | title, 60 | command: FixAllCodeActionsCommand.Id, 61 | arguments: [document, cancellationToken], 62 | }; 63 | return [action]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 10 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 11 | "preLaunchTask": "npm: compile:dev" 12 | }, 13 | { 14 | "name": "Test gradle-project", 15 | "type": "extensionHost", 16 | "request": "launch", 17 | "runtimeExecutable": "${execPath}", 18 | "args": [ 19 | "--extensionDevelopmentPath=${workspaceFolder}", 20 | "--extensionTestsPath=${workspaceFolder}/out/test/integration/gradle-project/index", 21 | "--disable-extension=redhat.java", 22 | "--disable-extension=vscjava.vscode-java-dependency", 23 | "--disable-extension=vscjava.vscode-java-test", 24 | "--disable-extension=shengchen.vscode-checkstyle", 25 | "--disable-extension=eamodio.gitlens", 26 | "--disable-extension=sonarsource.sonarlint-vscode", 27 | "--disable-extension=esbenp.prettier-vscode", 28 | "${workspaceFolder}/test-fixtures/gradle-project" 29 | ], 30 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 31 | "preLaunchTask": "npm: compile:all" 32 | }, 33 | { 34 | "name": "Test gradle-multi-project", 35 | "type": "extensionHost", 36 | "request": "launch", 37 | "runtimeExecutable": "${execPath}", 38 | "args": [ 39 | "--extensionDevelopmentPath=${workspaceFolder}", 40 | "--extensionTestsPath=${workspaceFolder}/out/test/integration/gradle-multi-project/index", 41 | "--disable-extension=redhat.java", 42 | "--disable-extension=vscjava.vscode-java-dependency", 43 | "--disable-extension=vscjava.vscode-java-test", 44 | "--disable-extension=shengchen.vscode-checkstyle", 45 | "--disable-extension=eamodio.gitlens", 46 | "--disable-extension=sonarsource.sonarlint-vscode", 47 | "--disable-extension=esbenp.prettier-vscode", 48 | "${workspaceFolder}/test-fixtures/gradle-multi-project" 49 | ], 50 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 51 | "preLaunchTask": "npm: compile:all" 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.spotlessGradle": true 5 | }, 6 | "java.format.enabled": false, 7 | "typescript.validate.enable": false, 8 | "typescript.format.enable": false, 9 | "javascript.validate.enable": false, 10 | "javascript.format.enable": false, 11 | "spotlessGradle.diagnostics.enable": false, 12 | "spotlessGradle.format.enable": true, 13 | "files.trimTrailingWhitespace": false, 14 | "[java]": { 15 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle", 16 | "spotlessGradle.diagnostics.enable": true 17 | }, 18 | "[kotlinscript]": { 19 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 20 | }, 21 | "[scala]": { 22 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 23 | }, 24 | "[sql]": { 25 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 26 | }, 27 | "[groovy]": { 28 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle", 29 | "spotlessGradle.diagnostics.enable": true 30 | }, 31 | "[gradle]": { 32 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle", 33 | "spotlessGradle.diagnostics.enable": true 34 | }, 35 | "[javascript]": { 36 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 37 | }, 38 | "[javascriptreact]": { 39 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 40 | }, 41 | "[typescript]": { 42 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle", 43 | "spotlessGradle.diagnostics.enable": true 44 | }, 45 | "[typescriptreact]": { 46 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 47 | }, 48 | "[css]": { 49 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 50 | }, 51 | "[scss]": { 52 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 53 | }, 54 | "[less]": { 55 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 56 | }, 57 | "[json]": { 58 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 59 | }, 60 | "[yaml]": { 61 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 62 | }, 63 | "[markdown]": { 64 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 65 | }, 66 | "[python]": { 67 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 68 | }, 69 | "[c]": { 70 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 71 | }, 72 | "[cpp]": { 73 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 74 | }, 75 | "[csharp]": { 76 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 77 | }, 78 | "[objective-c]": { 79 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 80 | }, 81 | "[objective-cpp]": { 82 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 83 | }, 84 | "java.configuration.updateBuildConfiguration": "automatic" 85 | } 86 | -------------------------------------------------------------------------------- /src/test/integration/gradle-project/dependencyChecker.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import * as vscode from 'vscode'; 4 | import * as sinon from 'sinon'; 5 | import * as assert from 'assert'; 6 | import { DependencyChecker } from '../../../DependencyChecker'; 7 | import { GRADLE_FOR_JAVA_EXTENSION_ID } from '../../../constants'; 8 | 9 | describe('Dependency checker', () => { 10 | afterEach(() => { 11 | sinon.restore(); 12 | }); 13 | 14 | it('should match patch versions', async () => { 15 | sinon.stub(vscode.extensions, 'getExtension').callsFake(() => { 16 | return { 17 | id: GRADLE_FOR_JAVA_EXTENSION_ID, 18 | packageJSON: { 19 | version: '1.0.2', 20 | }, 21 | isActive: true, 22 | } as vscode.Extension; 23 | }); 24 | const dependencyChecker = new DependencyChecker({ 25 | extensionDependencies: [GRADLE_FOR_JAVA_EXTENSION_ID], 26 | extensionDependenciesCompatibility: { 27 | [GRADLE_FOR_JAVA_EXTENSION_ID]: '^1.0.1', 28 | }, 29 | }); 30 | const isValid = await dependencyChecker.check(); 31 | assert.ok(isValid, 'Dependencies do not match'); 32 | }); 33 | 34 | it('should match minor versions', async () => { 35 | sinon.stub(vscode.extensions, 'getExtension').callsFake(() => { 36 | return { 37 | id: GRADLE_FOR_JAVA_EXTENSION_ID, 38 | packageJSON: { 39 | version: '1.1.0', 40 | }, 41 | isActive: true, 42 | } as vscode.Extension; 43 | }); 44 | const dependencyChecker = new DependencyChecker({ 45 | extensionDependencies: [GRADLE_FOR_JAVA_EXTENSION_ID], 46 | extensionDependenciesCompatibility: { 47 | [GRADLE_FOR_JAVA_EXTENSION_ID]: '^1.0.1', 48 | }, 49 | }); 50 | const isValid = await dependencyChecker.check(); 51 | assert.ok(isValid, 'Dependencies do not match'); 52 | }); 53 | 54 | it('should not match major versions', async () => { 55 | const errorSpy = sinon.spy(vscode.window, 'showErrorMessage'); 56 | sinon.stub(vscode.extensions, 'getExtension').callsFake(() => { 57 | return { 58 | id: GRADLE_FOR_JAVA_EXTENSION_ID, 59 | packageJSON: { 60 | version: '2.0.0', 61 | }, 62 | isActive: true, 63 | } as vscode.Extension; 64 | }); 65 | const dependencyChecker = new DependencyChecker({ 66 | extensionDependencies: [GRADLE_FOR_JAVA_EXTENSION_ID], 67 | extensionDependenciesCompatibility: { 68 | [GRADLE_FOR_JAVA_EXTENSION_ID]: '^1.0.1', 69 | }, 70 | }); 71 | const isValid = await dependencyChecker.check(); 72 | assert.equal(isValid, false, 'Dependencies match'); 73 | assert.ok( 74 | errorSpy.calledWith( 75 | 'Dependant extension versions are incompatible: vscjava.vscode-gradle@^1.0.1. Update those extensions to use this version of Spotless Gradle.', 76 | 'Install Compatible Versions' as vscode.MessageOptions 77 | ), 78 | 'Error message not shown' 79 | ); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/FeatureManager.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { 3 | CONFIG_FORMAT_ENABLE, 4 | CONFIG_DIAGNOSTICS_ENABLE, 5 | CONFIG_NAMESPACE, 6 | } from './constants'; 7 | import { Disposables } from './Disposables'; 8 | import { DocumentFormattingEditProvider } from './DocumentFormattingEditProvider'; 9 | import { 10 | getFormatDocumentSelector, 11 | getDiagnosticsDocumentSelector, 12 | } from './documentSelector'; 13 | import { FixAllCodeActionProvider } from './FixAllCodeActionProvider'; 14 | import { Spotless } from './Spotless'; 15 | import { SpotlessDiagnostics } from './SpotlessDiagnostics'; 16 | 17 | export class FeatureManager implements vscode.Disposable { 18 | private disposables = new Disposables(); 19 | private knownLanguages: string[] = []; 20 | 21 | constructor( 22 | private readonly spotless: Spotless, 23 | private readonly fixAllCodeActionProvider: FixAllCodeActionProvider, 24 | private readonly documentFormattingEditProvider: DocumentFormattingEditProvider, 25 | private readonly spotlessDiagnostics: SpotlessDiagnostics 26 | ) {} 27 | 28 | public async register(): Promise { 29 | this.knownLanguages = await vscode.languages.getLanguages(); 30 | this.spotless.onReady(this.onSpotlessReady); 31 | this.disposables.add( 32 | vscode.workspace.onDidChangeConfiguration( 33 | this.onDidChangeConfigurationHandler 34 | ) 35 | ); 36 | } 37 | 38 | public dispose(): void { 39 | this.disposables.dispose(); 40 | } 41 | 42 | private onSpotlessReady = (isReady: boolean): void => { 43 | if (isReady) { 44 | this.setEnabledLanguages(); 45 | } else { 46 | this.disableAllLanguages(); 47 | } 48 | }; 49 | 50 | private onDidChangeConfigurationHandler = async ( 51 | event: vscode.ConfigurationChangeEvent 52 | ): Promise => { 53 | if (this.spotless.isReady) { 54 | if ( 55 | event.affectsConfiguration( 56 | `${CONFIG_NAMESPACE}.${CONFIG_FORMAT_ENABLE}` 57 | ) || 58 | event.affectsConfiguration( 59 | `${CONFIG_NAMESPACE}.${CONFIG_DIAGNOSTICS_ENABLE}` 60 | ) 61 | ) { 62 | this.setEnabledLanguages(); 63 | } 64 | if ( 65 | event.affectsConfiguration( 66 | `${CONFIG_NAMESPACE}.${CONFIG_DIAGNOSTICS_ENABLE}` 67 | ) 68 | ) { 69 | this.spotlessDiagnostics.reset(); 70 | } 71 | } 72 | }; 73 | 74 | private async setEnabledLanguages(): Promise { 75 | const formatDocumentSelector = getFormatDocumentSelector( 76 | this.knownLanguages 77 | ); 78 | this.fixAllCodeActionProvider.setDocumentSelector(formatDocumentSelector); 79 | this.documentFormattingEditProvider.setDocumentSelector( 80 | formatDocumentSelector 81 | ); 82 | 83 | const diagnosticsDocumentSelector = getDiagnosticsDocumentSelector( 84 | this.knownLanguages 85 | ); 86 | this.spotlessDiagnostics.setDocumentSelector(diagnosticsDocumentSelector); 87 | } 88 | 89 | private disableAllLanguages(): void { 90 | this.spotlessDiagnostics.setDocumentSelector([]); 91 | this.fixAllCodeActionProvider.setDocumentSelector([]); 92 | this.documentFormattingEditProvider.setDocumentSelector([]); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import type { ExtensionApi as GradleApi } from 'vscode-gradle'; 5 | import { FixAllCodeActionProvider } from './FixAllCodeActionProvider'; 6 | import { logger, Logger } from './logger'; 7 | import { DocumentFormattingEditProvider } from './DocumentFormattingEditProvider'; 8 | import { Spotless } from './Spotless'; 9 | import { GRADLE_FOR_JAVA_EXTENSION_ID, OUTPUT_CHANNEL_ID } from './constants'; 10 | import { DependencyChecker } from './DependencyChecker'; 11 | import { SpotlessDiagnostics } from './SpotlessDiagnostics'; 12 | import { SpotlessRunner } from './SpotlessRunner'; 13 | import { FixAllCodeActionsCommand } from './FixAllCodeActionCommand'; 14 | import { FeatureManager } from './FeatureManager'; 15 | 16 | export interface ExtensionApi { 17 | logger: Logger; 18 | spotless: Spotless; 19 | } 20 | 21 | export async function activate( 22 | context: vscode.ExtensionContext 23 | ): Promise { 24 | logger.setLoggingChannel( 25 | vscode.window.createOutputChannel(OUTPUT_CHANNEL_ID) 26 | ); 27 | 28 | const packageJson = JSON.parse( 29 | fs.readFileSync(path.join(context.extensionPath, 'package.json'), 'utf8') 30 | ); 31 | 32 | const dependencyChecker = new DependencyChecker(packageJson); 33 | if (!dependencyChecker.check()) { 34 | return; 35 | } 36 | 37 | const gradleForJavaExtension = vscode.extensions.getExtension( 38 | GRADLE_FOR_JAVA_EXTENSION_ID 39 | ); 40 | if (!gradleForJavaExtension || !gradleForJavaExtension.isActive) { 41 | throw new Error('Gradle for Java extension is not installed/active'); 42 | } 43 | 44 | const gradleApi = gradleForJavaExtension.exports as GradleApi; 45 | const spotless = new Spotless(gradleApi); 46 | const spotlessRunner = new SpotlessRunner(spotless); 47 | const formatDocumentSelector: vscode.DocumentFilter[] = []; 48 | const diagnosticsDocumentSelector: vscode.DocumentFilter[] = []; 49 | 50 | const spotlessDiagnostics = new SpotlessDiagnostics( 51 | spotless, 52 | spotlessRunner, 53 | diagnosticsDocumentSelector 54 | ); 55 | 56 | const fixAllCodeActionsCommand = new FixAllCodeActionsCommand( 57 | context, 58 | spotlessRunner 59 | ); 60 | 61 | const fixAllCodeActionProvider = new FixAllCodeActionProvider( 62 | formatDocumentSelector 63 | ); 64 | 65 | const documentFormattingEditProvider = new DocumentFormattingEditProvider( 66 | spotlessRunner, 67 | formatDocumentSelector 68 | ); 69 | 70 | const featureManager = new FeatureManager( 71 | spotless, 72 | fixAllCodeActionProvider, 73 | documentFormattingEditProvider, 74 | spotlessDiagnostics 75 | ); 76 | 77 | context.subscriptions.push( 78 | spotless, 79 | spotlessDiagnostics, 80 | fixAllCodeActionProvider, 81 | documentFormattingEditProvider, 82 | featureManager 83 | ); 84 | 85 | await featureManager.register(); 86 | spotless.register(); 87 | fixAllCodeActionsCommand.register(); 88 | fixAllCodeActionProvider.register(); 89 | documentFormattingEditProvider.register(); 90 | spotlessDiagnostics.register(); 91 | 92 | return { logger, spotless }; 93 | } 94 | 95 | // eslint-disable-next-line @typescript-eslint/no-empty-function 96 | export function deactivate(): void {} 97 | -------------------------------------------------------------------------------- /src/test/runTests.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as cp from 'child_process'; 3 | import * as fs from 'fs-extra'; 4 | import * as os from 'os'; 5 | 6 | import { 7 | runTests, 8 | downloadAndUnzipVSCode, 9 | resolveCliPathFromVSCodeExecutablePath, 10 | } from 'vscode-test'; 11 | 12 | const VSCODE_VERSION = '1.45.0'; 13 | const extensionDevelopmentPath = path.resolve(__dirname, '../..'); 14 | 15 | function runTestWithGradle( 16 | vscodeExecutablePath: string, 17 | userDir: string 18 | ): Promise { 19 | const extensionTestsPath = path.resolve( 20 | __dirname, 21 | './integration/gradle-project/index' 22 | ); 23 | const fixturePath = path.resolve( 24 | __dirname, 25 | '../../test-fixtures/gradle-project/' 26 | ); 27 | 28 | return runTests({ 29 | vscodeExecutablePath, 30 | extensionDevelopmentPath, 31 | extensionTestsPath, 32 | launchArgs: [ 33 | fixturePath, 34 | '--disable-extension=vscjava.vscode-java-pack', 35 | '--disable-extension=redhat.java', 36 | '--disable-extension=vscjava.vscode-java-dependency', 37 | '--disable-extension=vscjava.vscode-java-test', 38 | '--disable-extension=shengchen.vscode-checkstyle', 39 | '--disable-extension=eamodio.gitlens', 40 | '--disable-extension=sonarsource.sonarlint-vscode', 41 | '--disable-extension=esbenp.prettier-vscode', 42 | `--user-data-dir=${userDir}`, 43 | ], 44 | }); 45 | } 46 | 47 | function runTestWithGradleMultiProject( 48 | vscodeExecutablePath: string, 49 | userDir: string 50 | ): Promise { 51 | const extensionTestsPath = path.resolve( 52 | __dirname, 53 | './integration/gradle-multi-project/index' 54 | ); 55 | const fixturePath = path.resolve( 56 | __dirname, 57 | '../../test-fixtures/gradle-multi-project/' 58 | ); 59 | 60 | return runTests({ 61 | vscodeExecutablePath, 62 | extensionDevelopmentPath, 63 | extensionTestsPath, 64 | launchArgs: [ 65 | fixturePath, 66 | '--disable-extension=vscjava.vscode-java-pack', 67 | '--disable-extension=redhat.java', 68 | '--disable-extension=vscjava.vscode-java-dependency', 69 | '--disable-extension=vscjava.vscode-java-test', 70 | '--disable-extension=shengchen.vscode-checkstyle', 71 | '--disable-extension=eamodio.gitlens', 72 | '--disable-extension=sonarsource.sonarlint-vscode', 73 | '--disable-extension=esbenp.prettier-vscode', 74 | `--user-data-dir=${userDir}`, 75 | ], 76 | }); 77 | } 78 | 79 | async function main(): Promise { 80 | const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-user')); 81 | fs.copySync( 82 | path.resolve(__dirname, '../../test-fixtures/vscode-user/User'), 83 | path.join(tmpDir, 'User') 84 | ); 85 | 86 | try { 87 | const vscodeExecutablePath = await downloadAndUnzipVSCode(VSCODE_VERSION); 88 | const cliPath = 89 | resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath); 90 | 91 | cp.spawnSync(cliPath, ['--install-extension', 'vscjava.vscode-gradle'], { 92 | encoding: 'utf-8', 93 | stdio: 'inherit', 94 | }); 95 | 96 | await runTestWithGradle(vscodeExecutablePath, tmpDir); 97 | await runTestWithGradleMultiProject(vscodeExecutablePath, tmpDir); 98 | } catch (err) { 99 | console.error('Failed to run tests', (err as Error).message); 100 | process.exit(1); 101 | } finally { 102 | fs.removeSync(tmpDir); 103 | } 104 | } 105 | 106 | main(); 107 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/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 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/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 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build & Publish 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | build-and-test: 12 | runs-on: ${{ matrix.os }} 13 | name: 'Build & Analyse (${{ matrix.os }} - Java ${{ matrix.java-version }} - Node ${{ matrix.node-version }})' 14 | strategy: 15 | matrix: 16 | node-version: ['16'] 17 | java-version: ['11'] 18 | os: [ubuntu-latest, windows-latest] 19 | # os: [ubuntu-latest, windows-latest, macos-latest] 20 | steps: 21 | - uses: actions/checkout@v1 22 | - name: Use Node ${{ matrix.node-version }} 23 | uses: actions/setup-node@v2.5.1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - name: Use Java ${{ matrix.java-version }} 27 | uses: actions/setup-java@v2.5.0 28 | with: 29 | java-version: ${{ matrix.java-version }} 30 | distribution: 'adopt' 31 | architecture: x64 32 | - name: Install NPM Packages 33 | run: npm ci 34 | - name: Lint extension 35 | run: npm run lint 36 | - name: Prepare Gradle 37 | uses: gradle/gradle-build-action@v2 38 | with: 39 | arguments: build -x spotlessCheck 40 | build-root-directory: test-fixtures/gradle-project 41 | gradle-executable: test-fixtures/gradle-project/gradlew 42 | - name: Prepare Spotless 43 | uses: gradle/gradle-build-action@v2 44 | with: 45 | arguments: spotlessDiagnose --no-configuration-cache 46 | build-root-directory: test-fixtures/gradle-project 47 | gradle-executable: test-fixtures/gradle-project/gradlew 48 | - name: Prepare Gradle (multi-project) 49 | uses: gradle/gradle-build-action@v2 50 | with: 51 | arguments: build -x spotlessCheck 52 | build-root-directory: test-fixtures/gradle-multi-project 53 | gradle-executable: test-fixtures/gradle-multi-project/gradlew 54 | - name: Prepare Spotless (multi-project) 55 | uses: gradle/gradle-build-action@v2 56 | with: 57 | arguments: spotlessDiagnose --no-configuration-cache 58 | build-root-directory: test-fixtures/gradle-multi-project 59 | gradle-executable: test-fixtures/gradle-multi-project/gradlew 60 | - name: Install test-fixtures NPM packages 61 | run: | 62 | npm ci --prefix test-fixtures/gradle-project 63 | - name: Start Xvfb 64 | run: | 65 | Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 66 | if: matrix.os == 'ubuntu-latest' 67 | - name: Build & Test Extension 68 | run: | 69 | npm run compile:all 70 | npm run test 71 | env: 72 | DISPLAY: ':99.0' 73 | 74 | publish: 75 | name: Publish extension 76 | runs-on: ubuntu-latest 77 | needs: [build-and-test] 78 | if: github.event_name == 'release' && github.event.action == 'published' 79 | steps: 80 | - uses: actions/checkout@v1 81 | - uses: actions/setup-node@v2.5.1 82 | with: 83 | node-version: 16 84 | - name: Install packages 85 | run: | 86 | npm install 87 | - name: Build & publish extension 88 | env: 89 | AZURE_TOKEN: ${{ secrets.AZURE_TOKEN }} 90 | VSX_REGISTRY_ACCESS_TOKEN: ${{ secrets.VSX_REGISTRY_ACCESS_TOKEN }} 91 | run: | 92 | tag=${GITHUB_REF#refs/tags/} 93 | echo "Setting package version $tag" 94 | npm --no-git-tag-version version "$tag" 95 | npm run publish -- -p "$AZURE_TOKEN" 96 | # npx ovsx publish -p "$VSX_REGISTRY_ACCESS_TOKEN" 97 | -------------------------------------------------------------------------------- /src/DependencyChecker.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import * as vscode from 'vscode'; 3 | import * as semver from 'semver'; 4 | import { 5 | INSTALL_COMPATIBLE_EXTENSION_VERSIONS, 6 | SPOTLESS_GRADLE_EXTENSION_ID, 7 | } from './constants'; 8 | 9 | export interface PackageJson { 10 | extensionDependencies: string[]; 11 | extensionDependenciesCompatibility?: { 12 | [key: string]: string; 13 | }; 14 | [key: string]: any; 15 | } 16 | 17 | export interface ExtensionVersion { 18 | id: string; 19 | required: string; 20 | compatible: boolean; 21 | } 22 | 23 | export class DependencyChecker { 24 | constructor(private readonly packageJson: PackageJson) { 25 | if (!packageJson.extensionDependenciesCompatibility) { 26 | throw new Error( 27 | `'extensionDependenciesCompatibility' not specified in packageJson` 28 | ); 29 | } 30 | } 31 | 32 | public check(): boolean { 33 | const extensions = this.getExtensionDependencies(); 34 | const extensionVersions = this.getExtensionVersions(extensions); 35 | const incompatibleExtensions = extensionVersions.filter( 36 | (extensionVersion) => !extensionVersion.compatible 37 | ); 38 | 39 | if (incompatibleExtensions.length) { 40 | this.notify(incompatibleExtensions); 41 | return false; 42 | } 43 | 44 | return true; 45 | } 46 | 47 | private getExtensionDependencies(): vscode.Extension[] { 48 | return this.packageJson.extensionDependencies 49 | .map((extensionDependency) => 50 | vscode.extensions.getExtension(extensionDependency) 51 | ) 52 | .filter( 53 | (extensionDependency) => 54 | extensionDependency && extensionDependency.isActive 55 | ) as vscode.Extension[]; 56 | } 57 | 58 | private getExtensionVersions( 59 | extensions: vscode.Extension[] 60 | ): ExtensionVersion[] { 61 | const { extensionDependenciesCompatibility: compatibleVersions } = 62 | this.packageJson; 63 | return extensions.map((extensionDependency) => { 64 | const extensionVersion = extensionDependency.packageJSON.version; 65 | const requiredVersion = compatibleVersions![extensionDependency.id]; 66 | const validRange = semver.validRange(requiredVersion); 67 | if (validRange === null) { 68 | throw new Error('Invalid version range'); 69 | } 70 | return { 71 | id: extensionDependency.id, 72 | required: requiredVersion, 73 | compatible: 74 | extensionVersion === '0.0.0' || 75 | semver.satisfies(extensionVersion, validRange), 76 | }; 77 | }); 78 | } 79 | 80 | private async notify( 81 | incompatibleExtensions: ExtensionVersion[] 82 | ): Promise { 83 | const requiredVersions = incompatibleExtensions 84 | .map((extension) => `${extension.id}@${extension.required}`) 85 | .join(', '); 86 | const message = [ 87 | `Dependant extension versions are incompatible: ${requiredVersions}.`, 88 | 'Update those extensions to use this version of Spotless Gradle.', 89 | ].join(' '); 90 | const input = await vscode.window.showErrorMessage( 91 | message, 92 | INSTALL_COMPATIBLE_EXTENSION_VERSIONS 93 | ); 94 | if (input === INSTALL_COMPATIBLE_EXTENSION_VERSIONS) { 95 | const extensionIds = incompatibleExtensions.map( 96 | (extension) => extension.id 97 | ); 98 | extensionIds.push(SPOTLESS_GRADLE_EXTENSION_ID); 99 | // From here it's up to the user to choose the correct dependency 100 | await vscode.commands.executeCommand( 101 | 'workbench.extensions.action.showExtensionsWithIds', 102 | extensionIds 103 | ); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/test/integration/gradle-multi-project/formatting.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import * as vscode from 'vscode'; 4 | import * as fs from 'fs'; 5 | import * as sinon from 'sinon'; 6 | import * as assert from 'assert'; 7 | import { 8 | formatFileWithCommand, 9 | formatFileOnSave, 10 | waitFor, 11 | multiProjectJavaAppFilePath, 12 | multiProjectJavaAppFileContents, 13 | multiProjectJavaFormattedAppFileContents, 14 | multiProjectJavaHelloFilePath, 15 | multiProjectJavaHelloFileContents, 16 | } from '../../testUtil'; 17 | import { SPOTLESS_GRADLE_EXTENSION_ID } from '../../../constants'; 18 | import { ExtensionApi } from '../../../extension'; 19 | 20 | describe('Formatting', () => { 21 | const { logger, spotless } = vscode.extensions.getExtension( 22 | SPOTLESS_GRADLE_EXTENSION_ID 23 | )!.exports as ExtensionApi; 24 | 25 | afterEach(() => { 26 | sinon.restore(); 27 | }); 28 | 29 | describe('Running Spotless', () => { 30 | const reset = async ( 31 | appFilePath: string, 32 | appFileContents: string 33 | ): Promise => { 34 | await vscode.commands.executeCommand( 35 | 'workbench.action.closeActiveEditor' 36 | ); 37 | fs.writeFileSync(appFilePath, appFileContents, 'utf8'); 38 | }; 39 | 40 | describe('Java multi project', function () { 41 | // VS Code might choose to cancel the formatting 42 | this.timeout(6000); 43 | this.retries(5); 44 | 45 | afterEach(async () => { 46 | await reset( 47 | multiProjectJavaAppFilePath, 48 | multiProjectJavaAppFileContents 49 | ); 50 | }); 51 | 52 | it('should call spotless.apply when saving a file', async () => { 53 | const spotlessSpy = sinon.spy(spotless, 'apply'); 54 | 55 | const document = await vscode.workspace.openTextDocument( 56 | multiProjectJavaAppFilePath 57 | ); 58 | await vscode.window.showTextDocument(document); 59 | vscode.commands.executeCommand('workbench.action.files.save'); 60 | 61 | await waitFor(() => spotlessSpy.calledWith(document)); 62 | }); 63 | 64 | it('should run spotless when saving a file', async () => { 65 | const loggerSpy = sinon.spy(logger, 'info'); 66 | 67 | const document = await formatFileOnSave(multiProjectJavaAppFilePath); 68 | 69 | assert.equal( 70 | document?.getText(), 71 | multiProjectJavaFormattedAppFileContents, 72 | 'The formatted document does not match the expected formatting' 73 | ); 74 | assert.equal( 75 | fs.readFileSync(multiProjectJavaHelloFilePath, 'utf8'), 76 | multiProjectJavaHelloFileContents, 77 | 'Spotless formatted multiple files' 78 | ); 79 | assert.ok( 80 | loggerSpy.calledWith('App.java: IS DIRTY'), 81 | 'Spotless status not logged' 82 | ); 83 | }); 84 | 85 | it('should run spotless when formatting a file', async () => { 86 | const loggerSpy = sinon.spy(logger, 'info'); 87 | const document = await formatFileWithCommand( 88 | multiProjectJavaAppFilePath 89 | ); 90 | assert.equal( 91 | document?.getText(), 92 | multiProjectJavaFormattedAppFileContents, 93 | 'The formatted document does not match the expected formatting' 94 | ); 95 | assert.equal( 96 | fs.readFileSync(multiProjectJavaHelloFilePath, 'utf8'), 97 | multiProjectJavaHelloFileContents, 98 | 'Spotless formatted multiple files' 99 | ); 100 | assert.equal(document?.isDirty, true, 'The document was saved'); 101 | assert.ok( 102 | loggerSpy.calledWith('App.java: IS DIRTY'), 103 | 'Spotless status not logged' 104 | ); 105 | }); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /src/Spotless.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as util from 'util'; 3 | import * as vscode from 'vscode'; 4 | import { ExtensionApi as GradleApi, RunBuildOpts } from 'vscode-gradle'; 5 | import type { Output } from 'vscode-gradle'; 6 | import { logger } from './logger'; 7 | import { getWorkspaceFolder, sanitizePath } from './util'; 8 | import { 9 | SPOTLESS_STATUSES, 10 | SPOTLESS_STATUS_IS_DIRTY, 11 | SPOTLESS_STATUS_IS_CLEAN, 12 | } from './constants'; 13 | import { Deferred } from './Deferred'; 14 | import { Disposables } from './Disposables'; 15 | 16 | const OUTPUT_STDOUT = 1; 17 | const OUTPUT_STDERR = 0; 18 | 19 | export class Spotless { 20 | private disposables = new Disposables(); 21 | private readyHandlers: Array<(isReady: boolean) => void> = []; 22 | public isReady = false; 23 | 24 | constructor(private readonly gradleApi: GradleApi) {} 25 | 26 | public register(): void { 27 | this.disposables.add( 28 | this.gradleApi 29 | .getTaskProvider() 30 | .onDidLoadTasks((tasks: vscode.Task[]) => { 31 | this.isReady = this.hasSpotlessTask(tasks); 32 | this.readyHandlers.forEach((handler) => handler(this.isReady)); 33 | }) 34 | ); 35 | this.gradleApi.getTaskProvider().provideTasks(); 36 | } 37 | 38 | public dispose(): void { 39 | this.disposables.dispose(); 40 | } 41 | 42 | public onReady(callback: (isReady: boolean) => void): void { 43 | this.readyHandlers.push(callback); 44 | } 45 | 46 | private hasSpotlessTask(tasks: vscode.Task[]): boolean { 47 | return !!tasks.find( 48 | (task) => 49 | task.name === 'spotlessApply' || task.name.endsWith(':spotlessApply') 50 | ); 51 | } 52 | 53 | public async apply( 54 | document: vscode.TextDocument, 55 | cancellationToken?: vscode.CancellationToken 56 | ): Promise { 57 | if (document.isClosed || document.isUntitled) { 58 | throw new Error( 59 | 'Document is closed or not saved, skipping spotlessApply' 60 | ); 61 | } 62 | const basename = path.basename(document.uri.fsPath); 63 | const sanitizedPath = sanitizePath(document.uri.fsPath); 64 | const args = [ 65 | 'spotlessApply', 66 | `-PspotlessIdeHook=${sanitizedPath}`, 67 | '-PspotlessIdeHookUseStdIn', 68 | '-PspotlessIdeHookUseStdOut', 69 | '--no-configuration-cache', 70 | '--quiet', 71 | ]; 72 | const workspaceFolder = getWorkspaceFolder(document.uri); 73 | const cancelledDeferred = new Deferred(); 74 | 75 | cancellationToken?.onCancellationRequested(() => 76 | cancelledDeferred.resolve(undefined) 77 | ); 78 | 79 | let stdOut = ''; 80 | let stdErr = ''; 81 | 82 | const runBuildOpts: RunBuildOpts = { 83 | projectFolder: workspaceFolder.uri.fsPath, 84 | args, 85 | input: document.getText(), 86 | showOutputColors: false, 87 | onOutput: (output: Output) => { 88 | const outputString = new util.TextDecoder('utf-8').decode( 89 | output.getOutputBytes_asU8() 90 | ); 91 | switch (output.getOutputType()) { 92 | case OUTPUT_STDOUT: 93 | stdOut += outputString; 94 | break; 95 | case OUTPUT_STDERR: 96 | stdErr += outputString; 97 | break; 98 | } 99 | }, 100 | }; 101 | 102 | logger.info(`Running spotlessApply on ${basename}`); 103 | 104 | const runBuild = this.gradleApi.runBuild(runBuildOpts); 105 | 106 | await Promise.race([runBuild, cancelledDeferred.promise]); 107 | 108 | if (cancellationToken?.isCancellationRequested) { 109 | logger.warning('Spotless formatting cancelled'); 110 | } else { 111 | const trimmedStdErr = stdErr.trim(); 112 | 113 | if (SPOTLESS_STATUSES.includes(trimmedStdErr)) { 114 | logger.info(`${basename}: ${trimmedStdErr}`); 115 | } 116 | if (trimmedStdErr === SPOTLESS_STATUS_IS_DIRTY) { 117 | return stdOut; 118 | } 119 | if (trimmedStdErr !== SPOTLESS_STATUS_IS_CLEAN) { 120 | throw new Error(trimmedStdErr || 'No status received from Spotless'); 121 | } 122 | } 123 | return null; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/test/integration/gradle-project/diagnostics.test.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import * as sinon from 'sinon'; 5 | import * as assert from 'assert'; 6 | import { SPOTLESS_GRADLE_EXTENSION_ID } from '../../../constants'; 7 | import { ExtensionApi } from '../../../extension'; 8 | import { 9 | javaAppFileContents, 10 | javaAppFilePath, 11 | javaBasePath, 12 | waitFor, 13 | waitForDiagnostics, 14 | } from '../../testUtil'; 15 | 16 | async function waitForDiagnosticsOnDocumentOpen( 17 | appFilePath: string, 18 | message: string 19 | ): Promise { 20 | const document = await vscode.workspace.openTextDocument(appFilePath); 21 | await vscode.window.showTextDocument(document); 22 | await waitForDiagnostics(message); 23 | return document; 24 | } 25 | 26 | describe('Diagnostics', () => { 27 | const { logger } = vscode.extensions.getExtension( 28 | SPOTLESS_GRADLE_EXTENSION_ID 29 | )!.exports as ExtensionApi; 30 | 31 | afterEach(() => { 32 | sinon.restore(); 33 | }); 34 | 35 | describe('Running Spotless', () => { 36 | const reset = async ( 37 | appFilePath: string, 38 | appFileContents: string 39 | ): Promise => { 40 | await vscode.commands.executeCommand( 41 | 'workbench.action.closeActiveEditor' 42 | ); 43 | fs.writeFileSync(appFilePath, appFileContents, 'utf8'); 44 | }; 45 | 46 | describe('Java', function () { 47 | afterEach(async () => { 48 | await reset(javaAppFilePath, javaAppFileContents); 49 | }); 50 | 51 | it('should provide spotless diagnostics when opening a text document', async () => { 52 | const loggerSpy = sinon.spy(logger, 'info'); 53 | await waitForDiagnosticsOnDocumentOpen( 54 | javaAppFilePath, 55 | 'Replace public·static·void·main(String[]·args)·{System.out.println("app");} with ⏎··public·static·void·main(String[]·args)·{⏎····System.out.println("app");⏎··}⏎' 56 | ); 57 | assert.ok(loggerSpy.calledWith('App.java: IS DIRTY')); 58 | assert.ok( 59 | loggerSpy.calledWith( 60 | 'Updated diagnostics (language: java) (total: 1)' 61 | ) 62 | ); 63 | }); 64 | 65 | it('should provide spotless diagnostics when changing a text document', async () => { 66 | const loggerSpy = sinon.spy(logger, 'info'); 67 | 68 | const document = await waitForDiagnosticsOnDocumentOpen( 69 | javaAppFilePath, 70 | 'Replace public·static·void·main(String[]·args)·{System.out.println("app");} with ⏎··public·static·void·main(String[]·args)·{⏎····System.out.println("app");⏎··}⏎' 71 | ); 72 | const workspaceEdit = new vscode.WorkspaceEdit(); 73 | workspaceEdit.insert(document.uri, new vscode.Position(0, 0), ' '); 74 | await vscode.workspace.applyEdit(workspaceEdit); 75 | await waitForDiagnostics('Delete ··'); 76 | assert.ok(loggerSpy.calledWith('App.java: IS DIRTY')); 77 | assert.ok( 78 | loggerSpy.calledWith( 79 | 'Updated diagnostics (language: java) (total: 2)' 80 | ) 81 | ); 82 | }); 83 | }); 84 | 85 | describe('Error path', () => { 86 | describe('Invalid supported files', () => { 87 | const invalidFilePath = path.resolve(javaBasePath, 'AppInvalid.java'); 88 | 89 | before(() => { 90 | fs.copyFileSync( 91 | path.resolve(javaBasePath, 'AppInvalid.java.txt'), 92 | invalidFilePath 93 | ); 94 | }); 95 | 96 | after(() => { 97 | fs.unlinkSync(invalidFilePath); 98 | }); 99 | 100 | it('should log errors when linting invalid Java files', async () => { 101 | const loggerSpy = sinon.spy(logger, 'error'); 102 | const document = await vscode.workspace.openTextDocument( 103 | invalidFilePath 104 | ); 105 | await vscode.window.showTextDocument(document); 106 | waitFor((): boolean => 107 | loggerSpy.calledWith(sinon.match('Unable to provide diagnostics')) 108 | ); 109 | }); 110 | }); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-spotless-gradle", 3 | "displayName": "Spotless Gradle", 4 | "description": "Format your source files using Spotless via Gradle", 5 | "version": "0.0.0", 6 | "private": true, 7 | "publisher": "richardwillis", 8 | "readme": "README.md", 9 | "author": "Richard Willis ", 10 | "license": "SEE LICENSE IN LICENSE.md", 11 | "engines": { 12 | "vscode": "^1.45.0", 13 | "node": "^16", 14 | "npm": "^8" 15 | }, 16 | "icon": "icon.png", 17 | "bugs": { 18 | "url": "https://github.com/badsyntax/vscode-spotless-gradle/issues" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/badsyntax/vscode-spotless-gradle/" 23 | }, 24 | "extensionDependencies": [ 25 | "vscjava.vscode-gradle" 26 | ], 27 | "extensionDependenciesCompatibility": { 28 | "vscjava.vscode-gradle": "^3.6.2" 29 | }, 30 | "categories": [ 31 | "Formatters", 32 | "Linters" 33 | ], 34 | "keywords": [ 35 | "spotless", 36 | "formatter", 37 | "linter", 38 | "format", 39 | "lint", 40 | "gradle", 41 | "java", 42 | "groovy", 43 | "scala", 44 | "kotlin", 45 | "python", 46 | "black", 47 | "javascript", 48 | "prettier", 49 | "clang", 50 | "clang-format", 51 | "c", 52 | "cpp", 53 | "csharp", 54 | "objective-c", 55 | "objective-cpp" 56 | ], 57 | "activationEvents": [ 58 | "workspaceContains:gradlew", 59 | "workspaceContains:gradlew.bat" 60 | ], 61 | "main": "./dist/extension", 62 | "contributes": { 63 | "configuration": { 64 | "id": "spotlessGradle", 65 | "type": "object", 66 | "title": "Spotless Gradle", 67 | "properties": { 68 | "spotlessGradle.format.enable": { 69 | "type": "boolean", 70 | "default": false, 71 | "scope": "language-overridable", 72 | "description": "Enable/disable formatting" 73 | }, 74 | "spotlessGradle.diagnostics.enable": { 75 | "type": "boolean", 76 | "default": false, 77 | "scope": "language-overridable", 78 | "description": "Enable/disable diagnostics" 79 | } 80 | } 81 | } 82 | }, 83 | "scripts": { 84 | "vscode:prepublish": "npm run compile:prod", 85 | "build": "npm run compile:dev", 86 | "lint": "npm run lint:prettier && npm run lint:eslint", 87 | "lint:fix": "npm run lint:fix:prettier && npm run lint:eslint -- --fix", 88 | "lint:prettier": "prettier --check \"**/*.{ts,js,json,svg,md,yml}\"", 89 | "lint:fix:prettier": "prettier --write '**/*.{ts,js,json,svg,md,yml}'", 90 | "lint:eslint": "eslint . --ext .ts", 91 | "test": "node ./out/test/runTests.js", 92 | "install:ext": "code --install-extension vscode-spotless-gradle-0.0.0.vsix --force", 93 | "preinstall:ext": "npm run package", 94 | "snyk-protect": "snyk protect", 95 | "prepare": "npm run snyk-protect", 96 | "compile:dev": "webpack --mode development", 97 | "compile:prod": "webpack --mode production", 98 | "compile:test": "tsc -p ./", 99 | "compile:all": "npm run compile:dev && npm run compile:test", 100 | "install:vscode-gradle": "npm install $(npm pack ../vscode-gradle/npm-package | tail -1)", 101 | "package": "vsce package", 102 | "publish": "vsce publish" 103 | }, 104 | "devDependencies": { 105 | "@types/fs-extra": "^9.0.13", 106 | "@types/glob": "^7.2.0", 107 | "@types/mocha": "^9.1.0", 108 | "@types/node": "^17.0.16", 109 | "@types/prettier-linter-helpers": "^1.0.1", 110 | "@types/semver": "^7.3.9", 111 | "@types/sinon": "^10.0.11", 112 | "@types/vscode": "^1.45.0", 113 | "@typescript-eslint/eslint-plugin": "^5.11.0", 114 | "@typescript-eslint/parser": "^5.11.0", 115 | "eslint": "^8.8.0", 116 | "eslint-config-prettier": "^8.3.0", 117 | "eslint-plugin-prettier": "^4.0.0", 118 | "eslint-plugin-sonarjs": "^0.11.0", 119 | "fs-extra": "^10.0.0", 120 | "glob": "^7.2.0", 121 | "mocha": "^9.2.0", 122 | "prettier": "^2.5.1", 123 | "sinon": "^13.0.1", 124 | "snyk": "^1.849.0", 125 | "ts-loader": "^9.2.6", 126 | "typescript": "^4.5.5", 127 | "vsce": "^2.6.7", 128 | "vscode-test": "^1.6.1", 129 | "webpack": "^5.68.0", 130 | "webpack-cli": "^4.9.2" 131 | }, 132 | "dependencies": { 133 | "prettier-linter-helpers": "^1.0.0", 134 | "semver": "^7.3.5", 135 | "vscode-gradle": "^3.10.1" 136 | }, 137 | "snyk": true 138 | } 139 | -------------------------------------------------------------------------------- /src/test/testUtil.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as vscode from 'vscode'; 4 | import { DIAGNOSTICS_ID } from '../constants'; 5 | 6 | export const javaBasePath = path.resolve( 7 | __dirname, 8 | '../../test-fixtures/gradle-project/src/main/java/gradle/project' 9 | ); 10 | export const multiProjectJavaBasePath = path.resolve( 11 | __dirname, 12 | '../../test-fixtures/gradle-multi-project/app/src/main/java/gradle/project' 13 | ); 14 | export const groovyBasePath = path.resolve( 15 | __dirname, 16 | '../../test-fixtures/gradle-project/src/main/groovy/gradle/project' 17 | ); 18 | export const typeScriptBasePath = path.resolve( 19 | __dirname, 20 | '../../test-fixtures/gradle-project/src/main/typescript' 21 | ); 22 | 23 | export const javaAppFilePath = path.resolve(javaBasePath, 'App.java'); 24 | export const javaAppFileContents = fs.readFileSync(javaAppFilePath, 'utf8'); 25 | export const javaHelloFilePath = path.resolve(javaBasePath, 'Hello.java'); 26 | export const javaHelloFileContents = fs.readFileSync(javaHelloFilePath, 'utf8'); 27 | export const javaFormattedAppFilePath = path.resolve( 28 | javaBasePath, 29 | 'App.java.formatted.txt' 30 | ); 31 | export const javaFormattedAppFileContents = fs.readFileSync( 32 | javaFormattedAppFilePath, 33 | 'utf8' 34 | ); 35 | export const multiProjectJavaAppFilePath = path.resolve( 36 | multiProjectJavaBasePath, 37 | 'App.java' 38 | ); 39 | export const multiProjectJavaAppFileContents = fs.readFileSync( 40 | multiProjectJavaAppFilePath, 41 | 'utf8' 42 | ); 43 | export const multiProjectJavaHelloFilePath = path.resolve( 44 | multiProjectJavaBasePath, 45 | 'Hello.java' 46 | ); 47 | export const multiProjectJavaHelloFileContents = fs.readFileSync( 48 | multiProjectJavaHelloFilePath, 49 | 'utf8' 50 | ); 51 | export const multiProjectJavaFormattedAppFilePath = path.resolve( 52 | multiProjectJavaBasePath, 53 | 'App.java.formatted.txt' 54 | ); 55 | export const multiProjectJavaFormattedAppFileContents = fs.readFileSync( 56 | multiProjectJavaFormattedAppFilePath, 57 | 'utf8' 58 | ); 59 | export const typeScriptAppFilePath = path.resolve(typeScriptBasePath, 'App.ts'); 60 | export const typeScriptAppFileContents = fs.readFileSync( 61 | typeScriptAppFilePath, 62 | 'utf8' 63 | ); 64 | export const typeScriptFormattedAppFilePath = path.resolve( 65 | typeScriptBasePath, 66 | 'App.ts.formatted.txt' 67 | ); 68 | export const typeScriptFormattedAppFileContents = fs.readFileSync( 69 | typeScriptFormattedAppFilePath, 70 | 'utf8' 71 | ); 72 | export const groovyAppFilePath = path.resolve(groovyBasePath, 'App.groovy'); 73 | export const groovyAppFileContents = fs.readFileSync(groovyAppFilePath, 'utf8'); 74 | export const groovyFormattedAppFilePath = path.resolve( 75 | groovyBasePath, 76 | 'App.groovy.formatted.txt' 77 | ); 78 | export const groovyFormattedAppFileContents = fs.readFileSync( 79 | groovyFormattedAppFilePath, 80 | 'utf8' 81 | ); 82 | 83 | export async function formatFileOnSave( 84 | appFilePath: string 85 | ): Promise { 86 | const appFileContents = fs.readFileSync(appFilePath, 'utf8'); 87 | const document = await vscode.workspace.openTextDocument(appFilePath); 88 | return new Promise(async (resolve) => { 89 | const disposable = vscode.workspace.onDidSaveTextDocument( 90 | (savedDocument: vscode.TextDocument) => { 91 | if ( 92 | savedDocument === document && 93 | document.getText() !== appFileContents 94 | ) { 95 | disposable.dispose(); 96 | resolve(document); 97 | } 98 | } 99 | ); 100 | await vscode.window.showTextDocument(document); 101 | vscode.commands.executeCommand('workbench.action.files.save'); 102 | }); 103 | } 104 | 105 | export async function formatFileWithCommand( 106 | appFilePath: string 107 | ): Promise { 108 | const appFileContents = fs.readFileSync(appFilePath, 'utf8'); 109 | const document = await vscode.workspace.openTextDocument(appFilePath); 110 | return new Promise(async (resolve) => { 111 | const disposable = vscode.workspace.onDidChangeTextDocument( 112 | (e: vscode.TextDocumentChangeEvent) => { 113 | // This handler is called for various reasons. Bail out if there 114 | // are no content changes. 115 | if (e.document !== document || !e.contentChanges.length) { 116 | return; 117 | } 118 | // This timeout is required to allow the document state to be updated. 119 | // EG: document.isDirty 120 | setTimeout(() => { 121 | if (document.getText() !== appFileContents) { 122 | disposable.dispose(); 123 | resolve(document); 124 | } 125 | }, 1); 126 | } 127 | ); 128 | await vscode.window.showTextDocument(document); 129 | await vscode.commands.executeCommand('editor.action.formatDocument'); 130 | }); 131 | } 132 | 133 | export function waitForDiagnostics( 134 | message: string, 135 | source = DIAGNOSTICS_ID 136 | ): Promise { 137 | return new Promise(async (resolve) => { 138 | const disposable = vscode.languages.onDidChangeDiagnostics(() => { 139 | const diagnostics = vscode.languages.getDiagnostics( 140 | vscode.window.activeTextEditor!.document.uri 141 | ); 142 | const hasSpotlessDiagnostic = diagnostics.find( 143 | (diagnostic) => 144 | diagnostic.source === source && diagnostic.message === message 145 | ); 146 | if (hasSpotlessDiagnostic) { 147 | disposable.dispose(); 148 | resolve(); 149 | } 150 | }); 151 | }); 152 | } 153 | 154 | export function waitFor(func: () => boolean): Promise { 155 | const interval = 50; 156 | return new Promise((resolve) => { 157 | async function check(): Promise { 158 | if (func()) { 159 | resolve(); 160 | } else { 161 | setTimeout(check, interval); 162 | } 163 | } 164 | check(); 165 | }); 166 | } 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spotless Gradle 2 | 3 | [![Marketplace Version](https://vsmarketplacebadge.apphb.com/version-short/richardwillis.vscode-spotless-gradle.svg)](https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle) 4 | [![Visual Studio Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/richardwillis.vscode-spotless-gradle)](https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle) 5 | [![Build & Publish](https://github.com/badsyntax/vscode-spotless-gradle/workflows/Build%20&%20Publish/badge.svg)](https://github.com/badsyntax/vscode-spotless-gradle/actions?query=workflow%3A"Build+%26+Publish") 6 | [![CodeQL](https://github.com/badsyntax/vscode-spotless-gradle/workflows/CodeQL/badge.svg)](https://github.com/badsyntax/vscode-spotless-gradle/actions?query=workflow%3ACodeQL) 7 | [![GitHub bug issues](https://img.shields.io/github/issues/badsyntax/vscode-spotless-gradle/bug?label=bug%20reports)](https://github.com/badsyntax/vscode-spotless-gradle/issues?q=is%3Aissue+is%3Aopen+label%3Abug) 8 | 9 | A VS Code extension to lint & format your code using [Spotless](https://github.com/diffplug/spotless) (via Gradle). 10 | 11 | Spotless Gradle Screencast 12 | 13 | ## Features 14 | 15 | - Provides diagnostics to show invalid formatting (with quick fixes) 16 | - Provides a Spotless fixAll code action (`Format on Save`) 17 | - Provides a Spotless formatter (`Format Document`) 18 | - Supports all languages that Spotless supports 19 | 20 | ## Requirements 21 | 22 | - [VS Code >= 1.45.0](https://code.visualstudio.com/download) 23 | - [Gradle for Java Extension >= 3.5.2](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-gradle) 24 | - [Spotless Gradle Plugin >= 3.30.0](https://github.com/diffplug/spotless/tree/main/plugin-gradle) 25 | - [Java >= 8](https://adoptopenjdk.net/) 26 | 27 | ## Usage 28 | 29 | Before using this extension, ensure you've [configured Spotless](https://github.com/diffplug/spotless/tree/main/plugin-gradle) correctly in your Gradle build file. (Run `./gradlew spotlessDiagnose` to prepare & validate Spotless.) 30 | 31 | ### Enabling Spotless 32 | 33 | Spotless formatting & diagnostics are _disabled by default_. Change the settings to adjust this behavior: 34 | 35 | ```json 36 | { 37 | "[java]": { 38 | "spotlessGradle.format.enable": true, 39 | "spotlessGradle.diagnostics.enable": true 40 | } 41 | } 42 | ``` 43 | 44 | #### Formatting 45 | 46 | Format on save: 47 | 48 | ```json 49 | { 50 | "[java]": { 51 | "spotlessGradle.format.enable": true, 52 | "editor.codeActionsOnSave": { 53 | "source.fixAll.spotlessGradle": true 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | If there are multiple formatters for a language type, set Spotless to be the default: 60 | 61 | ```json 62 | { 63 | "[java]": { 64 | "spotlessGradle.format.enable": true, 65 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 66 | } 67 | } 68 | ``` 69 | 70 | Disable other formatters to improve the performance, for example: 71 | 72 | ```json 73 | { 74 | "[java]": { 75 | "spotlessGradle.format.enable": true, 76 | "files.trimTrailingWhitespace": false 77 | } 78 | } 79 | ``` 80 | 81 | ### Typical Usage 82 | 83 | As Spotless formatting is not a global feature, you should enable Spotless on a per-project basis, which is achieved by adding a `.vscode/settings.json` file to the root of the project 84 | 85 | Enable for specific languages: 86 | 87 | ```json 88 | { 89 | "java.format.enabled": false, 90 | "[java]": { 91 | "files.trimTrailingWhitespace": false, 92 | "spotlessGradle.diagnostics.enable": true, 93 | "spotlessGradle.format.enable": true, 94 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle", 95 | "editor.codeActionsOnSave": { 96 | "source.fixAll.spotlessGradle": true 97 | } 98 | }, 99 | "[gradle]": { 100 | "files.trimTrailingWhitespace": false, 101 | "spotlessGradle.diagnostics.enable": true, 102 | "spotlessGradle.format.enable": true, 103 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle", 104 | "editor.codeActionsOnSave": { 105 | "source.fixAll.spotlessGradle": true 106 | } 107 | } 108 | } 109 | ``` 110 | 111 | Enable for all languages: 112 | 113 | ```json 114 | { 115 | "java.format.enabled": false, 116 | "files.trimTrailingWhitespace": false, 117 | "spotlessGradle.diagnostics.enable": true, 118 | "spotlessGradle.format.enable": true, 119 | "editor.codeActionsOnSave": { 120 | "source.fixAll.spotlessGradle": true 121 | }, 122 | "[java]": { 123 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 124 | }, 125 | "[gradle]": { 126 | "editor.defaultFormatter": "richardwillis.vscode-spotless-gradle" 127 | } 128 | } 129 | ``` 130 | 131 | ## How it Works 132 | 133 | This extension runs the `spotlessApply` Gradle task on the focused file using the Spotless [IDE hook](https://github.com/diffplug/spotless/blob/main/plugin-gradle/IDE_HOOK.md) feature. Untitled/Unsaved files are ignored. 134 | 135 | The vscode => Spotless interface is provided by the [Gradle for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-gradle) extension. 136 | 137 | 👉 [Architecture Overview](./ARCHITECTURE.md) 138 | 139 | ## Troubleshooting 140 | 141 | View logs by selecting `Spotless Gradle` and/or `Gradle for Java` in the output panel. 142 | 143 | ## Support 144 | 145 | - 👉 [Submit a bug report](https://github.com/badsyntax/vscode-spotless-gradle/issues/new?assignees=badsyntax&labels=bug&template=bug_report.md&title=) 146 | - 👉 [Submit a feature request](https://github.com/badsyntax/vscode-spotless-gradle/issues/new?assignees=badsyntax&labels=enhancement&template=feature_request.md&title=) 147 | 148 | ## Credits 149 | 150 | - Thanks to [Ned Twigg](https://github.com/nedtwigg) for adapting Spotless for better IDE integration 151 | - Thanks to all the [Spotless contributors](https://github.com/diffplug/spotless#acknowledgements) 152 | 153 | ## Release Notes 154 | 155 | See [CHANGELOG.md](./CHANGELOG.md). 156 | 157 | ## License 158 | 159 | See [LICENSE.md](./LICENSE.md). 160 | -------------------------------------------------------------------------------- /test-fixtures/gradle-project/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or 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 UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /test-fixtures/gradle-multi-project/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or 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 UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /src/test/integration/gradle-project/formatting.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import * as vscode from 'vscode'; 4 | import * as path from 'path'; 5 | import * as fs from 'fs'; 6 | import * as sinon from 'sinon'; 7 | import * as assert from 'assert'; 8 | import { 9 | formatFileWithCommand, 10 | formatFileOnSave, 11 | waitFor, 12 | javaAppFileContents, 13 | javaAppFilePath, 14 | groovyAppFileContents, 15 | groovyAppFilePath, 16 | groovyFormattedAppFileContents, 17 | javaFormattedAppFileContents, 18 | javaHelloFileContents, 19 | javaHelloFilePath, 20 | javaBasePath, 21 | } from '../../testUtil'; 22 | import { SPOTLESS_GRADLE_EXTENSION_ID } from '../../../constants'; 23 | import { ExtensionApi } from '../../../extension'; 24 | 25 | describe('Formatting', () => { 26 | const { logger, spotless } = vscode.extensions.getExtension( 27 | SPOTLESS_GRADLE_EXTENSION_ID 28 | )!.exports as ExtensionApi; 29 | 30 | afterEach(() => { 31 | sinon.restore(); 32 | }); 33 | 34 | describe('Running Spotless', () => { 35 | const reset = async ( 36 | appFilePath: string, 37 | appFileContents: string 38 | ): Promise => { 39 | await vscode.commands.executeCommand( 40 | 'workbench.action.closeActiveEditor' 41 | ); 42 | fs.writeFileSync(appFilePath, appFileContents, 'utf8'); 43 | }; 44 | 45 | describe('Java', function () { 46 | // VS Code might choose to cancel the formatting 47 | this.timeout(6000); 48 | this.retries(5); 49 | 50 | afterEach(async () => { 51 | await reset(javaAppFilePath, javaAppFileContents); 52 | }); 53 | 54 | it('should call spotless.apply when saving a file', async () => { 55 | const spotlessSpy = sinon.spy(spotless, 'apply'); 56 | 57 | const document = await vscode.workspace.openTextDocument( 58 | javaAppFilePath 59 | ); 60 | await vscode.window.showTextDocument(document); 61 | vscode.commands.executeCommand('workbench.action.files.save'); 62 | 63 | await waitFor(() => spotlessSpy.calledWith(document)); 64 | }); 65 | 66 | it('should run spotless when saving a file', async () => { 67 | const loggerSpy = sinon.spy(logger, 'info'); 68 | 69 | const document = await formatFileOnSave(javaAppFilePath); 70 | 71 | assert.equal( 72 | document?.getText(), 73 | javaFormattedAppFileContents, 74 | 'The formatted document does not match the expected formatting' 75 | ); 76 | assert.equal( 77 | fs.readFileSync(javaHelloFilePath, 'utf8'), 78 | javaHelloFileContents, 79 | 'Spotless formatted multiple files' 80 | ); 81 | assert.ok( 82 | loggerSpy.calledWith('App.java: IS DIRTY'), 83 | 'Spotless status not logged' 84 | ); 85 | }); 86 | 87 | it('should run spotless when formatting a file', async () => { 88 | const loggerSpy = sinon.spy(logger, 'info'); 89 | const document = await formatFileWithCommand(javaAppFilePath); 90 | assert.equal( 91 | document?.getText(), 92 | javaFormattedAppFileContents, 93 | 'The formatted document does not match the expected formatting' 94 | ); 95 | assert.equal( 96 | fs.readFileSync(javaHelloFilePath, 'utf8'), 97 | javaHelloFileContents, 98 | 'Spotless formatted multiple files' 99 | ); 100 | assert.equal(document?.isDirty, true, 'The document was saved'); 101 | assert.ok( 102 | loggerSpy.calledWith('App.java: IS DIRTY'), 103 | 'Spotless status not logged' 104 | ); 105 | }); 106 | }); 107 | 108 | describe('Groovy', function () { 109 | // VS Code might choose to cancel the formatting 110 | this.timeout(6000); 111 | this.retries(5); 112 | 113 | afterEach(async () => { 114 | await reset(groovyAppFilePath, groovyAppFileContents); 115 | }); 116 | 117 | it('should run spotless when saving a file', async () => { 118 | const loggerSpy = sinon.spy(logger, 'info'); 119 | const document = await formatFileOnSave(groovyAppFilePath); 120 | assert.equal( 121 | document?.getText(), 122 | groovyFormattedAppFileContents, 123 | 'The formatted document does not match the expected formatting' 124 | ); 125 | assert.ok( 126 | loggerSpy.calledWith('App.groovy: IS DIRTY'), 127 | 'Spotless status not logged' 128 | ); 129 | }); 130 | 131 | it('should run spotless when formatting a file', async () => { 132 | const loggerSpy = sinon.spy(logger, 'info'); 133 | const document = await formatFileWithCommand(groovyAppFilePath); 134 | assert.equal( 135 | document?.getText(), 136 | groovyFormattedAppFileContents, 137 | 'The formatted document does not match the expected formatting' 138 | ); 139 | assert.equal(document?.isDirty, true, 'The document was saved'); 140 | assert.ok( 141 | loggerSpy.calledWith('App.groovy: IS DIRTY'), 142 | 'Spotless status not logged' 143 | ); 144 | }); 145 | }); 146 | 147 | describe('Error path', () => { 148 | const invalidFilePath = path.resolve(javaBasePath, 'AppInvalid.java'); 149 | 150 | before(() => { 151 | fs.copyFileSync( 152 | path.resolve(javaBasePath, 'AppInvalid.java.txt'), 153 | invalidFilePath 154 | ); 155 | }); 156 | 157 | after(() => { 158 | fs.unlinkSync(invalidFilePath); 159 | }); 160 | 161 | it('should log errors when formatting invalid Java files', async () => { 162 | const loggerSpy = sinon.spy(logger, 'error'); 163 | const document = await vscode.workspace.openTextDocument( 164 | invalidFilePath 165 | ); 166 | await vscode.window.showTextDocument(document); 167 | await vscode.commands.executeCommand('editor.action.formatDocument'); 168 | assert.ok( 169 | loggerSpy.calledWith(sinon.match('Unable to apply formatting')), 170 | 'Spotless formatting error not logged' 171 | ); 172 | }); 173 | }); 174 | }); 175 | 176 | // We can't test for .kt, .scala, .graphql or .vue as they're not known language identifiers 177 | // See: https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers 178 | describe('Supported language types', async () => { 179 | const basePath = path.resolve( 180 | __dirname, 181 | '../../../../test-fixtures/gradle-project/src/main/resources/language-types' 182 | ); 183 | const files = fs.readdirSync(basePath); 184 | 185 | files.forEach((file) => { 186 | describe(file, () => { 187 | it('should format with spotless', async () => { 188 | const spotlessApplySpy = sinon.spy(spotless, 'apply'); 189 | const filePath = path.resolve(basePath, file); 190 | const document = await vscode.workspace.openTextDocument(filePath); 191 | await vscode.window.showTextDocument(document); 192 | await vscode.commands.executeCommand('editor.action.formatDocument'); 193 | assert.ok( 194 | spotlessApplySpy.calledWith(document, sinon.match.any), 195 | 'Spotless was not called' 196 | ); 197 | }); 198 | }); 199 | }); 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /src/SpotlessDiagnostics.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { 3 | Difference, 4 | generateDifferences, 5 | showInvisibles, 6 | } from 'prettier-linter-helpers'; 7 | import { Spotless } from './Spotless'; 8 | import { logger } from './logger'; 9 | import { SpotlessRunner } from './SpotlessRunner'; 10 | import { AsyncWait } from './AsyncWait'; 11 | import { FixAllCodeActionsCommand } from './FixAllCodeActionCommand'; 12 | import { DIAGNOSTICS_ID, DIAGNOSTICS_SOURCE_ID } from './constants'; 13 | import { Disposables } from './Disposables'; 14 | 15 | export interface SpotlessDiff { 16 | source: string; 17 | formattedSource: string; 18 | differences: Difference[]; 19 | } 20 | 21 | export class SpotlessDiagnostics 22 | extends AsyncWait 23 | implements vscode.CodeActionProvider, vscode.Disposable 24 | { 25 | public static readonly quickFixCodeActionKind = 26 | vscode.CodeActionKind.QuickFix.append('spotlessGradle'); 27 | public static metadata: vscode.CodeActionProviderMetadata = { 28 | providedCodeActionKinds: [SpotlessDiagnostics.quickFixCodeActionKind], 29 | }; 30 | 31 | private disposables = new Disposables(); 32 | private diagnosticCollection: vscode.DiagnosticCollection; 33 | private diagnosticDifferenceMap: Map = 34 | new Map(); 35 | private codeActionsProvider: vscode.Disposable | undefined; 36 | 37 | constructor( 38 | private readonly spotless: Spotless, 39 | private readonly spotlessRunner: SpotlessRunner, 40 | private documentSelector: Array 41 | ) { 42 | super(); 43 | this.diagnosticCollection = 44 | vscode.languages.createDiagnosticCollection(DIAGNOSTICS_ID); 45 | } 46 | 47 | public register(): void { 48 | this.registerCodeActionsProvider(); 49 | this.registerEditorEvents(); 50 | } 51 | 52 | public dispose(): void { 53 | this.diagnosticCollection.dispose(); 54 | this.codeActionsProvider?.dispose(); 55 | this.disposables.dispose(); 56 | } 57 | 58 | public reset(): void { 59 | this.diagnosticCollection.clear(); 60 | this.diagnosticDifferenceMap.clear(); 61 | } 62 | 63 | public setDocumentSelector( 64 | documentSelector: Array 65 | ): void { 66 | this.documentSelector = documentSelector; 67 | this.reset(); 68 | this.codeActionsProvider?.dispose(); 69 | this.registerCodeActionsProvider(); 70 | } 71 | 72 | private handleChangeTextDocument(document: vscode.TextDocument): void { 73 | void this.runDiagnostics(document); 74 | } 75 | 76 | public async runDiagnostics( 77 | document: vscode.TextDocument, 78 | cancellationToken?: vscode.CancellationToken 79 | ): Promise { 80 | const shouldRunDiagnostics = 81 | this.spotless.isReady && 82 | this.documentSelector.find( 83 | (selector) => selector.language === document.languageId 84 | ) && 85 | vscode.workspace.getWorkspaceFolder(document.uri); 86 | if (shouldRunDiagnostics) { 87 | this.waitAndRun(async () => { 88 | try { 89 | const diff = await this.getDiff(document, cancellationToken); 90 | this.updateDiagnostics(document, diff); 91 | } catch (e) { 92 | logger.error( 93 | `Unable to provide diagnostics: ${(e as Error).message}` 94 | ); 95 | } 96 | }); 97 | } 98 | } 99 | 100 | public updateDiagnostics( 101 | document: vscode.TextDocument, 102 | diff: SpotlessDiff 103 | ): void { 104 | const diagnostics = this.getDiagnostics(document, diff); 105 | this.diagnosticCollection.set(document.uri, diagnostics); 106 | logger.info( 107 | `Updated diagnostics (language: ${document.languageId}) (total: ${diagnostics.length})` 108 | ); 109 | } 110 | 111 | private registerEditorEvents(): void { 112 | this.spotless.onReady((isReady: boolean) => { 113 | if (isReady) { 114 | const activeDocument = vscode.window.activeTextEditor?.document; 115 | if (activeDocument) { 116 | void this.runDiagnostics(activeDocument); 117 | } 118 | } 119 | }); 120 | 121 | const onDidChangeTextDocument = vscode.workspace.onDidChangeTextDocument( 122 | (e: vscode.TextDocumentChangeEvent) => { 123 | if ( 124 | e.contentChanges.length && 125 | vscode.window.activeTextEditor?.document === e.document 126 | ) { 127 | this.handleChangeTextDocument(e.document); 128 | } 129 | } 130 | ); 131 | 132 | const onDidChangeActiveTextEditor = 133 | vscode.window.onDidChangeActiveTextEditor( 134 | (editor?: vscode.TextEditor) => { 135 | if (editor) { 136 | void this.runDiagnostics(editor.document); 137 | } 138 | } 139 | ); 140 | 141 | this.disposables.add( 142 | onDidChangeTextDocument, 143 | onDidChangeActiveTextEditor, 144 | this.diagnosticCollection 145 | ); 146 | } 147 | 148 | private registerCodeActionsProvider(): void { 149 | this.codeActionsProvider = vscode.languages.registerCodeActionsProvider( 150 | this.documentSelector, 151 | this, 152 | SpotlessDiagnostics.metadata 153 | ); 154 | } 155 | 156 | private getDiagnostics( 157 | document: vscode.TextDocument, 158 | diff: SpotlessDiff 159 | ): vscode.Diagnostic[] { 160 | const diagnostics: vscode.Diagnostic[] = []; 161 | for (const difference of diff.differences) { 162 | const diagnostic = this.getDiagnostic(document, difference); 163 | this.diagnosticDifferenceMap.set(diagnostic, difference); 164 | diagnostics.push(diagnostic); 165 | } 166 | return diagnostics; 167 | } 168 | 169 | private getDiagnostic( 170 | document: vscode.TextDocument, 171 | difference: Difference 172 | ): vscode.Diagnostic { 173 | const range = this.getRange(document, difference); 174 | const message = this.getMessage(difference); 175 | const diagnostic = new vscode.Diagnostic(range, message); 176 | diagnostic.source = DIAGNOSTICS_ID; 177 | diagnostic.code = DIAGNOSTICS_SOURCE_ID; 178 | return diagnostic; 179 | } 180 | 181 | private getMessage(difference: Difference): string { 182 | switch (difference.operation) { 183 | case generateDifferences.INSERT: 184 | return `Insert ${showInvisibles(difference.insertText!)}`; 185 | case generateDifferences.REPLACE: 186 | return `Replace ${showInvisibles( 187 | difference.deleteText! 188 | )} with ${showInvisibles(difference.insertText!)}`; 189 | case generateDifferences.DELETE: 190 | return `Delete ${showInvisibles(difference.deleteText!)}`; 191 | default: 192 | return ''; 193 | } 194 | } 195 | 196 | private getRange( 197 | document: vscode.TextDocument, 198 | difference: Difference 199 | ): vscode.Range { 200 | if (difference.operation === generateDifferences.INSERT) { 201 | const start = document.positionAt(difference.offset); 202 | return new vscode.Range( 203 | start.line, 204 | start.character, 205 | start.line, 206 | start.character 207 | ); 208 | } 209 | const start = document.positionAt(difference.offset); 210 | const end = document.positionAt( 211 | difference.offset + difference.deleteText!.length 212 | ); 213 | return new vscode.Range( 214 | start.line, 215 | start.character, 216 | end.line, 217 | end.character 218 | ); 219 | } 220 | 221 | private async getDiff( 222 | document: vscode.TextDocument, 223 | cancellationToken?: vscode.CancellationToken 224 | ): Promise { 225 | const source = document.getText(); 226 | const formattedSource = 227 | (await this.spotlessRunner.run(document, cancellationToken)) || source; 228 | const differences = generateDifferences(source, formattedSource); 229 | return { 230 | source, 231 | formattedSource, 232 | differences, 233 | }; 234 | } 235 | 236 | public provideCodeActions( 237 | document: vscode.TextDocument, 238 | range: vscode.Range | vscode.Selection 239 | ): vscode.CodeAction[] { 240 | let totalDiagnostics = 0; 241 | const codeActions: vscode.CodeAction[] = []; 242 | this.diagnosticCollection.forEach( 243 | (uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]) => { 244 | if (document.uri.fsPath !== uri.fsPath) { 245 | return; 246 | } 247 | diagnostics.forEach((diagnostic: vscode.Diagnostic) => { 248 | totalDiagnostics += 1; 249 | if (!range.isEqual(diagnostic.range)) { 250 | return; 251 | } 252 | const difference = this.diagnosticDifferenceMap.get(diagnostic); 253 | codeActions.push( 254 | this.getQuickFixCodeAction(document.uri, diagnostic, difference!) 255 | ); 256 | }); 257 | } 258 | ); 259 | if (totalDiagnostics > 1) { 260 | codeActions.push( 261 | this.getQuickFixAllProblemsCodeAction(document, totalDiagnostics) 262 | ); 263 | } 264 | return codeActions; 265 | } 266 | 267 | private getQuickFixCodeAction( 268 | uri: vscode.Uri, 269 | diagnostic: vscode.Diagnostic, 270 | difference: Difference 271 | ): vscode.CodeAction { 272 | const action = new vscode.CodeAction( 273 | `Fix this ${DIAGNOSTICS_ID} problem`, 274 | SpotlessDiagnostics.quickFixCodeActionKind 275 | ); 276 | action.edit = new vscode.WorkspaceEdit(); 277 | if (difference.operation === generateDifferences.INSERT) { 278 | action.edit.insert(uri, diagnostic.range.start, difference.insertText!); 279 | } else if (difference.operation === generateDifferences.REPLACE) { 280 | action.edit.replace(uri, diagnostic.range, difference.insertText!); 281 | } else if (difference.operation === generateDifferences.DELETE) { 282 | action.edit.delete(uri, diagnostic.range); 283 | } 284 | return action; 285 | } 286 | 287 | private getQuickFixAllProblemsCodeAction( 288 | document: vscode.TextDocument, 289 | totalDiagnostics: number 290 | ): vscode.CodeAction { 291 | const title = `Fix all ${DIAGNOSTICS_ID} problems (${totalDiagnostics})`; 292 | const action = new vscode.CodeAction( 293 | title, 294 | SpotlessDiagnostics.quickFixCodeActionKind 295 | ); 296 | action.command = { 297 | title, 298 | command: FixAllCodeActionsCommand.Id, 299 | arguments: [document], 300 | }; 301 | return action; 302 | } 303 | } 304 | --------------------------------------------------------------------------------