diff --git a/.circleci/config.yml b/.circleci/config.yml
index 3c6d67bfd0..c9e2f911d3 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,8 +1,11 @@
anchors:
env_gradle: &env_gradle
environment:
- _JAVA_OPTIONS: "-Xmx3g"
- GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2"
+ # java doesn't play nice with containers, it tries to hog the entire machine
+ # https://circleci.com/blog/how-to-handle-java-oom-errors/
+ # try the experimental JVM option
+ _JAVA_OPTIONS: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap"
+ GRADLE_OPTS: "-Dorg.gradle.workers.max=2" # and we're only allowed to use 2 vCPUs
docker:
- image: cimg/openjdk:8.0
@@ -67,6 +70,8 @@ jobs:
<<: *env_gradle
docker:
- image: cimg/openjdk:11.0
+ environment: # java 11 does play nice with containers, doesn't need special settings
+ _JAVA_OPTIONS: ""
<<: *test_nomaven
test_justmaven_8:
<< : *env_gradle
diff --git a/CHANGES.md b/CHANGES.md
index 0ddf566b74..f9a9af5394 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -10,6 +10,8 @@ This document is intended for Spotless developers.
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
## [Unreleased]
+### Added
+* `PaddedCell.calculateDirtyState(Formatter, File, byte[])` to allow IDE integrations to send dirty editor buffers.
## [1.29.0] - 2020-05-05
### Added
diff --git a/lib/src/main/java/com/diffplug/spotless/PaddedCell.java b/lib/src/main/java/com/diffplug/spotless/PaddedCell.java
index 0888e1646a..d801db038f 100644
--- a/lib/src/main/java/com/diffplug/spotless/PaddedCell.java
+++ b/lib/src/main/java/com/diffplug/spotless/PaddedCell.java
@@ -186,6 +186,10 @@ public static DirtyState calculateDirtyState(Formatter formatter, File file) thr
Objects.requireNonNull(file, "file");
byte[] rawBytes = Files.readAllBytes(file.toPath());
+ return calculateDirtyState(formatter, file, rawBytes);
+ }
+
+ public static DirtyState calculateDirtyState(Formatter formatter, File file, byte[] rawBytes) throws IOException {
String raw = new String(rawBytes, formatter.getEncoding());
String rawUnix = LineEnding.toUnix(raw);
diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md
index 74d6a578b5..b4ec663675 100644
--- a/plugin-gradle/CHANGES.md
+++ b/plugin-gradle/CHANGES.md
@@ -3,6 +3,8 @@
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).
## [Unreleased]
+### Added
+* `-PspotlessIdeHook` which makes the VS Code extension faster and more reliable. See [`IDE_INTEGRATION.md`](IDE_INTEGRATION.md) for more details. ([#568](https://github.com/diffplug/spotless/pull/568))
## [3.29.0] - 2020-05-05
### Added
diff --git a/plugin-gradle/IDE_HOOK.md b/plugin-gradle/IDE_HOOK.md
new file mode 100644
index 0000000000..8734245cad
--- /dev/null
+++ b/plugin-gradle/IDE_HOOK.md
@@ -0,0 +1,26 @@
+# Spotless Gradle IDE integrations
+
+Thanks to `spotlessApply`, it is not necessary for Spotless and your IDE to agree on formatting - you can always run spotless at the end to fix things up. But if you want them to agree, there are two approaches:
+
+- 👎setup your IDE to match Spotless: tricky to get right, hard to keep up-to-date
+ - [eclipse](https://github.com/diffplug/spotless/blob/master/ECLIPSE_SCREENSHOTS.md)
+- 👍setup your IDE to use Spotless as the source of truth: easy to setup, guaranteed to stay up-to-date
+ - [VS Code](https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle)
+ - (add your IDE here!)
+
+## How to add an IDE
+
+The Spotless plugin for Gradle accepts a command-line argument `-PspotlessIdeHook=${ABSOLUTE_PATH_TO_FILE}`. In this mode, `spotlessCheck` is disabled, and `spotlessApply` will apply only to that one file. Because it already knows the absolute path of the only file you are asking about, it is able to run much faster than a normal invocation of `spotlessApply`.
+
+For extra flexibility, you can add `-PspotlessIdeHookUseStdIn`, and Spotless will read the file content from `stdin`. This allows you to send the content of a dirty editor buffer without writing to a file. You can also add `-PspotlessIdeHookUseStdOut`, and Spotless will return the formatted content on `stdout` rather than writing it to a file (you should also add `--quiet` to make sure Gradle doesn't dump logging info into `stdout`).
+
+In all of these cases, Spotless will send useful status information on `stderr`:
+
+- if `stderr` starts with `IS DIRTY`, then the file was dirty, and `stdout` contains its full formatted contents
+ - in every other case, `stdout` will be empty / the file will be unchanged because there is nothing to change
+- if `stderr` starts with `IS CLEAN`, then the file is already clean
+- if `stderr` starts with `DID NOT CONVERGE`, then the formatter is misbehaving, and the rest of `stderr` has useful diagnostic info (e.g. `spotlessDiagnose` for [padded cell](../PADDEDCELL.md))
+- if `stderr` is empty, then the file is not being formatted by spotless (not included in any target)
+- if `stderr` is anything else, then it is the stacktrace of whatever went wrong
+
+See the VS Code extension above for a working example, or [the PR](https://github.com/diffplug/spotless/pull/568) where this feature was added for more context.
diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md
index 61fffb9f2f..f6ca7af118 100644
--- a/plugin-gradle/README.md
+++ b/plugin-gradle/README.md
@@ -1,34 +1,39 @@
-#
Spotless: Keep your code spotless with Gradle
+#
Spotless plugin for Gradle
+*Keep your code Spotless with Gradle*
[](https://plugins.gradle.org/plugin/com.diffplug.gradle.spotless)
-[](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.diffplug.spotless%22%20AND%20a%3A%22spotless-plugin-gradle%22)
-[](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/3.29.0/index.html)
+[](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.diffplug.spotless%22%20AND%20a%3A%22spotless-plugin-gradle%22)
+[](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/3.29.0/index.html)
+[](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0))
+[](CHANGES.md)
-[](CHANGES.md)
-[](https://travis-ci.org/diffplug/spotless)
+[](https://circleci.com/gh/diffplug/spotless/tree/master)
[](https://gitter.im/diffplug/spotless)
-[](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0))
+[](https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle)
+[](IDE_HOOK.md)
-Spotless is a general-purpose formatting plugin used by [2,700 projects on GitHub (Jan 2020)](https://github.com/search?l=gradle&q=spotless&type=Code). It is completely à la carte, but also includes powerful "batteries-included" if you opt-in.
+Spotless is a general-purpose formatting plugin used by [3,500 projects on GitHub (May 2020)](https://github.com/search?l=gradle&q=spotless&type=Code). It is completely à la carte, but also includes powerful "batteries-included" if you opt-in.
-To people who use your build, it looks like this:
+To people who use your build, it looks like this ([IDE support also available]()):
```
cmd> gradlew build
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java
new file mode 100644
index 0000000000..eb21fb5547
--- /dev/null
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.gradle.spotless;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+import com.diffplug.common.base.Errors;
+import com.diffplug.common.io.ByteStreams;
+import com.diffplug.spotless.Formatter;
+import com.diffplug.spotless.PaddedCell;
+
+class IdeHook {
+ final static String PROPERTY = "spotlessIdeHook";
+ final static String USE_STD_IN = "spotlessIdeHookUseStdIn";
+ final static String USE_STD_OUT = "spotlessIdeHookUseStdOut";
+
+ static void performHook(SpotlessTask spotlessTask) {
+ String path = (String) spotlessTask.getProject().property(PROPERTY);
+ File file = new File(path);
+ if (!file.isAbsolute()) {
+ System.err.println("Argument passed to " + PROPERTY + " must be an absolute path");
+ return;
+ }
+ if (spotlessTask.getTarget().contains(file)) {
+ try (Formatter formatter = spotlessTask.buildFormatter()) {
+ byte[] bytes;
+ if (spotlessTask.getProject().hasProperty(USE_STD_IN)) {
+ bytes = ByteStreams.toByteArray(System.in);
+ } else {
+ bytes = Files.readAllBytes(file.toPath());
+ }
+ PaddedCell.DirtyState dirty = PaddedCell.calculateDirtyState(formatter, file, bytes);
+ if (dirty.isClean()) {
+ System.err.println("IS CLEAN");
+ } else if (dirty.didNotConverge()) {
+ System.err.println("DID NOT CONVERGE");
+ System.err.println("Run 'spotlessDiagnose' for details https://github.com/diffplug/spotless/blob/master/PADDEDCELL.md");
+ } else {
+ System.err.println("IS DIRTY");
+ if (spotlessTask.getProject().hasProperty(USE_STD_OUT)) {
+ dirty.writeCanonicalTo(System.out);
+ } else {
+ dirty.writeCanonicalTo(file);
+ }
+ }
+ System.err.close();
+ System.out.close();
+ } catch (IOException e) {
+ e.printStackTrace(System.err);
+ throw Errors.asRuntime(e);
+ }
+ }
+ }
+}
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java
index 7ebb62bab4..ffb62ad039 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java
@@ -302,5 +302,14 @@ public Object doCall(TaskExecutionGraph graph) {
diagnoseTask.source = spotlessTask;
rootDiagnoseTask.dependsOn(diagnoseTask);
diagnoseTask.mustRunAfter(clean);
+
+ if (project.hasProperty(IdeHook.PROPERTY)) {
+ // disable the normal tasks, to disable their up-to-date checking
+ spotlessTask.setEnabled(false);
+ checkTask.setEnabled(false);
+ applyTask.setEnabled(false);
+ // the rootApplyTask is no longer just a marker task, now it does a bit of work itself
+ rootApplyTask.doLast(unused -> IdeHook.performHook(spotlessTask));
+ }
}
}
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java
index 0a40d8b3ac..8cd04ed980 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java
@@ -30,6 +30,7 @@
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
+import org.gradle.api.file.FileCollection;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Internal;
@@ -104,15 +105,19 @@ public FormatExceptionPolicy getExceptionPolicy() {
return exceptionPolicy;
}
- protected Iterable target;
+ protected FileCollection target;
@Internal
- public Iterable getTarget() {
+ public FileCollection getTarget() {
return target;
}
public void setTarget(Iterable target) {
- this.target = Objects.requireNonNull(target);
+ if (target instanceof FileCollection) {
+ this.target = (FileCollection) target;
+ } else {
+ this.target = getProject().files(target);
+ }
}
/** Internal use only. */
diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IdeHookTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IdeHookTest.java
new file mode 100644
index 0000000000..c13d3d98bb
--- /dev/null
+++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IdeHookTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2016 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.gradle.spotless;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+
+import org.assertj.core.api.Assertions;
+import org.gradle.testkit.runner.GradleRunner;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.diffplug.common.base.StringPrinter;
+import com.diffplug.common.io.Files;
+
+public class IdeHookTest extends GradleIntegrationTest {
+ private String output, error;
+ private File dirty, clean, diverge, outofbounds;
+
+ @Before
+ public void before() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.gradle.spotless'",
+ "}",
+ "spotless {",
+ " format 'misc', {",
+ " target 'DIRTY.md', 'CLEAN.md'",
+ " customLazyGroovy('lowercase') {",
+ " return { str -> str.toLowerCase(Locale.ROOT) }",
+ " }",
+ " }",
+ " format 'diverge', {",
+ " target 'DIVERGE.md'",
+ " customLazyGroovy('diverge') {",
+ " return { str -> str + ' ' }",
+ " }",
+ " }",
+ "}");
+ dirty = new File(rootFolder(), "DIRTY.md");
+ Files.write("ABC".getBytes(StandardCharsets.UTF_8), dirty);
+ clean = new File(rootFolder(), "CLEAN.md");
+ Files.write("abc".getBytes(StandardCharsets.UTF_8), clean);
+ diverge = new File(rootFolder(), "DIVERGE.md");
+ Files.write("ABC".getBytes(StandardCharsets.UTF_8), diverge);
+ outofbounds = new File(rootFolder(), "OUTOFBOUNDS.md");
+ Files.write("ABC".getBytes(StandardCharsets.UTF_8), outofbounds);
+ }
+
+ private void runWith(String... arguments) throws IOException {
+ StringBuilder output = new StringBuilder();
+ StringBuilder error = new StringBuilder();
+ try (Writer outputWriter = new StringPrinter(output::append).toWriter();
+ Writer errorWriter = new StringPrinter(error::append).toWriter();) {
+ // gradle 2.14 -> 4.6 confirmed to work
+ // gradle 4.7 -> 5.1 don't work in tooling API because of https://github.com/gradle/gradle/issues/7617
+ // gradle 5.1 -> current confirmed to work
+ GradleRunner.create()
+ .withProjectDir(rootFolder())
+ .withPluginClasspath()
+ .withArguments(arguments)
+ .forwardStdOutput(outputWriter)
+ .forwardStdError(errorWriter)
+ .build();
+ }
+ this.output = output.toString();
+ this.error = error.toString();
+ }
+
+ @Test
+ public void dirty() throws IOException {
+ runWith("spotlessApply", "--quiet", "-PspotlessIdeHook=" + dirty.getAbsolutePath(), "-PspotlessIdeHookUseStdOut");
+ Assertions.assertThat(output).isEqualTo("abc");
+ Assertions.assertThat(error).startsWith("IS DIRTY");
+ }
+
+ @Test
+ public void clean() throws IOException {
+ runWith("spotlessApply", "--quiet", "-PspotlessIdeHook=" + clean.getAbsolutePath(), "-PspotlessIdeHookUseStdOut");
+ Assertions.assertThat(output).isEmpty();
+ Assertions.assertThat(error).startsWith("IS CLEAN");
+ }
+
+ @Test
+ public void diverge() throws IOException {
+ runWith("spotlessApply", "--quiet", "-PspotlessIdeHook=" + diverge.getAbsolutePath(), "-PspotlessIdeHookUseStdOut");
+ Assertions.assertThat(output).isEmpty();
+ Assertions.assertThat(error).startsWith("DID NOT CONVERGE");
+ }
+
+ @Test
+ public void outofbounds() throws IOException {
+ runWith("spotlessApply", "--quiet", "-PspotlessIdeHook=" + outofbounds.getAbsolutePath(), "-PspotlessIdeHookUseStdOut");
+ Assertions.assertThat(output).isEmpty();
+ Assertions.assertThat(error).isEmpty();
+ }
+
+ @Test
+ public void notAbsolute() throws IOException {
+ runWith("spotlessApply", "--quiet", "-PspotlessIdeHook=build.gradle", "-PspotlessIdeHookUseStdOut");
+ Assertions.assertThat(output).isEmpty();
+ Assertions.assertThat(error).contains("Argument passed to spotlessIdeHook must be an absolute path");
+ }
+}