1 /* 2 * Copyright 2017 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 GoogleJavaFormatter extends CodeFormatter { 32 33 private static final int INDENTATION_SIZE = 2; 34 35 @Override format( int kind, String source, int offset, int length, int indentationLevel, String lineSeparator)36 public TextEdit format( 37 int kind, String source, int offset, int length, int indentationLevel, String lineSeparator) { 38 IRegion[] regions = new IRegion[] {new Region(offset, length)}; 39 return formatInternal(kind, source, regions, indentationLevel); 40 } 41 42 @Override format( int kind, String source, IRegion[] regions, int indentationLevel, String lineSeparator)43 public TextEdit format( 44 int kind, String source, IRegion[] regions, int indentationLevel, String lineSeparator) { 45 return formatInternal(kind, source, regions, indentationLevel); 46 } 47 48 @Override createIndentationString(int indentationLevel)49 public String createIndentationString(int indentationLevel) { 50 Preconditions.checkArgument( 51 indentationLevel >= 0, 52 "Indentation level cannot be less than zero. Given: %s", 53 indentationLevel); 54 int spaces = indentationLevel * INDENTATION_SIZE; 55 StringBuilder buf = new StringBuilder(spaces); 56 for (int i = 0; i < spaces; i++) { 57 buf.append(' '); 58 } 59 return buf.toString(); 60 } 61 62 /** Runs the Google Java formatter on the given source, with only the given ranges specified. */ formatInternal(int kind, String source, IRegion[] regions, int initialIndent)63 private TextEdit formatInternal(int kind, String source, IRegion[] regions, int initialIndent) { 64 try { 65 boolean includeComments = 66 (kind & CodeFormatter.F_INCLUDE_COMMENTS) == CodeFormatter.F_INCLUDE_COMMENTS; 67 kind &= ~CodeFormatter.F_INCLUDE_COMMENTS; 68 SnippetKind snippetKind; 69 switch (kind) { 70 case ASTParser.K_EXPRESSION: 71 snippetKind = SnippetKind.EXPRESSION; 72 break; 73 case ASTParser.K_STATEMENTS: 74 snippetKind = SnippetKind.STATEMENTS; 75 break; 76 case ASTParser.K_CLASS_BODY_DECLARATIONS: 77 snippetKind = SnippetKind.CLASS_BODY_DECLARATIONS; 78 break; 79 case ASTParser.K_COMPILATION_UNIT: 80 snippetKind = SnippetKind.COMPILATION_UNIT; 81 break; 82 default: 83 throw new IllegalArgumentException(String.format("Unknown snippet kind: %d", kind)); 84 } 85 List<Replacement> replacements = 86 new SnippetFormatter() 87 .format( 88 snippetKind, source, rangesFromRegions(regions), initialIndent, includeComments); 89 if (idempotent(source, regions, replacements)) { 90 // Do not create edits if there's no diff. 91 return null; 92 } 93 // Convert replacements to text edits. 94 return editFromReplacements(replacements); 95 } catch (IllegalArgumentException | FormatterException exception) { 96 // Do not format on errors. 97 return null; 98 } 99 } 100 rangesFromRegions(IRegion[] regions)101 private List<Range<Integer>> rangesFromRegions(IRegion[] regions) { 102 List<Range<Integer>> ranges = new ArrayList<>(); 103 for (IRegion region : regions) { 104 ranges.add(Range.closedOpen(region.getOffset(), region.getOffset() + region.getLength())); 105 } 106 return ranges; 107 } 108 109 /** @return {@code true} if input and output texts are equal, else {@code false}. */ idempotent(String source, IRegion[] regions, List<Replacement> replacements)110 private boolean idempotent(String source, IRegion[] regions, List<Replacement> replacements) { 111 // This implementation only checks for single replacement. 112 if (replacements.size() == 1) { 113 Replacement replacement = replacements.get(0); 114 String output = replacement.getReplacementString(); 115 // Entire source case: input = output, nothing changed. 116 if (output.equals(source)) { 117 return true; 118 } 119 // Single region and single replacement case: if they are equal, nothing changed. 120 if (regions.length == 1) { 121 Range<Integer> range = replacement.getReplaceRange(); 122 String snippet = source.substring(range.lowerEndpoint(), range.upperEndpoint()); 123 if (output.equals(snippet)) { 124 return true; 125 } 126 } 127 } 128 return false; 129 } 130 editFromReplacements(List<Replacement> replacements)131 private TextEdit editFromReplacements(List<Replacement> replacements) { 132 // Split the replacements that cross line boundaries. 133 TextEdit edit = new MultiTextEdit(); 134 for (Replacement replacement : replacements) { 135 Range<Integer> replaceRange = replacement.getReplaceRange(); 136 edit.addChild( 137 new ReplaceEdit( 138 replaceRange.lowerEndpoint(), 139 replaceRange.upperEndpoint() - replaceRange.lowerEndpoint(), 140 replacement.getReplacementString())); 141 } 142 return edit; 143 } 144 } 145