1 /* 2 * Copyright 2023 Google Inc. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.googlejavaformat.intellij; 18 19 import com.google.common.collect.ImmutableList; 20 import com.google.common.collect.ImmutableSet; 21 import com.google.common.collect.Range; 22 import com.google.googlejavaformat.java.Formatter; 23 import com.google.googlejavaformat.java.FormatterException; 24 import com.google.googlejavaformat.java.JavaFormatterOptions; 25 import com.google.googlejavaformat.java.JavaFormatterOptions.Style; 26 import com.intellij.formatting.service.AsyncDocumentFormattingService; 27 import com.intellij.formatting.service.AsyncFormattingRequest; 28 import com.intellij.ide.highlighter.JavaFileType; 29 import com.intellij.lang.ImportOptimizer; 30 import com.intellij.openapi.project.Project; 31 import com.intellij.openapi.util.TextRange; 32 import com.intellij.psi.PsiFile; 33 import java.util.Collection; 34 import java.util.List; 35 import java.util.Set; 36 import org.jetbrains.annotations.NotNull; 37 38 /** Uses {@code google-java-format} to reformat code. */ 39 public class GoogleJavaFormatFormattingService extends AsyncDocumentFormattingService { 40 41 public static final ImmutableSet<ImportOptimizer> IMPORT_OPTIMIZERS = 42 ImmutableSet.of(new GoogleJavaFormatImportOptimizer()); 43 44 @Override createFormattingTask(AsyncFormattingRequest request)45 protected FormattingTask createFormattingTask(AsyncFormattingRequest request) { 46 Project project = request.getContext().getProject(); 47 48 if (!JreConfigurationChecker.checkJreConfiguration(project)) { 49 return null; 50 } 51 52 Style style = GoogleJavaFormatSettings.getInstance(project).getStyle(); 53 Formatter formatter = createFormatter(style, request.canChangeWhitespaceOnly()); 54 return new GoogleJavaFormatFormattingTask(formatter, request); 55 } 56 57 @Override getNotificationGroupId()58 protected String getNotificationGroupId() { 59 return Notifications.PARSING_ERROR_NOTIFICATION_GROUP; 60 } 61 62 @Override getName()63 protected String getName() { 64 return "google-java-format"; 65 } 66 createFormatter(Style style, boolean canChangeWhiteSpaceOnly)67 private static Formatter createFormatter(Style style, boolean canChangeWhiteSpaceOnly) { 68 JavaFormatterOptions.Builder optBuilder = JavaFormatterOptions.builder().style(style); 69 if (canChangeWhiteSpaceOnly) { 70 optBuilder.formatJavadoc(false).reorderModifiers(false); 71 } 72 return new Formatter(optBuilder.build()); 73 } 74 75 @Override getFeatures()76 public @NotNull Set<Feature> getFeatures() { 77 return Set.of(Feature.FORMAT_FRAGMENTS, Feature.OPTIMIZE_IMPORTS); 78 } 79 80 @Override canFormat(@otNull PsiFile file)81 public boolean canFormat(@NotNull PsiFile file) { 82 return JavaFileType.INSTANCE.equals(file.getFileType()) 83 && GoogleJavaFormatSettings.getInstance(file.getProject()).isEnabled(); 84 } 85 86 @Override getImportOptimizers(@otNull PsiFile file)87 public @NotNull Set<ImportOptimizer> getImportOptimizers(@NotNull PsiFile file) { 88 return IMPORT_OPTIMIZERS; 89 } 90 91 private static final class GoogleJavaFormatFormattingTask implements FormattingTask { 92 private final Formatter formatter; 93 private final AsyncFormattingRequest request; 94 GoogleJavaFormatFormattingTask(Formatter formatter, AsyncFormattingRequest request)95 private GoogleJavaFormatFormattingTask(Formatter formatter, AsyncFormattingRequest request) { 96 this.formatter = formatter; 97 this.request = request; 98 } 99 100 @Override run()101 public void run() { 102 try { 103 String formattedText = formatter.formatSource(request.getDocumentText(), toRanges(request)); 104 request.onTextReady(formattedText); 105 } catch (FormatterException e) { 106 request.onError( 107 Notifications.PARSING_ERROR_TITLE, 108 Notifications.parsingErrorMessage(request.getContext().getContainingFile().getName())); 109 } 110 } 111 toRanges(AsyncFormattingRequest request)112 private static Collection<Range<Integer>> toRanges(AsyncFormattingRequest request) { 113 if (isWholeFile(request)) { 114 // The IDE sometimes passes invalid ranges when the file is unsaved before invoking the 115 // formatter. So this is a workaround for that issue. 116 return ImmutableList.of(Range.closedOpen(0, request.getDocumentText().length())); 117 } 118 return request.getFormattingRanges().stream() 119 .map(textRange -> Range.closedOpen(textRange.getStartOffset(), textRange.getEndOffset())) 120 .collect(ImmutableList.toImmutableList()); 121 } 122 isWholeFile(AsyncFormattingRequest request)123 private static boolean isWholeFile(AsyncFormattingRequest request) { 124 List<TextRange> ranges = request.getFormattingRanges(); 125 return ranges.size() == 1 126 && ranges.get(0).getStartOffset() == 0 127 // using greater than or equal because ranges are sometimes passed inaccurately 128 && ranges.get(0).getEndOffset() >= request.getDocumentText().length(); 129 } 130 131 @Override isRunUnderProgress()132 public boolean isRunUnderProgress() { 133 return true; 134 } 135 136 @Override cancel()137 public boolean cancel() { 138 return false; 139 } 140 } 141 } 142