ranges = new ArrayList<>();
- if (info.insertedRanges != null) {
- ranges.addAll(info.insertedRanges);
- }
- ranges.addAll(info.allChangedRanges);
- reformatTextWithContext(file, ranges);
- }
-
- @Override
- public void reformatTextWithContext(
- @NotNull PsiFile file, @NotNull Collection extends TextRange> ranges) {
- if (overrideFormatterForFile(file)) {
- formatInternal(file, ranges);
- } else {
- super.reformatTextWithContext(file, ranges);
- }
- }
-
- @Override
- public PsiElement reformatRange(
- @NotNull PsiElement element,
- int startOffset,
- int endOffset,
- boolean canChangeWhiteSpacesOnly) {
- // Only handle elements that are PsiFile for now -- otherwise we need to search for some
- // element within the file at new locations given the original startOffset and endOffsets
- // to serve as the return value.
- PsiFile file = element instanceof PsiFile ? (PsiFile) element : null;
- if (file != null && canChangeWhiteSpacesOnly && overrideFormatterForFile(file)) {
- formatInternal(file, ImmutableList.of(new TextRange(startOffset, endOffset)));
- return file;
- } else {
- return super.reformatRange(element, startOffset, endOffset, canChangeWhiteSpacesOnly);
- }
- }
-
- /** Return whether this formatter can handle formatting the given file. */
- private boolean overrideFormatterForFile(PsiFile file) {
- return JavaFileType.INSTANCE.equals(file.getFileType())
- && GoogleJavaFormatSettings.getInstance(getProject()).isEnabled();
- }
-
- private void formatInternal(PsiFile file, Collection extends TextRange> ranges) {
- ApplicationManager.getApplication().assertWriteAccessAllowed();
- PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
- documentManager.commitAllDocuments();
- CheckUtil.checkWritable(file);
-
- Document document = documentManager.getDocument(file);
-
- if (document == null) {
- return;
- }
- // If there are postponed PSI changes (e.g., during a refactoring), just abort.
- // If we apply them now, then the incoming text ranges may no longer be valid.
- if (documentManager.isDocumentBlockedByPsi(document)) {
- return;
- }
-
- format(document, ranges);
- }
-
- /**
- * Format the ranges of the given document.
- *
- * Overriding methods will need to modify the document with the result of the external
- * formatter (usually using {@link #performReplacements(Document, Map)}).
- */
- private void format(Document document, Collection extends TextRange> ranges) {
- Style style = GoogleJavaFormatSettings.getInstance(getProject()).getStyle();
- Formatter formatter =
- new Formatter(JavaFormatterOptions.builder().style(style).reorderModifiers(false).build());
- performReplacements(
- document, FormatterUtil.getReplacements(formatter, document.getText(), ranges));
- }
-
- private void performReplacements(
- final Document document, final Map replacements) {
-
- if (replacements.isEmpty()) {
- return;
- }
-
- TreeMap sorted = new TreeMap<>(comparing(TextRange::getStartOffset));
- sorted.putAll(replacements);
- WriteCommandAction.runWriteCommandAction(
- getProject(),
- () -> {
- for (Entry entry : sorted.descendingMap().entrySet()) {
- document.replaceString(
- entry.getKey().getStartOffset(), entry.getKey().getEndOffset(), entry.getValue());
- }
- PsiDocumentManager.getInstance(getProject()).commitDocument(document);
- });
- }
-}
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatInstaller.java b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatInstaller.java
deleted file mode 100644
index c60673602..000000000
--- a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatInstaller.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2016 Google 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.google.googlejavaformat.intellij;
-
-import static com.google.common.base.Preconditions.checkState;
-
-import com.intellij.ide.plugins.IdeaPluginDescriptor;
-import com.intellij.ide.plugins.PluginManagerCore;
-import com.intellij.openapi.extensions.PluginId;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.project.ProjectManagerListener;
-import com.intellij.psi.codeStyle.CodeStyleManager;
-import com.intellij.serviceContainer.ComponentManagerImpl;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * A component that replaces the default IntelliJ {@link CodeStyleManager} with one that formats via
- * google-java-format.
- */
-final class GoogleJavaFormatInstaller implements ProjectManagerListener {
-
- @Override
- public void projectOpened(@NotNull Project project) {
- installFormatter(project);
- }
-
- private static void installFormatter(Project project) {
- CodeStyleManager currentManager = CodeStyleManager.getInstance(project);
-
- if (currentManager instanceof GoogleJavaFormatCodeStyleManager) {
- currentManager = ((GoogleJavaFormatCodeStyleManager) currentManager).getDelegate();
- }
-
- setManager(project, new GoogleJavaFormatCodeStyleManager(currentManager));
- }
-
- private static void setManager(Project project, CodeStyleManager newManager) {
- ComponentManagerImpl platformComponentManager = (ComponentManagerImpl) project;
- IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(PluginId.getId("google-java-format"));
- checkState(plugin != null, "Couldn't locate our own PluginDescriptor.");
- platformComponentManager.registerServiceInstance(CodeStyleManager.class, newManager, plugin);
- }
-}
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.form b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.form
similarity index 100%
rename from idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.form
rename to idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.form
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.java
similarity index 100%
rename from idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.java
rename to idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.java
diff --git a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatFormattingService.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatFormattingService.java
new file mode 100644
index 000000000..9d2d7a595
--- /dev/null
+++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatFormattingService.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2023 Google 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.google.googlejavaformat.intellij;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Range;
+import com.google.googlejavaformat.java.Formatter;
+import com.google.googlejavaformat.java.FormatterException;
+import com.google.googlejavaformat.java.JavaFormatterOptions;
+import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
+import com.intellij.formatting.service.AsyncDocumentFormattingService;
+import com.intellij.formatting.service.AsyncFormattingRequest;
+import com.intellij.ide.highlighter.JavaFileType;
+import com.intellij.lang.ImportOptimizer;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiFile;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import org.jetbrains.annotations.NotNull;
+
+/** Uses {@code google-java-format} to reformat code. */
+public class GoogleJavaFormatFormattingService extends AsyncDocumentFormattingService {
+
+ public static final ImmutableSet IMPORT_OPTIMIZERS =
+ ImmutableSet.of(new GoogleJavaFormatImportOptimizer());
+
+ @Override
+ protected FormattingTask createFormattingTask(AsyncFormattingRequest request) {
+ Project project = request.getContext().getProject();
+
+ if (!JreConfigurationChecker.checkJreConfiguration(project)) {
+ return null;
+ }
+
+ Style style = GoogleJavaFormatSettings.getInstance(project).getStyle();
+ Formatter formatter = createFormatter(style, request.canChangeWhitespaceOnly());
+ return new GoogleJavaFormatFormattingTask(formatter, request);
+ }
+
+ @Override
+ protected String getNotificationGroupId() {
+ return Notifications.PARSING_ERROR_NOTIFICATION_GROUP;
+ }
+
+ @Override
+ protected String getName() {
+ return "google-java-format";
+ }
+
+ private static Formatter createFormatter(Style style, boolean canChangeWhiteSpaceOnly) {
+ JavaFormatterOptions.Builder optBuilder = JavaFormatterOptions.builder().style(style);
+ if (canChangeWhiteSpaceOnly) {
+ optBuilder.formatJavadoc(false).reorderModifiers(false);
+ }
+ return new Formatter(optBuilder.build());
+ }
+
+ @Override
+ public @NotNull Set getFeatures() {
+ return Set.of(Feature.FORMAT_FRAGMENTS, Feature.OPTIMIZE_IMPORTS);
+ }
+
+ @Override
+ public boolean canFormat(@NotNull PsiFile file) {
+ return JavaFileType.INSTANCE.equals(file.getFileType())
+ && GoogleJavaFormatSettings.getInstance(file.getProject()).isEnabled();
+ }
+
+ @Override
+ public @NotNull Set getImportOptimizers(@NotNull PsiFile file) {
+ return IMPORT_OPTIMIZERS;
+ }
+
+ private static final class GoogleJavaFormatFormattingTask implements FormattingTask {
+ private final Formatter formatter;
+ private final AsyncFormattingRequest request;
+
+ private GoogleJavaFormatFormattingTask(Formatter formatter, AsyncFormattingRequest request) {
+ this.formatter = formatter;
+ this.request = request;
+ }
+
+ @Override
+ public void run() {
+ try {
+ String formattedText = formatter.formatSource(request.getDocumentText(), toRanges(request));
+ request.onTextReady(formattedText);
+ } catch (FormatterException e) {
+ request.onError(
+ Notifications.PARSING_ERROR_TITLE,
+ Notifications.parsingErrorMessage(request.getContext().getContainingFile().getName()));
+ }
+ }
+
+ private static Collection> toRanges(AsyncFormattingRequest request) {
+ if (isWholeFile(request)) {
+ // The IDE sometimes passes invalid ranges when the file is unsaved before invoking the
+ // formatter. So this is a workaround for that issue.
+ return ImmutableList.of(Range.closedOpen(0, request.getDocumentText().length()));
+ }
+ return request.getFormattingRanges().stream()
+ .map(textRange -> Range.closedOpen(textRange.getStartOffset(), textRange.getEndOffset()))
+ .collect(ImmutableList.toImmutableList());
+ }
+
+ private static boolean isWholeFile(AsyncFormattingRequest request) {
+ List ranges = request.getFormattingRanges();
+ return ranges.size() == 1
+ && ranges.get(0).getStartOffset() == 0
+ // using greater than or equal because ranges are sometimes passed inaccurately
+ && ranges.get(0).getEndOffset() >= request.getDocumentText().length();
+ }
+
+ @Override
+ public boolean isRunUnderProgress() {
+ return true;
+ }
+
+ @Override
+ public boolean cancel() {
+ return false;
+ }
+ }
+}
diff --git a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizer.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizer.java
new file mode 100644
index 000000000..498c88526
--- /dev/null
+++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizer.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2023 Google 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.google.googlejavaformat.intellij;
+
+import com.google.common.util.concurrent.Runnables;
+import com.google.googlejavaformat.java.FormatterException;
+import com.google.googlejavaformat.java.ImportOrderer;
+import com.google.googlejavaformat.java.JavaFormatterOptions;
+import com.google.googlejavaformat.java.RemoveUnusedImports;
+import com.intellij.ide.highlighter.JavaFileType;
+import com.intellij.lang.ImportOptimizer;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+
+/** Uses {@code google-java-format} to optimize imports. */
+public class GoogleJavaFormatImportOptimizer implements ImportOptimizer {
+
+ @Override
+ public boolean supports(@NotNull PsiFile file) {
+ return JavaFileType.INSTANCE.equals(file.getFileType())
+ && GoogleJavaFormatSettings.getInstance(file.getProject()).isEnabled();
+ }
+
+ @Override
+ public @NotNull Runnable processFile(@NotNull PsiFile file) {
+ Project project = file.getProject();
+
+ if (!JreConfigurationChecker.checkJreConfiguration(file.getProject())) {
+ return Runnables.doNothing();
+ }
+
+ PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project);
+ Document document = documentManager.getDocument(file);
+
+ if (document == null) {
+ return Runnables.doNothing();
+ }
+
+ JavaFormatterOptions.Style style = GoogleJavaFormatSettings.getInstance(project).getStyle();
+
+ String text;
+ try {
+ text =
+ ImportOrderer.reorderImports(
+ RemoveUnusedImports.removeUnusedImports(document.getText()), style);
+ } catch (FormatterException e) {
+ Notifications.displayParsingErrorNotification(project, file.getName());
+ return Runnables.doNothing();
+ }
+
+ return () -> document.setText(text);
+ }
+}
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java
similarity index 90%
rename from idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java
rename to idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java
index 1e92a4bdd..ee187c00d 100644
--- a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java
+++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java
@@ -18,7 +18,6 @@
import com.google.googlejavaformat.java.JavaFormatterOptions;
import com.intellij.openapi.components.PersistentStateComponent;
-import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.project.Project;
@@ -30,10 +29,16 @@
storages = {@Storage("google-java-format.xml")})
class GoogleJavaFormatSettings implements PersistentStateComponent {
+ private final Project project;
+
private State state = new State();
+ GoogleJavaFormatSettings(Project project) {
+ this.project = project;
+ }
+
static GoogleJavaFormatSettings getInstance(Project project) {
- return ServiceManager.getService(project, GoogleJavaFormatSettings.class);
+ return project.getService(GoogleJavaFormatSettings.class);
}
@Nullable
@@ -56,6 +61,9 @@ void setEnabled(boolean enabled) {
}
void setEnabled(EnabledState enabled) {
+ if (enabled.equals(EnabledState.ENABLED)) {
+ JreConfigurationChecker.checkJreConfiguration(project);
+ }
state.enabled = enabled;
}
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/InitialConfigurationProjectManagerListener.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/InitialConfigurationStartupActivity.java
similarity index 79%
rename from idea_plugin/src/com/google/googlejavaformat/intellij/InitialConfigurationProjectManagerListener.java
rename to idea_plugin/src/main/java/com/google/googlejavaformat/intellij/InitialConfigurationStartupActivity.java
index 1906347f7..940def655 100644
--- a/idea_plugin/src/com/google/googlejavaformat/intellij/InitialConfigurationProjectManagerListener.java
+++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/InitialConfigurationStartupActivity.java
@@ -21,22 +21,24 @@
import com.intellij.notification.NotificationGroupManager;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.project.Project;
-import com.intellij.openapi.project.ProjectManagerListener;
+import com.intellij.openapi.startup.StartupActivity;
import org.jetbrains.annotations.NotNull;
-final class InitialConfigurationProjectManagerListener implements ProjectManagerListener {
+final class InitialConfigurationStartupActivity implements StartupActivity.Background {
private static final String NOTIFICATION_TITLE = "Enable google-java-format";
private static final NotificationGroup NOTIFICATION_GROUP =
NotificationGroupManager.getInstance().getNotificationGroup(NOTIFICATION_TITLE);
@Override
- public void projectOpened(@NotNull Project project) {
+ public void runActivity(@NotNull Project project) {
GoogleJavaFormatSettings settings = GoogleJavaFormatSettings.getInstance(project);
if (settings.isUninitialized()) {
settings.setEnabled(false);
displayNewUserNotification(project, settings);
+ } else if (settings.isEnabled()) {
+ JreConfigurationChecker.checkJreConfiguration(project);
}
}
@@ -47,11 +49,12 @@ private void displayNewUserNotification(Project project, GoogleJavaFormatSetting
NOTIFICATION_TITLE,
"The google-java-format plugin is disabled by default. "
+ "Enable for this project.",
- NotificationType.INFORMATION,
- (n, e) -> {
- settings.setEnabled(true);
- n.expire();
- });
+ NotificationType.INFORMATION);
+ notification.setListener(
+ (n, e) -> {
+ settings.setEnabled(true);
+ n.expire();
+ });
notification.notify(project);
}
}
diff --git a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/JreConfigurationChecker.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/JreConfigurationChecker.java
new file mode 100644
index 000000000..5084b6a39
--- /dev/null
+++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/JreConfigurationChecker.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2023 Google 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.google.googlejavaformat.intellij;
+
+import com.google.common.base.Suppliers;
+import com.intellij.ide.ui.IdeUiService;
+import com.intellij.notification.Notification;
+import com.intellij.notification.NotificationType;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import java.util.function.Supplier;
+
+class JreConfigurationChecker {
+
+ private static final Supplier hasAccess =
+ Suppliers.memoize(JreConfigurationChecker::checkJreConfiguration);
+ private static final Logger logger = Logger.getInstance(JreConfigurationChecker.class);
+
+ private final Project project;
+
+ public JreConfigurationChecker(Project project) {
+ this.project = project;
+ }
+
+ static boolean checkJreConfiguration(Project project) {
+ var success = hasAccess.get();
+ if (!success) {
+ project.getService(JreConfigurationChecker.class).displayConfigurationErrorNotification();
+ }
+ return success;
+ }
+
+ /**
+ * Determine whether the JRE is configured to work with the google-java-format plugin. If not,
+ * display a notification with instructions and return false.
+ */
+ private static boolean checkJreConfiguration() {
+ try {
+ return testClassAccess(
+ "com.sun.tools.javac.api.JavacTrees",
+ "com.sun.tools.javac.code.Flags",
+ "com.sun.tools.javac.file.JavacFileManager",
+ "com.sun.tools.javac.parser.JavacParser",
+ "com.sun.tools.javac.tree.JCTree",
+ "com.sun.tools.javac.util.Log");
+ } catch (ClassNotFoundException e) {
+ logger.error("Error checking jre configuration for google-java-format", e);
+ return false;
+ }
+ }
+
+ private static boolean testClassAccess(String... classNames) throws ClassNotFoundException {
+ for (String className : classNames) {
+ if (!testClassAccess(className)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean testClassAccess(String className) throws ClassNotFoundException {
+ Class> klass = Class.forName(className);
+ return klass
+ .getModule()
+ // isExported returns true if the package is either open or exported. Either one is
+ // sufficient
+ // to run the google-java-format code (even though the documentation specifies --add-opens).
+ .isExported(
+ klass.getPackageName(),
+ JreConfigurationChecker.class.getClassLoader().getUnnamedModule());
+ }
+
+ private void displayConfigurationErrorNotification() {
+ Notification notification =
+ new Notification(
+ "Configure JRE for google-java-format",
+ "Configure the JRE for google-java-format",
+ "The google-java-format plugin needs additional configuration before it can be used. "
+ + "Follow the instructions here.",
+ NotificationType.INFORMATION);
+ notification.setListener(
+ (n, e) -> {
+ IdeUiService.getInstance()
+ .browse(
+ "https://github.com/google/google-java-format/blob/master/README.md#intellij-jre-config");
+ n.expire();
+ });
+ notification.notify(project);
+ }
+}
diff --git a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/Notifications.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/Notifications.java
new file mode 100644
index 000000000..d32aa98b9
--- /dev/null
+++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/Notifications.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 Google 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.google.googlejavaformat.intellij;
+
+import com.intellij.formatting.service.FormattingNotificationService;
+import com.intellij.openapi.project.Project;
+
+class Notifications {
+
+ static final String PARSING_ERROR_NOTIFICATION_GROUP = "google-java-format parsing error";
+ static final String PARSING_ERROR_TITLE = PARSING_ERROR_NOTIFICATION_GROUP;
+
+ static String parsingErrorMessage(String filename) {
+ return "google-java-format failed. Does " + filename + " have syntax errors?";
+ }
+
+ static void displayParsingErrorNotification(Project project, String filename) {
+ FormattingNotificationService.getInstance(project)
+ .reportError(
+ Notifications.PARSING_ERROR_NOTIFICATION_GROUP,
+ Notifications.PARSING_ERROR_TITLE,
+ Notifications.parsingErrorMessage(filename));
+ }
+}
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/UiFormatterStyle.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/UiFormatterStyle.java
similarity index 100%
rename from idea_plugin/src/com/google/googlejavaformat/intellij/UiFormatterStyle.java
rename to idea_plugin/src/main/java/com/google/googlejavaformat/intellij/UiFormatterStyle.java
diff --git a/idea_plugin/resources/META-INF/plugin.xml b/idea_plugin/src/main/resources/META-INF/plugin.xml
similarity index 61%
rename from idea_plugin/resources/META-INF/plugin.xml
rename to idea_plugin/src/main/resources/META-INF/plugin.xml
index 42d5f3b31..42011a208 100644
--- a/idea_plugin/resources/META-INF/plugin.xml
+++ b/idea_plugin/src/main/resources/META-INF/plugin.xml
@@ -22,13 +22,29 @@
Google
-
- com.intellij.java
+ com.intellij.modules.java
+ com.intellij.modules.lang
+ com.intellij.modules.platform
+
+
+ This plugin requires additional IDE configuration. For more information,
+ read
+ the documentation.
+ ]]>
+ 1.16.0.2
+ Disable AD_HOC_FORMATTING, which should stop the formatter from running so often when it wasn't specifically requested.
+ 1.16.0.1
+ When the plugin isn't configured correctly, show the error on every
+ format command. Previously it was only being shown at startup and going
+ unnoticed.
+ 1.16.0.0
+ Updated to use google-java-format 1.16.0.
+ Use the new IDE formatting APIs for a simplified plugin.
+ Optimize Imports now uses google-java-format.
1.15.0.0
Updated to use google-java-format 1.15.0.
1.14.0.0
@@ -62,23 +78,23 @@
]]>
-
-
-
-
-
+
+
+
+
+
diff --git a/idea_plugin/src/test/java/com/google/googlejavaformat/intellij/GoogleJavaFormatFormattingServiceTest.java b/idea_plugin/src/test/java/com/google/googlejavaformat/intellij/GoogleJavaFormatFormattingServiceTest.java
new file mode 100644
index 000000000..fec086c68
--- /dev/null
+++ b/idea_plugin/src/test/java/com/google/googlejavaformat/intellij/GoogleJavaFormatFormattingServiceTest.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2023 Google 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.google.googlejavaformat.intellij;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.googlejavaformat.intellij.GoogleJavaFormatSettings.State;
+import com.google.googlejavaformat.java.Formatter;
+import com.google.googlejavaformat.java.JavaFormatterOptions;
+import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
+import com.intellij.formatting.service.AsyncFormattingRequest;
+import com.intellij.formatting.service.FormattingService;
+import com.intellij.formatting.service.FormattingServiceUtil;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.testFramework.ExtensionTestUtil;
+import com.intellij.testFramework.fixtures.IdeaProjectTestFixture;
+import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory;
+import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture;
+import com.intellij.testFramework.fixtures.JavaTestFixtureFactory;
+import com.intellij.testFramework.fixtures.TestFixtureBuilder;
+import java.io.IOException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GoogleJavaFormatFormattingServiceTest {
+ private JavaCodeInsightTestFixture fixture;
+ private GoogleJavaFormatSettings settings;
+ private DelegatingFormatter delegatingFormatter;
+
+ @Before
+ public void setUp() throws Exception {
+ TestFixtureBuilder projectBuilder =
+ IdeaTestFixtureFactory.getFixtureFactory().createFixtureBuilder(getClass().getName());
+ fixture =
+ JavaTestFixtureFactory.getFixtureFactory()
+ .createCodeInsightFixture(projectBuilder.getFixture());
+ fixture.setUp();
+
+ delegatingFormatter = new DelegatingFormatter();
+ ExtensionTestUtil.maskExtensions(
+ FormattingService.EP_NAME,
+ ImmutableList.of(delegatingFormatter),
+ fixture.getProjectDisposable());
+
+ settings = GoogleJavaFormatSettings.getInstance(fixture.getProject());
+ State resetState = new State();
+ resetState.setEnabled("true");
+ settings.loadState(resetState);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ fixture.tearDown();
+ }
+
+ @Test
+ public void defaultFormatSettings() throws Exception {
+ PsiFile file =
+ createPsiFile(
+ "com/foo/FormatTest.java",
+ "package com.foo;",
+ "public class FormatTest {",
+ "static final String CONST_STR = \"Hello\";",
+ "}");
+ String origText = file.getText();
+ CodeStyleManager manager = CodeStyleManager.getInstance(file.getProject());
+ WriteCommandAction.runWriteCommandAction(
+ file.getProject(), () -> manager.reformatText(file, 0, file.getTextLength()));
+
+ assertThat(file.getText()).isEqualTo(new Formatter().formatSource(origText));
+ assertThat(delegatingFormatter.wasInvoked()).isTrue();
+ }
+
+ @Test
+ public void aospStyle() throws Exception {
+ settings.setStyle(Style.AOSP);
+ PsiFile file =
+ createPsiFile(
+ "com/foo/FormatTest.java",
+ "package com.foo;",
+ "public class FormatTest {",
+ "static final String CONST_STR = \"Hello\";",
+ "}");
+ String origText = file.getText();
+ CodeStyleManager manager = CodeStyleManager.getInstance(file.getProject());
+ WriteCommandAction.runWriteCommandAction(
+ file.getProject(), () -> manager.reformatText(file, 0, file.getTextLength()));
+
+ assertThat(file.getText())
+ .isEqualTo(
+ new Formatter(JavaFormatterOptions.builder().style(Style.AOSP).build())
+ .formatSource(origText));
+ assertThat(delegatingFormatter.wasInvoked()).isTrue();
+ }
+
+ @Test
+ public void canChangeWhitespaceOnlyDoesNotReorderModifiers() throws Exception {
+ settings.setStyle(Style.GOOGLE);
+ PsiFile file =
+ createPsiFile(
+ "com/foo/FormatTest.java",
+ "package com.foo;",
+ "public class FormatTest {",
+ "final static String CONST_STR = \"Hello\";",
+ "}");
+ CodeStyleManager manager = CodeStyleManager.getInstance(file.getProject());
+ var offset = file.getText().indexOf("final static");
+ WriteCommandAction.runWriteCommandAction(
+ file.getProject(),
+ () ->
+ FormattingServiceUtil.formatElement(
+ file.findElementAt(offset), /* canChangeWhitespaceOnly= */ true));
+
+ // In non-whitespace mode, this would flip the order of these modifiers. (Also check for leading
+ // spaces to make sure the formatter actually ran.
+ assertThat(file.getText()).containsMatch(" final static");
+ assertThat(delegatingFormatter.wasInvoked()).isTrue();
+ }
+
+ @Test
+ public void canChangeWhitespaceOnlyDoesNotReformatJavadoc() throws Exception {
+ settings.setStyle(Style.GOOGLE);
+ PsiFile file =
+ createPsiFile(
+ "com/foo/FormatTest.java",
+ "package com.foo;",
+ "public class FormatTest {",
+ "/**",
+ " * hello",
+ " */",
+ "static final String CONST_STR = \"Hello\";",
+ "}");
+ CodeStyleManager manager = CodeStyleManager.getInstance(file.getProject());
+ var offset = file.getText().indexOf("hello");
+ WriteCommandAction.runWriteCommandAction(
+ file.getProject(),
+ () ->
+ FormattingServiceUtil.formatElement(
+ file.findElementAt(offset), /* canChangeWhitespaceOnly= */ true));
+
+ // In non-whitespace mode, this would join the Javadoc into a single line.
+ assertThat(file.getText()).containsMatch(" \\* hello");
+ // Also check for leading spaces to make sure the formatter actually ran. (Technically, this is
+ // outside the range that we asked to be formatted, but gjf will still format it.)
+ assertThat(file.getText()).containsMatch(" static final");
+ assertThat(delegatingFormatter.wasInvoked()).isTrue();
+ }
+
+ @Test
+ public void canChangeNonWhitespaceReordersModifiers() throws Exception {
+ settings.setStyle(Style.GOOGLE);
+ PsiFile file =
+ createPsiFile(
+ "com/foo/FormatTest.java",
+ "package com.foo;",
+ "public class FormatTest {",
+ "final static String CONST_STR = \"Hello\";",
+ "}");
+ CodeStyleManager manager = CodeStyleManager.getInstance(file.getProject());
+ var offset = file.getText().indexOf("final static");
+ WriteCommandAction.runWriteCommandAction(
+ file.getProject(),
+ () ->
+ FormattingServiceUtil.formatElement(
+ file.findElementAt(offset), /* canChangeWhitespaceOnly= */ false));
+
+ assertThat(file.getText()).containsMatch("static final");
+ assertThat(delegatingFormatter.wasInvoked()).isTrue();
+ }
+
+ @Test
+ public void canChangeNonWhitespaceReformatsJavadoc() throws Exception {
+ settings.setStyle(Style.GOOGLE);
+ PsiFile file =
+ createPsiFile(
+ "com/foo/FormatTest.java",
+ "package com.foo;",
+ "public class FormatTest {",
+ "/**",
+ " * hello",
+ " */",
+ "static final String CONST_STR = \"Hello\";",
+ "}");
+ CodeStyleManager manager = CodeStyleManager.getInstance(file.getProject());
+ var offset = file.getText().indexOf("hello");
+ WriteCommandAction.runWriteCommandAction(
+ file.getProject(),
+ () ->
+ FormattingServiceUtil.formatElement(
+ file.findElementAt(offset), /* canChangeWhitespaceOnly= */ false));
+
+ assertThat(file.getText()).containsMatch("/\\*\\* hello \\*/");
+ assertThat(delegatingFormatter.wasInvoked()).isTrue();
+ }
+
+ private PsiFile createPsiFile(String path, String... contents) throws IOException {
+ VirtualFile virtualFile =
+ fixture.getTempDirFixture().createFile(path, String.join("\n", contents));
+ fixture.configureFromExistingVirtualFile(virtualFile);
+ PsiFile psiFile = fixture.getFile();
+ assertThat(psiFile).isNotNull();
+ return psiFile;
+ }
+
+ private static final class DelegatingFormatter extends GoogleJavaFormatFormattingService {
+
+ private boolean invoked = false;
+
+ private boolean wasInvoked() {
+ return invoked;
+ }
+
+ @Override
+ protected FormattingTask createFormattingTask(AsyncFormattingRequest request) {
+ FormattingTask delegateTask = super.createFormattingTask(request);
+ return new FormattingTask() {
+ @Override
+ public boolean cancel() {
+ return delegateTask.cancel();
+ }
+
+ @Override
+ public void run() {
+ invoked = true;
+ delegateTask.run();
+ }
+ };
+ }
+ }
+}
diff --git a/idea_plugin/src/test/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizerTest.java b/idea_plugin/src/test/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizerTest.java
new file mode 100644
index 000000000..ad9fe2744
--- /dev/null
+++ b/idea_plugin/src/test/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizerTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2023 Google 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.google.googlejavaformat.intellij;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.googlejavaformat.intellij.GoogleJavaFormatSettings.State;
+import com.intellij.codeInsight.actions.OptimizeImportsProcessor;
+import com.intellij.formatting.service.FormattingService;
+import com.intellij.lang.ImportOptimizer;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiFile;
+import com.intellij.testFramework.ExtensionTestUtil;
+import com.intellij.testFramework.fixtures.IdeaProjectTestFixture;
+import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory;
+import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture;
+import com.intellij.testFramework.fixtures.JavaTestFixtureFactory;
+import com.intellij.testFramework.fixtures.TestFixtureBuilder;
+import java.io.IOException;
+import java.util.Set;
+import org.jetbrains.annotations.NotNull;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GoogleJavaFormatImportOptimizerTest {
+ private JavaCodeInsightTestFixture fixture;
+ private DelegatingFormatter delegatingFormatter;
+
+ @Before
+ public void setUp() throws Exception {
+ TestFixtureBuilder projectBuilder =
+ IdeaTestFixtureFactory.getFixtureFactory().createFixtureBuilder(getClass().getName());
+ fixture =
+ JavaTestFixtureFactory.getFixtureFactory()
+ .createCodeInsightFixture(projectBuilder.getFixture());
+ fixture.setUp();
+
+ delegatingFormatter = new DelegatingFormatter();
+ ExtensionTestUtil.maskExtensions(
+ FormattingService.EP_NAME,
+ ImmutableList.of(delegatingFormatter),
+ fixture.getProjectDisposable());
+
+ var settings = GoogleJavaFormatSettings.getInstance(fixture.getProject());
+ State resetState = new State();
+ resetState.setEnabled("true");
+ settings.loadState(resetState);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ fixture.tearDown();
+ }
+
+ @Test
+ public void removesUnusedImports() throws Exception {
+ PsiFile file =
+ createPsiFile(
+ "com/foo/ImportTest.java",
+ "package com.foo;",
+ "import java.util.List;",
+ "import java.util.ArrayList;",
+ "import java.util.Map;",
+ "public class ImportTest {",
+ "static final Map map;",
+ "}");
+ OptimizeImportsProcessor processor = new OptimizeImportsProcessor(file.getProject(), file);
+ WriteCommandAction.runWriteCommandAction(
+ file.getProject(),
+ () -> {
+ processor.run();
+ PsiDocumentManager.getInstance(file.getProject()).commitAllDocuments();
+ });
+
+ assertThat(file.getText()).doesNotContain("List");
+ assertThat(file.getText()).contains("import java.util.Map;");
+ assertThat(delegatingFormatter.wasInvoked()).isTrue();
+ }
+
+ @Test
+ public void reordersImports() throws Exception {
+ PsiFile file =
+ createPsiFile(
+ "com/foo/ImportTest.java",
+ "package com.foo;",
+ "import java.util.List;",
+ "import java.util.ArrayList;",
+ "import java.util.Map;",
+ "public class ImportTest {",
+ "static final ArrayList arrayList;",
+ "static final List list;",
+ "static final Map map;",
+ "}");
+ OptimizeImportsProcessor processor = new OptimizeImportsProcessor(file.getProject(), file);
+ WriteCommandAction.runWriteCommandAction(
+ file.getProject(),
+ () -> {
+ processor.run();
+ PsiDocumentManager.getInstance(file.getProject()).commitAllDocuments();
+ });
+
+ assertThat(file.getText())
+ .contains(
+ "import java.util.ArrayList;\n"
+ + "import java.util.List;\n"
+ + "import java.util.Map;\n");
+ assertThat(delegatingFormatter.wasInvoked()).isTrue();
+ }
+
+ private PsiFile createPsiFile(String path, String... contents) throws IOException {
+ VirtualFile virtualFile =
+ fixture.getTempDirFixture().createFile(path, String.join("\n", contents));
+ fixture.configureFromExistingVirtualFile(virtualFile);
+ PsiFile psiFile = fixture.getFile();
+ assertThat(psiFile).isNotNull();
+ return psiFile;
+ }
+
+ private static final class DelegatingFormatter extends GoogleJavaFormatFormattingService {
+
+ private boolean invoked = false;
+
+ private boolean wasInvoked() {
+ return invoked;
+ }
+
+ @Override
+ public @NotNull Set getImportOptimizers(@NotNull PsiFile file) {
+ return super.getImportOptimizers(file).stream().map(this::wrap).collect(toImmutableSet());
+ }
+
+ private ImportOptimizer wrap(ImportOptimizer delegate) {
+ return new ImportOptimizer() {
+ @Override
+ public boolean supports(@NotNull PsiFile file) {
+ return delegate.supports(file);
+ }
+
+ @Override
+ public @NotNull Runnable processFile(@NotNull PsiFile file) {
+ return () -> {
+ invoked = true;
+ delegate.processFile(file).run();
+ };
+ }
+ };
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 150173ee6..43842be2b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,7 +23,7 @@
com.google.googlejavaformat
google-java-format-parent
pom
- HEAD-SNAPSHOT
+ 1.17.0
core
@@ -235,15 +235,12 @@
jdk.compiler/com.sun.tools.javac.api jdk.compiler/com.sun.tools.javac.code jdk.compiler/com.sun.tools.javac.file jdk.compiler/com.sun.tools.javac.main jdk.compiler/com.sun.tools.javac.model jdk.compiler/com.sun.tools.javac.parser jdk.compiler/com.sun.tools.javac.processing jdk.compiler/com.sun.tools.javac.tree jdk.compiler/com.sun.tools.javac.util
- jdk.compiler/com.sun.tools.javac.code jdk.compiler/com.sun.tools.javac.comp