Skip to content

IDE hook #568

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/PaddedCell.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions plugin-gradle/IDE_HOOK.md
Original file line number Diff line number Diff line change
@@ -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.
31 changes: 18 additions & 13 deletions plugin-gradle/README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
# <img align="left" src="../_images/spotless_logo.png"> Spotless: Keep your code spotless with Gradle
# <img align="left" src="../_images/spotless_logo.png"> Spotless plugin for Gradle
*Keep your code Spotless with Gradle*

<!---freshmark shields
output = [
link(shield('Gradle plugin', 'plugins.gradle.org', 'com.diffplug.gradle.spotless', 'blue'), 'https://plugins.gradle.org/plugin/com.diffplug.gradle.spotless'),
link(shield('Maven central', 'mavencentral', 'com.diffplug.gradle.spotless:spotless', 'blue'), 'https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.diffplug.spotless%22%20AND%20a%3A%22spotless-plugin-gradle%22'),
link(shield('Javadoc', 'javadoc', '{{versionLast}}', 'blue'), 'https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/{{versionLast}}/index.html'),
link(shield('Maven central', 'mavencentral', 'yes', 'blue'), 'https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.diffplug.spotless%22%20AND%20a%3A%22spotless-plugin-gradle%22'),
link(shield('Javadoc', 'javadoc', 'yes', 'blue'), 'https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/{{versionLast}}/index.html'),
link(shield('License Apache', 'license', 'apache', 'blue'), 'https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)'),
link(shield('Changelog', 'changelog', '{{versionLast}}', 'blue'), 'CHANGES.md'),
'',
link(shield('Changelog', 'changelog', '{{versionLast}}', 'brightgreen'), 'CHANGES.md'),
link(image('Travis CI', 'https://travis-ci.org/{{org}}/{{name}}.svg?branch=master'), 'https://travis-ci.org/{{org}}/{{name}}'),
link(image('Circle CI', 'https://circleci.com/gh/diffplug/spotless/tree/master.svg?style=shield'), 'https://circleci.com/gh/diffplug/spotless/tree/master'),
link(shield('Live chat', 'gitter', 'chat', 'brightgreen'), 'https://gitter.im/{{org}}/{{name}}'),
link(shield('License Apache', 'license', 'apache', 'brightgreen'), 'https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)')
link(shield('VS Code plugin Apache', 'IDE', 'VS Code', 'blueviolet'), 'https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle'),
link(shield('VS Code plugin Apache', 'IDE', 'add yours', 'blueviolet'), 'IDE_HOOK.md')
].join('\n');
-->
[![Gradle plugin](https://img.shields.io/badge/plugins.gradle.org-com.diffplug.gradle.spotless-blue.svg)](https://plugins.gradle.org/plugin/com.diffplug.gradle.spotless)
[![Maven central](https://img.shields.io/badge/mavencentral-com.diffplug.gradle.spotless%3Aspotless-blue.svg)](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.diffplug.spotless%22%20AND%20a%3A%22spotless-plugin-gradle%22)
[![Javadoc](https://img.shields.io/badge/javadoc-3.29.0-blue.svg)](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/3.29.0/index.html)
[![Maven central](https://img.shields.io/badge/mavencentral-yes-blue.svg)](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.diffplug.spotless%22%20AND%20a%3A%22spotless-plugin-gradle%22)
[![Javadoc](https://img.shields.io/badge/javadoc-yes-blue.svg)](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/3.29.0/index.html)
[![License Apache](https://img.shields.io/badge/license-apache-blue.svg)](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0))
[![Changelog](https://img.shields.io/badge/changelog-3.29.0-blue.svg)](CHANGES.md)

[![Changelog](https://img.shields.io/badge/changelog-3.29.0-brightgreen.svg)](CHANGES.md)
[![Travis CI](https://travis-ci.org/diffplug/spotless.svg?branch=master)](https://travis-ci.org/diffplug/spotless)
[![Circle CI](https://circleci.com/gh/diffplug/spotless/tree/master.svg?style=shield)](https://circleci.com/gh/diffplug/spotless/tree/master)
[![Live chat](https://img.shields.io/badge/gitter-chat-brightgreen.svg)](https://gitter.im/diffplug/spotless)
[![License Apache](https://img.shields.io/badge/license-apache-brightgreen.svg)](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0))
[![VS Code plugin Apache](https://img.shields.io/badge/IDE-VS_Code-blueviolet.svg)](https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle)
[![VS Code plugin Apache](https://img.shields.io/badge/IDE-add_yours-blueviolet.svg)](IDE_HOOK.md)
<!---freshmark /shields -->

<!---freshmark javadoc
output = prefixDelimiterReplace(input, 'https://javadoc.io/static/com.diffplug.spotless/spotless-plugin-gradle/', '/', versionLast)
-->

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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -104,15 +105,19 @@ public FormatExceptionPolicy getExceptionPolicy() {
return exceptionPolicy;
}

protected Iterable<File> target;
protected FileCollection target;

@Internal
public Iterable<File> getTarget() {
public FileCollection getTarget() {
return target;
}

public void setTarget(Iterable<File> target) {
this.target = Objects.requireNonNull(target);
if (target instanceof FileCollection) {
this.target = (FileCollection) target;
} else {
this.target = getProject().files(target);
}
}

/** Internal use only. */
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
}