Skip to content
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
6 changes: 6 additions & 0 deletions changelog/@unreleased/pr-1151.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type: improvement
improvement:
description: When palantir-java-format is applied to a repo, configure the intellij
project to auto-format on save.
links:
- https://github.com/palantir/gradle-baseline/pull/1151
Original file line number Diff line number Diff line change
Expand Up @@ -32,42 +32,28 @@ import org.gradle.plugins.ide.idea.GenerateIdeaProject
import org.gradle.plugins.ide.idea.GenerateIdeaWorkspace
import org.gradle.plugins.ide.idea.IdeaPlugin
import org.gradle.plugins.ide.idea.model.IdeaModel
import org.gradle.plugins.ide.idea.model.ModuleDependency

class BaselineIdea extends AbstractBaselinePlugin {

static SAVE_ACTIONS_PLUGIN_MINIMUM_VERSION = '1.9.0'

void apply(Project project) {
this.project = project

project.plugins.apply IdeaPlugin
IdeaModel ideaModuleModel = project.extensions.getByType(IdeaModel)

project.afterEvaluate {
// Configure Idea project
IdeaModel ideaRootModel = project.rootProject.extensions.findByType(IdeaModel)
if (ideaRootModel) {
ideaRootModel.project.ipr.withXml { provider ->
def node = provider.asNode()
addCodeStyle(node)
addCopyright(node)
addCheckstyle(node)
addEclipseFormat(node)
addGit(node)
addInspectionProjectProfile(node)
addJavacSettings(node)
addGitHubIssueNavigation(node)
}

ideaRootModel.workspace.iws.withXml { provider ->
def node = provider.asNode()
setRunManagerWorkingDirectory(node)
addEditorSettings(node)
}
}

// Configure Idea module
moveProjectReferencesToEnd(ideaModuleModel)
if (project == project.rootProject) {
applyToRootProject(project)
} else {
// Be defensive - it never makes sense to apply this project to only a subproject but not to the root.
project.rootProject.pluginManager.apply(BaselineIdea)
}

// Configure Idea module
IdeaModel ideaModuleModel = project.extensions.getByType(IdeaModel)
moveProjectReferencesToEnd(ideaModuleModel)

// If someone renames a project, leftover {ipr,iml,ipr} files may still exist on disk and
// confuse users, so we proactively clean them up. Intentionally using an Action<Task> to allow up-to-dateness.
Action<Task> cleanup = new Action<Task>() {
Expand All @@ -90,6 +76,42 @@ class BaselineIdea extends AbstractBaselinePlugin {
project.getTasks().findByName("idea").doLast(cleanup)
}

void applyToRootProject(Project project) {
// Configure Idea project
IdeaModel ideaRootModel = project.extensions.findByType(IdeaModel)
ideaRootModel.project.ipr.withXml { provider ->
Node node = provider.asNode()
addCodeStyle(node)
addCopyright(node)
addCheckstyle(node)
addEclipseFormat(node)
addGit(node)
addInspectionProjectProfile(node)
addJavacSettings(node)
addGitHubIssueNavigation(node)
}

project.afterEvaluate {
ideaRootModel.workspace.iws.withXml { provider ->
Node node = provider.asNode()
setRunManagerWorkingDirectory(node)
addEditorSettings(node)
}
}

// Suggest and configure the "save actions" plugin if Palantir Java Format is turned on.
// This plugin can only be applied to the root project, and it applied as a side-effect of applying
// 'com.palantir.java-format' to any subproject.
project.getPluginManager().withPlugin("com.palantir.java-format-idea") {
ideaRootModel.project.ipr.withXml { provider ->
Node node = provider.asNode()
configureSaveActions(node)
configureExternalDependencies(node)
}
configureSaveActionsForIntellijImport(project)
}
}

@CompileStatic
static Spec<FileTreeElement> isFile(File file) {
{ FileTreeElement details -> details.file == file } as Spec<FileTreeElement>
Expand Down Expand Up @@ -167,8 +189,8 @@ class BaselineIdea extends AbstractBaselinePlugin {
</option>
</component>
"""))
def externalDependencies = matchOrCreateChild(node, 'component', [name: 'ExternalDependencies'])
matchOrCreateChild(externalDependencies, 'plugin', [id: 'EclipseCodeFormatter'])
def externalDependencies = GroovyXmlUtils.matchOrCreateChild(node, 'component', [name: 'ExternalDependencies'])
GroovyXmlUtils.matchOrCreateChild(externalDependencies, 'plugin', [id: 'EclipseCodeFormatter'])
}

private void addCheckstyle(node) {
Expand Down Expand Up @@ -196,8 +218,8 @@ class BaselineIdea extends AbstractBaselinePlugin {
</option>
</component>
""".stripIndent()))
def externalDependencies = matchOrCreateChild(node, 'component', [name: 'ExternalDependencies'])
matchOrCreateChild(externalDependencies, 'plugin', [id: 'CheckStyle-IDEA'])
def externalDependencies = GroovyXmlUtils.matchOrCreateChild(node, 'component', [name: 'ExternalDependencies'])
GroovyXmlUtils.matchOrCreateChild(externalDependencies, 'plugin', [id: 'CheckStyle-IDEA'])
}

/**
Expand All @@ -216,7 +238,7 @@ class BaselineIdea extends AbstractBaselinePlugin {
'''.stripIndent()))
}

private void addInspectionProjectProfile(node) {
private static void addInspectionProjectProfile(node) {
node.append(new XmlParser().parseText("""
<component name="InspectionProjectProfileManager">
<profile version="1.0">
Expand All @@ -231,23 +253,23 @@ class BaselineIdea extends AbstractBaselinePlugin {
""".stripIndent()))
}

private void addJavacSettings(node) {
private static void addJavacSettings(node) {
node.append(new XmlParser().parseText("""
<component name="JavacSettings">
<option name="PREFER_TARGET_JDK_COMPILER" value="false" />
</component>
""".stripIndent()))
}

private void addEditorSettings(node) {
private static void addEditorSettings(node) {
node.append(new XmlParser().parseText("""
<component name="CodeInsightWorkspaceSettings">
<option name="optimizeImportsOnTheFly" value="true" />
</component>
""".stripIndent()))
}

private void addGitHubIssueNavigation(node) {
private static void addGitHubIssueNavigation(node) {
GitUtils.maybeGitHubUri().ifPresent { githubUri ->
node.append(new XmlParser().parseText("""
<component name="IssueNavigationConfiguration">
Expand All @@ -264,31 +286,34 @@ class BaselineIdea extends AbstractBaselinePlugin {
}
}

private static void configureSaveActionsForIntellijImport(Project project) {
if (!Boolean.getBoolean("idea.active")) {
return
}
XmlUtils.createOrUpdateXmlFile(
project.file(".idea/externalDependencies.xml"),
BaselineIdea.&configureExternalDependencies)
XmlUtils.createOrUpdateXmlFile(
project.file(".idea/saveactions_settings.xml"),
BaselineIdea.&configureSaveActions)
}

/**
* Configure the default working directory of RunManager configurations to be the module directory.
*/
private static void setRunManagerWorkingDirectory(Node node) {
def runTypes = ['Application', 'JUnit'] as Set

def runManager = matchOrCreateChild(node, 'component', [name: 'RunManager'])
def runManager = GroovyXmlUtils.matchOrCreateChild(node, 'component', [name: 'RunManager'])
runTypes.each { runType ->
def configuration = matchOrCreateChild(runManager, 'configuration',
def configuration = GroovyXmlUtils.matchOrCreateChild(runManager, 'configuration',
[default: 'true', type: runType],
[factoryName: runType])
def workingDirectory = matchOrCreateChild(configuration, 'option', [name: 'WORKING_DIRECTORY'])
def workingDirectory = GroovyXmlUtils.matchOrCreateChild(configuration, 'option', [name: 'WORKING_DIRECTORY'])
workingDirectory.'@value' = 'file://$MODULE_DIR$'
}
}

private static Node matchOrCreateChild(Node base, String name, Map attributes = [:], Map defaults = [:]) {
def child = base[name].find { it.attributes().entrySet().containsAll(attributes.entrySet()) }
if (child) {
return child
}

return base.appendNode(name, attributes + defaults)
}

/**
* By default, IntelliJ and Gradle have different classpath behaviour with subprojects.
*
Expand All @@ -304,11 +329,43 @@ class BaselineIdea extends AbstractBaselinePlugin {
* This moves all project references to the end of the dependencies list, which unifies behaviour
* between Gradle and IntelliJ.
*/
private void moveProjectReferencesToEnd(IdeaModel ideaModel) {
private static void moveProjectReferencesToEnd(IdeaModel ideaModel) {
ideaModel.module.iml.whenMerged { module ->
def projectRefs = module.dependencies.findAll { it instanceof org.gradle.plugins.ide.idea.model.ModuleDependency }
def projectRefs = module.dependencies.findAll { it instanceof ModuleDependency }
module.dependencies.removeAll(projectRefs)
module.dependencies.addAll(projectRefs)
}
}

/**
* Configures some defaults on the save-actions plugin, but only if it hasn't been configured before.
*/
private static void configureSaveActions(Node rootNode) {
GroovyXmlUtils.matchOrCreateChild(rootNode, 'component', [name: 'SaveActionSettings'], [:]) {
// Configure defaults if this plugin is configured for the first time only
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this to allow users to override the configuration?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, or they might have configured this plugin already for this project and then we don't want to override their configuration

appendNode('option', [name: 'actions']).appendNode('set').with {
appendNode('option', [value: 'activate'])
appendNode('option', [value: 'noActionIfCompileErrors'])
appendNode('option', [value: 'organizeImports'])
appendNode('option', [value: 'reformat'])
}
appendNode('option', [name: 'configurationPath', value: ''])
appendNode('inclusions').appendNode('set').with {
appendNode('option', [value: 'src/.*\\.java'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, so it's a bit tricky as if people already use this plugin (and have configured it for a project) then we do nothing.. but in the majority of cases, this is probably what people want.

An unsolved problem I will add to the description is how to deal with changing flags / how do we know what's safe to update because we set it up that way vs the user set it up that way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd have to look into what other options the plugin exposes, but we may want to narrow what users can configure to a very narrow set of flags

}
}
}

private static void configureExternalDependencies(Node rootNode) {
def externalDependencies =
GroovyXmlUtils.matchOrCreateChild(rootNode, 'component', [name: 'ExternalDependencies'])
// I kid you not, this is the id for the save actions plugin:
// https://github.com/dubreuia/intellij-plugin-save-actions/blob/v1.9.0/src/main/resources/META-INF/plugin.xml#L5
// https://plugins.jetbrains.com/plugin/7642-save-actions/
GroovyXmlUtils.matchOrCreateChild(
externalDependencies,
'plugin',
[id: 'com.dubreuia'],
['min-version': SAVE_ACTIONS_PLUGIN_MINIMUM_VERSION])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* (c) Copyright 2020 Palantir Technologies Inc. All rights reserved.
*
* 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.palantir.baseline.plugins

final class GroovyXmlUtils {
static Node matchOrCreateChild(
Node base,
String name,
Map attributes = [:],
Map defaults = [:],
@DelegatesTo(value = Node, strategy = Closure.DELEGATE_FIRST) Closure ifCreated = {}) {
def child = base[name].find { it.attributes().entrySet().containsAll(attributes.entrySet()) }
if (child) {
return child
}

def created = base.appendNode(name, attributes + defaults)
ifCreated.delegate = created
ifCreated(created)
return created
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* (c) Copyright 2020 Palantir Technologies Inc. All rights reserved.
*
* 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.palantir.baseline.plugins;

import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
import groovy.util.Node;
import groovy.util.XmlNodePrinter;
import groovy.util.XmlParser;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.function.Consumer;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;

final class XmlUtils {
private XmlUtils() {}

static void createOrUpdateXmlFile(File configurationFile, Consumer<Node> configure) {
Node rootNode;
if (configurationFile.isFile()) {
try {
rootNode = new XmlParser().parse(configurationFile);
} catch (IOException | SAXException | ParserConfigurationException e) {
throw new RuntimeException("Couldn't parse existing configuration file: " + configurationFile, e);
}
} else {
rootNode = new Node(null, "project", ImmutableMap.of("version", "4"));
}

configure.accept(rootNode);

try (BufferedWriter writer = Files.newWriter(configurationFile, StandardCharsets.UTF_8);
PrintWriter printWriter = new PrintWriter(writer)) {
XmlNodePrinter nodePrinter = new XmlNodePrinter(printWriter);
nodePrinter.setPreserveWhitespace(true);
nodePrinter.print(rootNode);
} catch (IOException e) {
throw new RuntimeException("Failed to write back to configuration file: " + configurationFile, e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,12 @@ class AbstractPluginTest extends Specification {
return it
}
}

/**
* Copied from {@link nebula.test.BaseIntegrationSpec#findModuleName()}, we really should just use that class
* instead.
*/
protected String getModuleName() {
return getProjectDir().getName().replaceAll(/_\d+/, '')
}
}
Loading