|  | 
|  | 1 | +/* | 
|  | 2 | + * Copyright 2025 Google Inc. | 
|  | 3 | + * | 
|  | 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except | 
|  | 5 | + * in compliance with the License. You may obtain a copy of the License at | 
|  | 6 | + * | 
|  | 7 | + *     http://www.apache.org/licenses/LICENSE-2.0 | 
|  | 8 | + * | 
|  | 9 | + * Unless required by applicable law or agreed to in writing, software distributed under the License | 
|  | 10 | + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express | 
|  | 11 | + * or implied. See the License for the specific language governing permissions and limitations under | 
|  | 12 | + * the License. | 
|  | 13 | + */ | 
|  | 14 | + | 
|  | 15 | +package com.google.googlejavaformat.java; | 
|  | 16 | + | 
|  | 17 | +import com.google.common.base.Preconditions; | 
|  | 18 | +import com.google.common.collect.Range; | 
|  | 19 | +import com.google.googlejavaformat.java.SnippetFormatter.SnippetKind; | 
|  | 20 | +import java.util.ArrayList; | 
|  | 21 | +import java.util.List; | 
|  | 22 | +import org.eclipse.jdt.core.dom.ASTParser; | 
|  | 23 | +import org.eclipse.jdt.core.formatter.CodeFormatter; | 
|  | 24 | +import org.eclipse.jface.text.IRegion; | 
|  | 25 | +import org.eclipse.jface.text.Region; | 
|  | 26 | +import org.eclipse.text.edits.MultiTextEdit; | 
|  | 27 | +import org.eclipse.text.edits.ReplaceEdit; | 
|  | 28 | +import org.eclipse.text.edits.TextEdit; | 
|  | 29 | + | 
|  | 30 | +/** Runs the Google Java formatter on the given code. */ | 
|  | 31 | +public class JavaFormatterBase extends CodeFormatter { | 
|  | 32 | + | 
|  | 33 | +  private static final int INDENTATION_SIZE = 2; | 
|  | 34 | +  private final JavaFormatterOptions formatterOptions; | 
|  | 35 | + | 
|  | 36 | +  JavaFormatterBase(JavaFormatterOptions formatterOptions) { | 
|  | 37 | +    this.formatterOptions = formatterOptions; | 
|  | 38 | +  } | 
|  | 39 | + | 
|  | 40 | +  @Override | 
|  | 41 | +  public TextEdit format( | 
|  | 42 | +      int kind, String source, int offset, int length, int indentationLevel, String lineSeparator) { | 
|  | 43 | +    IRegion[] regions = new IRegion[] {new Region(offset, length)}; | 
|  | 44 | +    return formatInternal(kind, source, regions, indentationLevel); | 
|  | 45 | +  } | 
|  | 46 | + | 
|  | 47 | +  @Override | 
|  | 48 | +  public TextEdit format( | 
|  | 49 | +      int kind, String source, IRegion[] regions, int indentationLevel, String lineSeparator) { | 
|  | 50 | +    return formatInternal(kind, source, regions, indentationLevel); | 
|  | 51 | +  } | 
|  | 52 | + | 
|  | 53 | +  @Override | 
|  | 54 | +  public String createIndentationString(int indentationLevel) { | 
|  | 55 | +    Preconditions.checkArgument( | 
|  | 56 | +        indentationLevel >= 0, | 
|  | 57 | +        "Indentation level cannot be less than zero. Given: %s", | 
|  | 58 | +        indentationLevel); | 
|  | 59 | +    int spaces = indentationLevel * INDENTATION_SIZE; | 
|  | 60 | +    StringBuilder buf = new StringBuilder(spaces); | 
|  | 61 | +    for (int i = 0; i < spaces; i++) { | 
|  | 62 | +      buf.append(' '); | 
|  | 63 | +    } | 
|  | 64 | +    return buf.toString(); | 
|  | 65 | +  } | 
|  | 66 | + | 
|  | 67 | +  /** Runs the Google Java formatter on the given source, with only the given ranges specified. */ | 
|  | 68 | +  private TextEdit formatInternal(int kind, String source, IRegion[] regions, int initialIndent) { | 
|  | 69 | +    try { | 
|  | 70 | +      boolean includeComments = | 
|  | 71 | +          (kind & CodeFormatter.F_INCLUDE_COMMENTS) == CodeFormatter.F_INCLUDE_COMMENTS; | 
|  | 72 | +      kind &= ~CodeFormatter.F_INCLUDE_COMMENTS; | 
|  | 73 | +      SnippetKind snippetKind; | 
|  | 74 | +      switch (kind) { | 
|  | 75 | +        case ASTParser.K_EXPRESSION: | 
|  | 76 | +          snippetKind = SnippetKind.EXPRESSION; | 
|  | 77 | +          break; | 
|  | 78 | +        case ASTParser.K_STATEMENTS: | 
|  | 79 | +          snippetKind = SnippetKind.STATEMENTS; | 
|  | 80 | +          break; | 
|  | 81 | +        case ASTParser.K_CLASS_BODY_DECLARATIONS: | 
|  | 82 | +          snippetKind = SnippetKind.CLASS_BODY_DECLARATIONS; | 
|  | 83 | +          break; | 
|  | 84 | +        case ASTParser.K_COMPILATION_UNIT: | 
|  | 85 | +          snippetKind = SnippetKind.COMPILATION_UNIT; | 
|  | 86 | +          break; | 
|  | 87 | +        default: | 
|  | 88 | +          throw new IllegalArgumentException(String.format("Unknown snippet kind: %d", kind)); | 
|  | 89 | +      } | 
|  | 90 | +      List<Replacement> replacements = | 
|  | 91 | +          new SnippetFormatter(formatterOptions) | 
|  | 92 | +              .format( | 
|  | 93 | +                  snippetKind, source, rangesFromRegions(regions), initialIndent, includeComments); | 
|  | 94 | +      if (idempotent(source, regions, replacements)) { | 
|  | 95 | +        // Do not create edits if there's no diff. | 
|  | 96 | +        return null; | 
|  | 97 | +      } | 
|  | 98 | +      // Convert replacements to text edits. | 
|  | 99 | +      return editFromReplacements(replacements); | 
|  | 100 | +    } catch (IllegalArgumentException | FormatterException exception) { | 
|  | 101 | +      // Do not format on errors. | 
|  | 102 | +      return null; | 
|  | 103 | +    } | 
|  | 104 | +  } | 
|  | 105 | + | 
|  | 106 | +  private List<Range<Integer>> rangesFromRegions(IRegion[] regions) { | 
|  | 107 | +    List<Range<Integer>> ranges = new ArrayList<>(); | 
|  | 108 | +    for (IRegion region : regions) { | 
|  | 109 | +      ranges.add(Range.closedOpen(region.getOffset(), region.getOffset() + region.getLength())); | 
|  | 110 | +    } | 
|  | 111 | +    return ranges; | 
|  | 112 | +  } | 
|  | 113 | + | 
|  | 114 | +  /** | 
|  | 115 | +   * @return {@code true} if input and output texts are equal, else {@code false}. | 
|  | 116 | +   */ | 
|  | 117 | +  private boolean idempotent(String source, IRegion[] regions, List<Replacement> replacements) { | 
|  | 118 | +    // This implementation only checks for single replacement. | 
|  | 119 | +    if (replacements.size() == 1) { | 
|  | 120 | +      Replacement replacement = replacements.get(0); | 
|  | 121 | +      String output = replacement.getReplacementString(); | 
|  | 122 | +      // Entire source case: input = output, nothing changed. | 
|  | 123 | +      if (output.equals(source)) { | 
|  | 124 | +        return true; | 
|  | 125 | +      } | 
|  | 126 | +      // Single region and single replacement case: if they are equal, nothing changed. | 
|  | 127 | +      if (regions.length == 1) { | 
|  | 128 | +        Range<Integer> range = replacement.getReplaceRange(); | 
|  | 129 | +        String snippet = source.substring(range.lowerEndpoint(), range.upperEndpoint()); | 
|  | 130 | +        if (output.equals(snippet)) { | 
|  | 131 | +          return true; | 
|  | 132 | +        } | 
|  | 133 | +      } | 
|  | 134 | +    } | 
|  | 135 | +    return false; | 
|  | 136 | +  } | 
|  | 137 | + | 
|  | 138 | +  private TextEdit editFromReplacements(List<Replacement> replacements) { | 
|  | 139 | +    // Split the replacements that cross line boundaries. | 
|  | 140 | +    TextEdit edit = new MultiTextEdit(); | 
|  | 141 | +    for (Replacement replacement : replacements) { | 
|  | 142 | +      Range<Integer> replaceRange = replacement.getReplaceRange(); | 
|  | 143 | +      edit.addChild( | 
|  | 144 | +          new ReplaceEdit( | 
|  | 145 | +              replaceRange.lowerEndpoint(), | 
|  | 146 | +              replaceRange.upperEndpoint() - replaceRange.lowerEndpoint(), | 
|  | 147 | +              replacement.getReplacementString())); | 
|  | 148 | +    } | 
|  | 149 | +    return edit; | 
|  | 150 | +  } | 
|  | 151 | +} | 
0 commit comments