| /* |
| * Copyright 2017 Google Inc. |
| * |
| * 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.java; |
| |
| import static com.google.common.collect.ImmutableList.toImmutableList; |
| |
| import com.google.common.base.CharMatcher; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.DiscreteDomain; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Range; |
| import com.google.common.collect.RangeSet; |
| import com.google.common.collect.TreeRangeSet; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** Formats a subset of a compilation unit. */ |
| public class SnippetFormatter { |
| |
| /** The kind of snippet to format. */ |
| public enum SnippetKind { |
| COMPILATION_UNIT, |
| CLASS_BODY_DECLARATIONS, |
| STATEMENTS, |
| EXPRESSION |
| } |
| |
| private class SnippetWrapper { |
| int offset; |
| final StringBuilder contents = new StringBuilder(); |
| |
| public SnippetWrapper append(String str) { |
| contents.append(str); |
| return this; |
| } |
| |
| public SnippetWrapper appendSource(String source) { |
| this.offset = contents.length(); |
| contents.append(source); |
| return this; |
| } |
| |
| public void closeBraces(int initialIndent) { |
| for (int i = initialIndent; --i >= 0; ) { |
| contents.append("\n").append(createIndentationString(i)).append("}"); |
| } |
| } |
| } |
| |
| private static final int INDENTATION_SIZE = 2; |
| private final Formatter formatter = new Formatter(); |
| private static final CharMatcher NOT_WHITESPACE = CharMatcher.whitespace().negate(); |
| |
| public String createIndentationString(int indentationLevel) { |
| Preconditions.checkArgument( |
| indentationLevel >= 0, |
| "Indentation level cannot be less than zero. Given: %s", |
| indentationLevel); |
| int spaces = indentationLevel * INDENTATION_SIZE; |
| StringBuilder buf = new StringBuilder(spaces); |
| for (int i = 0; i < spaces; i++) { |
| buf.append(' '); |
| } |
| return buf.toString(); |
| } |
| |
| private static Range<Integer> offsetRange(Range<Integer> range, int offset) { |
| range = range.canonical(DiscreteDomain.integers()); |
| return Range.closedOpen(range.lowerEndpoint() + offset, range.upperEndpoint() + offset); |
| } |
| |
| private static List<Range<Integer>> offsetRanges(List<Range<Integer>> ranges, int offset) { |
| List<Range<Integer>> result = new ArrayList<>(); |
| for (Range<Integer> range : ranges) { |
| result.add(offsetRange(range, offset)); |
| } |
| return result; |
| } |
| |
| /** Runs the Google Java formatter on the given source, with only the given ranges specified. */ |
| public ImmutableList<Replacement> format( |
| SnippetKind kind, |
| String source, |
| List<Range<Integer>> ranges, |
| int initialIndent, |
| boolean includeComments) |
| throws FormatterException { |
| RangeSet<Integer> rangeSet = TreeRangeSet.create(); |
| for (Range<Integer> range : ranges) { |
| rangeSet.add(range); |
| } |
| if (includeComments) { |
| if (kind != SnippetKind.COMPILATION_UNIT) { |
| throw new IllegalArgumentException( |
| "comment formatting is only supported for compilation units"); |
| } |
| return formatter.getFormatReplacements(source, ranges); |
| } |
| SnippetWrapper wrapper = snippetWrapper(kind, source, initialIndent); |
| ranges = offsetRanges(ranges, wrapper.offset); |
| |
| String replacement = formatter.formatSource(wrapper.contents.toString(), ranges); |
| replacement = |
| replacement.substring( |
| wrapper.offset, |
| replacement.length() - (wrapper.contents.length() - wrapper.offset - source.length())); |
| |
| return toReplacements(source, replacement).stream() |
| .filter(r -> rangeSet.encloses(r.getReplaceRange())) |
| .collect(toImmutableList()); |
| } |
| |
| /** |
| * Generates {@code Replacement}s rewriting {@code source} to {@code replacement}, under the |
| * assumption that they differ in whitespace alone. |
| */ |
| private static List<Replacement> toReplacements(String source, String replacement) { |
| if (!NOT_WHITESPACE.retainFrom(source).equals(NOT_WHITESPACE.retainFrom(replacement))) { |
| throw new IllegalArgumentException( |
| "source = \"" + source + "\", replacement = \"" + replacement + "\""); |
| } |
| /* |
| * In the past we seemed to have problems touching non-whitespace text in the formatter, even |
| * just replacing some code with itself. Retrospective attempts to reproduce this have failed, |
| * but this may be an issue for future changes. |
| */ |
| List<Replacement> replacements = new ArrayList<>(); |
| int i = NOT_WHITESPACE.indexIn(source); |
| int j = NOT_WHITESPACE.indexIn(replacement); |
| if (i != 0 || j != 0) { |
| replacements.add(Replacement.create(0, i, replacement.substring(0, j))); |
| } |
| while (i != -1 && j != -1) { |
| int i2 = NOT_WHITESPACE.indexIn(source, i + 1); |
| int j2 = NOT_WHITESPACE.indexIn(replacement, j + 1); |
| if (i2 == -1 || j2 == -1) { |
| break; |
| } |
| if ((i2 - i) != (j2 - j) |
| || !source.substring(i + 1, i2).equals(replacement.substring(j + 1, j2))) { |
| replacements.add(Replacement.create(i + 1, i2, replacement.substring(j + 1, j2))); |
| } |
| i = i2; |
| j = j2; |
| } |
| return replacements; |
| } |
| |
| private SnippetWrapper snippetWrapper(SnippetKind kind, String source, int initialIndent) { |
| /* |
| * Synthesize a dummy class around the code snippet provided by Eclipse. The dummy class is |
| * correctly formatted -- the blocks use correct indentation, etc. |
| */ |
| switch (kind) { |
| case COMPILATION_UNIT: |
| { |
| SnippetWrapper wrapper = new SnippetWrapper(); |
| for (int i = 1; i <= initialIndent; i++) { |
| wrapper.append("class Dummy {\n").append(createIndentationString(i)); |
| } |
| wrapper.appendSource(source); |
| wrapper.closeBraces(initialIndent); |
| return wrapper; |
| } |
| case CLASS_BODY_DECLARATIONS: |
| { |
| SnippetWrapper wrapper = new SnippetWrapper(); |
| for (int i = 1; i <= initialIndent; i++) { |
| wrapper.append("class Dummy {\n").append(createIndentationString(i)); |
| } |
| wrapper.appendSource(source); |
| wrapper.closeBraces(initialIndent); |
| return wrapper; |
| } |
| case STATEMENTS: |
| { |
| SnippetWrapper wrapper = new SnippetWrapper(); |
| wrapper.append("class Dummy {\n").append(createIndentationString(1)); |
| for (int i = 2; i <= initialIndent; i++) { |
| wrapper.append("{\n").append(createIndentationString(i)); |
| } |
| wrapper.appendSource(source); |
| wrapper.closeBraces(initialIndent); |
| return wrapper; |
| } |
| case EXPRESSION: |
| { |
| SnippetWrapper wrapper = new SnippetWrapper(); |
| wrapper.append("class Dummy {\n").append(createIndentationString(1)); |
| for (int i = 2; i <= initialIndent; i++) { |
| wrapper.append("{\n").append(createIndentationString(i)); |
| } |
| wrapper.append("Object o = "); |
| wrapper.appendSource(source); |
| wrapper.append(";"); |
| wrapper.closeBraces(initialIndent); |
| return wrapper; |
| } |
| default: |
| throw new IllegalArgumentException("Unknown snippet kind: " + kind); |
| } |
| } |
| } |