Move the plugin to the FormattingService API.

Also support optimizing imports, using the ImportOptimizer API.

PiperOrigin-RevId: 513562756
diff --git a/idea_plugin/build.gradle b/idea_plugin/build.gradle
index 6c9695d..7321486 100644
--- a/idea_plugin/build.gradle
+++ b/idea_plugin/build.gradle
@@ -15,7 +15,7 @@
  */
 
 plugins {
-  id "org.jetbrains.intellij" version "1.4.0"
+  id "org.jetbrains.intellij" version "1.13.0"
 }
 
 repositories {
@@ -23,7 +23,7 @@
 }
 
 ext {
-  googleJavaFormatVersion = "1.15.0"
+  googleJavaFormatVersion = "1.16.0"
 }
 
 apply plugin: "org.jetbrains.intellij"
@@ -35,14 +35,14 @@
 intellij {
   pluginName = "google-java-format"
   plugins = ["java"]
-  version = "221.3427-EAP-CANDIDATE-SNAPSHOT"
+  version = "2021.3"
 }
 
 patchPluginXml {
   pluginDescription = "Formats source code using the google-java-format tool. This version of " +
                       "the plugin uses version ${googleJavaFormatVersion} of the tool."
   version.set("${googleJavaFormatVersion}.0")
-  sinceBuild = "203"
+  sinceBuild = "213"
   untilBuild = ""
 }
 
@@ -50,6 +50,16 @@
   token = project.ext.properties.jetbrainsPluginRepoToken
 }
 
+tasks.withType(Test).configureEach {
+  jvmArgs += "--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED"
+  jvmArgs += "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED"
+  jvmArgs += "--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED"
+  jvmArgs += "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
+}
+
 dependencies {
   implementation "com.google.googlejavaformat:google-java-format:${googleJavaFormatVersion}"
+  testImplementation "junit:junit:4.13.2"
+  testImplementation "com.google.truth:truth:1.1.3"
+  testImplementation "com.google.truth.extensions:truth-java8-extension:1.1.3"
 }
