-
Notifications
You must be signed in to change notification settings - Fork 134
IntelliJ: Format on save when using PJF #1151
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
Changes from all commits
88d6148
b01f17a
9058c1b
6859e25
f8461cd
9a5618d
092f321
7eaae0a
ce47937
905a038
3cfdc27
40d6bb8
1aaef47
06581c9
43e1337
ab105c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
|---|---|---|
|
|
@@ -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>() { | ||
|
|
@@ -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> | ||
|
|
@@ -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) { | ||
|
|
@@ -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']) | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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"> | ||
|
|
@@ -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"> | ||
|
|
@@ -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. | ||
| * | ||
|
|
@@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this to allow users to override the configuration?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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']) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.