diff --git a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/CodeStyleManagerDecorator.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/CodeStyleManagerDecorator.java
deleted file mode 100644
index af5da95..0000000
--- a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/CodeStyleManagerDecorator.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * Copyright 2015 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.FormattingMode;
-import com.intellij.lang.ASTNode;
-import com.intellij.openapi.editor.Document;
-import com.intellij.openapi.fileTypes.FileType;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.util.Computable;
-import com.intellij.openapi.util.TextRange;
-import com.intellij.psi.PsiElement;
-import com.intellij.psi.PsiFile;
-import com.intellij.psi.codeStyle.ChangedRangesInfo;
-import com.intellij.psi.codeStyle.CodeStyleManager;
-import com.intellij.psi.codeStyle.DocCommentSettings;
-import com.intellij.psi.codeStyle.FormattingModeAwareIndentAdjuster;
-import com.intellij.psi.codeStyle.Indent;
-import com.intellij.util.IncorrectOperationException;
-import com.intellij.util.ThrowableRunnable;
-import java.util.Collection;
-import org.checkerframework.checker.nullness.qual.Nullable;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Decorates the {@link CodeStyleManager} abstract class by delegating to a concrete implementation
- * instance (likely IntelliJ's default instance).
- */
-@SuppressWarnings("deprecation")
-class CodeStyleManagerDecorator extends CodeStyleManager
-    implements FormattingModeAwareIndentAdjuster {
-
-  private final CodeStyleManager delegate;
-
-  CodeStyleManagerDecorator(CodeStyleManager delegate) {
-    this.delegate = delegate;
-  }
-
-  CodeStyleManager getDelegate() {
-    return delegate;
-  }
-
-  @Override
-  public @NotNull Project getProject() {
-    return delegate.getProject();
-  }
-
-  @Override
-  public @NotNull PsiElement reformat(@NotNull PsiElement element)
-      throws IncorrectOperationException {
-    return delegate.reformat(element);
-  }
-
-  @Override
-  public @NotNull PsiElement reformat(@NotNull PsiElement element, boolean canChangeWhiteSpacesOnly)
-      throws IncorrectOperationException {
-    return delegate.reformat(element, canChangeWhiteSpacesOnly);
-  }
-
-  @Override
-  public PsiElement reformatRange(@NotNull PsiElement element, int startOffset, int endOffset)
-      throws IncorrectOperationException {
-    return delegate.reformatRange(element, startOffset, endOffset);
-  }
-
-  @Override
-  public PsiElement reformatRange(
-      @NotNull PsiElement element, int startOffset, int endOffset, boolean canChangeWhiteSpacesOnly)
-      throws IncorrectOperationException {
-    return delegate.reformatRange(element, startOffset, endOffset, canChangeWhiteSpacesOnly);
-  }
-
-  @Override
-  public void reformatText(@NotNull PsiFile file, int startOffset, int endOffset)
-      throws IncorrectOperationException {
-    delegate.reformatText(file, startOffset, endOffset);
-  }
-
-  @Override
-  public void reformatText(@NotNull PsiFile file, @NotNull Collection<? extends TextRange> ranges)
-      throws IncorrectOperationException {
-    delegate.reformatText(file, ranges);
-  }
-
-  @Override
-  public void reformatTextWithContext(
-      @NotNull PsiFile psiFile, @NotNull ChangedRangesInfo changedRangesInfo)
-      throws IncorrectOperationException {
-    delegate.reformatTextWithContext(psiFile, changedRangesInfo);
-  }
-
-  @Override
-  public void reformatTextWithContext(
-      @NotNull PsiFile file, @NotNull Collection<? extends TextRange> ranges)
-      throws IncorrectOperationException {
-    delegate.reformatTextWithContext(file, ranges);
-  }
-
-  @Override
-  public void adjustLineIndent(@NotNull PsiFile file, TextRange rangeToAdjust)
-      throws IncorrectOperationException {
-    delegate.adjustLineIndent(file, rangeToAdjust);
-  }
-
-  @Override
-  public int adjustLineIndent(@NotNull PsiFile file, int offset)
-      throws IncorrectOperationException {
-    return delegate.adjustLineIndent(file, offset);
-  }
-
-  @Override
-  public int adjustLineIndent(@NotNull Document document, int offset) {
-    return delegate.adjustLineIndent(document, offset);
-  }
-
-  public void scheduleIndentAdjustment(@NotNull Document document, int offset) {
-    delegate.scheduleIndentAdjustment(document, offset);
-  }
-
-  @Override
-  public boolean isLineToBeIndented(@NotNull PsiFile file, int offset) {
-    return delegate.isLineToBeIndented(file, offset);
-  }
-
-  @Override
-  @Nullable
-  public String getLineIndent(@NotNull PsiFile file, int offset) {
-    return delegate.getLineIndent(file, offset);
-  }
-
-  @Override
-  @Nullable
-  public String getLineIndent(@NotNull PsiFile file, int offset, FormattingMode mode) {
-    return delegate.getLineIndent(file, offset, mode);
-  }
-
-  @Override
-  @Nullable
-  public String getLineIndent(@NotNull Document document, int offset) {
-    return delegate.getLineIndent(document, offset);
-  }
-
-  @Override
-  public Indent getIndent(String text, FileType fileType) {
-    return delegate.getIndent(text, fileType);
-  }
-
-  @Override
-  public String fillIndent(Indent indent, FileType fileType) {
-    return delegate.fillIndent(indent, fileType);
-  }
-
-  @Override
-  public Indent zeroIndent() {
-    return delegate.zeroIndent();
-  }
-
-  @Override
-  public void reformatNewlyAddedElement(@NotNull ASTNode block, @NotNull ASTNode addedElement)
-      throws IncorrectOperationException {
-    delegate.reformatNewlyAddedElement(block, addedElement);
-  }
-
-  @Override
-  public boolean isSequentialProcessingAllowed() {
-    return delegate.isSequentialProcessingAllowed();
-  }
-
-  @Override
-  public void performActionWithFormatterDisabled(Runnable r) {
-    delegate.performActionWithFormatterDisabled(r);
-  }
-
-  @Override
-  public <T extends Throwable> void performActionWithFormatterDisabled(ThrowableRunnable<T> r)
-      throws T {
-    delegate.performActionWithFormatterDisabled(r);
-  }
-
-  @Override
-  public <T> T performActionWithFormatterDisabled(Computable<T> r) {
-    return delegate.performActionWithFormatterDisabled(r);
-  }
-
-  @Override
-  public int getSpacing(@NotNull PsiFile file, int offset) {
-    return delegate.getSpacing(file, offset);
-  }
-
-  @Override
-  public int getMinLineFeeds(@NotNull PsiFile file, int offset) {
-    return delegate.getMinLineFeeds(file, offset);
-  }
-
-  @Override
-  public void runWithDocCommentFormattingDisabled(
-      @NotNull PsiFile file, @NotNull Runnable runnable) {
-    delegate.runWithDocCommentFormattingDisabled(file, runnable);
-  }
-
-  @Override
-  public @NotNull DocCommentSettings getDocCommentSettings(@NotNull PsiFile file) {
-    return delegate.getDocCommentSettings(file);
-  }
-
-  // From FormattingModeAwareIndentAdjuster
-
-  /** Uses same fallback as {@link CodeStyleManager#getCurrentFormattingMode}. */
-  @Override
-  public FormattingMode getCurrentFormattingMode() {
-    if (delegate instanceof FormattingModeAwareIndentAdjuster) {
-      return ((FormattingModeAwareIndentAdjuster) delegate).getCurrentFormattingMode();
-    }
-    return FormattingMode.REFORMAT;
-  }
-
-  @Override
-  public int adjustLineIndent(
-      final @NotNull Document document, final int offset, FormattingMode mode)
-      throws IncorrectOperationException {
-    if (delegate instanceof FormattingModeAwareIndentAdjuster) {
-      return ((FormattingModeAwareIndentAdjuster) delegate)
-          .adjustLineIndent(document, offset, mode);
-    }
-    return offset;
-  }
-
-  @Override
-  public void scheduleReformatWhenSettingsComputed(@NotNull PsiFile file) {
-    delegate.scheduleReformatWhenSettingsComputed(file);
-  }
-}
diff --git a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/FormatterUtil.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/FormatterUtil.java
deleted file mode 100644
index a5e69c9..0000000
--- a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/FormatterUtil.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright 2015 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.google.common.collect.BoundType;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Range;
-import com.google.googlejavaformat.java.Formatter;
-import com.google.googlejavaformat.java.FormatterException;
-import com.intellij.openapi.util.TextRange;
-import java.util.Collection;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-final class FormatterUtil {
-
-  private FormatterUtil() {}
-
-  static Map<TextRange, String> getReplacements(
-      Formatter formatter, String text, Collection<? extends TextRange> ranges) {
-    try {
-      ImmutableMap.Builder<TextRange, String> replacements = ImmutableMap.builder();
-      formatter
-          .getFormatReplacements(text, toRanges(ranges))
-          .forEach(
-              replacement ->
-                  replacements.put(
-                      toTextRange(replacement.getReplaceRange()),
-                      replacement.getReplacementString()));
-      return replacements.build();
-    } catch (FormatterException e) {
-      return ImmutableMap.of();
-    }
-  }
-
-  private static Collection<Range<Integer>> toRanges(Collection<? extends TextRange> textRanges) {
-    return textRanges.stream()
-        .map(textRange -> Range.closedOpen(textRange.getStartOffset(), textRange.getEndOffset()))
-        .collect(Collectors.toList());
-  }
-
-  private static TextRange toTextRange(Range<Integer> range) {
-    checkState(
-        range.lowerBoundType().equals(BoundType.CLOSED)
-            && range.upperBoundType().equals(BoundType.OPEN));
-    return new TextRange(range.lowerEndpoint(), range.upperEndpoint());
-  }
-}
diff --git a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatCodeStyleManager.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatCodeStyleManager.java
deleted file mode 100644
index c9aa64a..0000000
--- a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatCodeStyleManager.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright 2015 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 java.util.Comparator.comparing;
-
-import com.google.common.collect.ImmutableList;
-import com.google.googlejavaformat.java.Formatter;
-import com.google.googlejavaformat.java.JavaFormatterOptions;
-import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
-import com.intellij.ide.highlighter.JavaFileType;
-import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.command.WriteCommandAction;
-import com.intellij.openapi.editor.Document;
-import com.intellij.openapi.util.TextRange;
-import com.intellij.psi.PsiDocumentManager;
-import com.intellij.psi.PsiElement;
-import com.intellij.psi.PsiFile;
-import com.intellij.psi.codeStyle.ChangedRangesInfo;
-import com.intellij.psi.codeStyle.CodeStyleManager;
-import com.intellij.psi.impl.CheckUtil;
-import com.intellij.util.IncorrectOperationException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.TreeMap;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * A {@link CodeStyleManager} implementation which formats .java files with google-java-format.
- * Formatting of all other types of files is delegated to IntelliJ's default implementation.
- */
-class GoogleJavaFormatCodeStyleManager extends CodeStyleManagerDecorator {
-
-  public GoogleJavaFormatCodeStyleManager(@NotNull CodeStyleManager original) {
-    super(original);
-  }
-
-  @Override
-  public void reformatText(@NotNull PsiFile file, int startOffset, int endOffset)
-      throws IncorrectOperationException {
-    if (overrideFormatterForFile(file)) {
-      formatInternal(file, ImmutableList.of(new TextRange(startOffset, endOffset)));
-    } else {
-      super.reformatText(file, startOffset, endOffset);
-    }
-  }
-
-  @Override
-  public void reformatText(@NotNull PsiFile file, @NotNull Collection<? extends TextRange> ranges)
-      throws IncorrectOperationException {
-    if (overrideFormatterForFile(file)) {
-      formatInternal(file, ranges);
-    } else {
-      super.reformatText(file, ranges);
-    }
-  }
-
-  @Override
-  public void reformatTextWithContext(@NotNull PsiFile file, @NotNull ChangedRangesInfo info)
-      throws IncorrectOperationException {
-    List<TextRange> 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.
-   *
-   * <p>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<TextRange, String> replacements) {
-
-    if (replacements.isEmpty()) {
-      return;
-    }
-
-    TreeMap<TextRange, String> sorted = new TreeMap<>(comparing(TextRange::getStartOffset));
-    sorted.putAll(replacements);
-    WriteCommandAction.runWriteCommandAction(
-        getProject(),
-        () -> {
-          for (Entry<TextRange, String> 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/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 0000000..1e7ca1a
--- /dev/null
+++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatFormattingService.java
@@ -0,0 +1,136 @@
+/*
+ * 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<ImportOptimizer> IMPORT_OPTIMIZERS =
+      ImmutableSet.of(new GoogleJavaFormatImportOptimizer());
+
+  @Override
+  protected FormattingTask createFormattingTask(AsyncFormattingRequest request) {
+    Project project = request.getContext().getProject();
+    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<Feature> getFeatures() {
+    return Set.of(Feature.AD_HOC_FORMATTING, 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<ImportOptimizer> 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<Range<Integer>> 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<TextRange> 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 0000000..3a9a30f
--- /dev/null
+++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizer.java
@@ -0,0 +1,65 @@
+/*
+ * 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();
+    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/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatInstaller.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatInstaller.java
deleted file mode 100644
index c606736..0000000
--- a/idea_plugin/src/main/java/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/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 0000000..d32aa98
--- /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/main/resources/META-INF/plugin.xml b/idea_plugin/src/main/resources/META-INF/plugin.xml
index 42d5f3b..2a82c95 100644
--- a/idea_plugin/src/main/resources/META-INF/plugin.xml
+++ b/idea_plugin/src/main/resources/META-INF/plugin.xml
@@ -22,13 +22,16 @@
     Google
   </vendor>
 
-  <!-- Mark it as available on all JetBrains IDEs. It's really only useful in
-       IDEA and Android Studio, but there's no way to specify that for some
-       reason. It won't crash PyCharm or anything, so whatever. -->
-  <depends>com.intellij.java</depends>
+  <depends>com.intellij.modules.java</depends>
+  <depends>com.intellij.modules.lang</depends>
+  <depends>com.intellij.modules.platform</depends>
 
   <change-notes><![CDATA[
     <dl>
+      <dt>1.16.0.0</dt>
+      <dd>Updated to use google-java-format 1.16.0.</dd>
+      <dd>Use the new IDE formatting APIs for a simplified plugin.</dd>
+      <dd>Optimize Imports now uses google-java-format.</dd>
       <dt>1.15.0.0</dt>
       <dd>Updated to use google-java-format 1.15.0.</dd>
       <dt>1.14.0.0</dt>
@@ -66,11 +69,11 @@
     <listener
       class="com.google.googlejavaformat.intellij.InitialConfigurationProjectManagerListener"
       topic="com.intellij.openapi.project.ProjectManagerListener"/>
-    <listener class="com.google.googlejavaformat.intellij.GoogleJavaFormatInstaller"
-      topic="com.intellij.openapi.project.ProjectManagerListener"/>
   </applicationListeners>
 
   <extensions defaultExtensionNs="com.intellij">
+    <formattingService
+      implementation="com.google.googlejavaformat.intellij.GoogleJavaFormatFormattingService"/>
     <projectConfigurable
       instance="com.google.googlejavaformat.intellij.GoogleJavaFormatConfigurable"
       id="google-java-format.settings"
@@ -78,7 +81,9 @@
     <projectService
       serviceImplementation="com.google.googlejavaformat.intellij.GoogleJavaFormatSettings"/>
     <notificationGroup displayType="STICKY_BALLOON" id="Enable google-java-format"
-      isLogByDefault="true"/>
+      isLogByDefault="false"/>
+    <notificationGroup displayType="BALLOON" id="google-java-format parsing error"
+      isLogByDefault="false"/>
   </extensions>
 
 </idea-plugin>
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 0000000..fec086c
--- /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<IdeaProjectTestFixture> 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.<PsiElement>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.<PsiElement>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.<PsiElement>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.<PsiElement>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 0000000..ad9fe27
--- /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<IdeaProjectTestFixture> 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<ImportOptimizer> 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();
+          };
+        }
+      };
+    }
+  }
+}