Merge tag 'google-java-format-1.8' into master

Bug: 158870037
Test: m checkbuild
Change-Id: I9ec115d3ab6e84838bcad3b366512598b728517e
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..235f69d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+.idea/
+*.ims
+*.iml
+
+.classpath
+.project
+.factorypath
+.settings/
+.apt_generated/
+
+target/
+
+bin/
+out/
+eclipse_plugin/lib/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..b8a7c33
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,42 @@
+language: java
+
+notifications:
+  email:
+    recipients:
+      - google-java-format-dev+ci@google.com
+    on_success: change
+    on_failure: always
+
+jdk:
+  - openjdk11
+  - openjdk14
+  - openjdk-ea
+
+matrix:
+  allow_failures:
+    - jdk: openjdk-ea
+
+# see https://github.com/travis-ci/travis-ci/issues/8408
+before_install:
+- unset _JAVA_OPTIONS
+
+install: echo "The default Travis install script is being skipped!"
+
+# use travis-ci docker based infrastructure
+sudo: false
+
+cache:
+  directories:
+    - $HOME/.m2
+
+script:
+- mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V
+- mvn test -B
+
+env:
+  global:
+  - secure: KkUX74NDDk95WR60zwN6x6pz49KAfR0zUu1thxl8Kke0+WVoIv1EBo7/e4ZXTdBKxlzQCX9Aa0OlIyUlhGJeuNIGtX16lcNyZNmKSacfrT68MpZqi+nAiYp8tnOyW/zuI+shSKHkGQOFq6c9KTtR9vG8kjr1Q9dNl/H5QjGaG1ZMiU/mGH9ompf+0nQTMDLKaEWV+SpKGjK5U1Zs2p08I9KKePbZoi9L2oAw5cH9wW8Q3pQJds6Rkwy9aecxRd4xmTza7Lb04dmByjqY8gsIjrTN0onOndBmLKTHiH5NVLKf0ilEVGiMQ1x4eCQolcRpGzxdTTKI0ahiWS59UABVoy1sXYqkIbZjpmMuGhHvbRir7YEXaG8LRUAxdWd9drJfvKQeBphQlIJKwajHSiMAdc9zisQg1UW75HSGKoPDHpzq+P7YBil2PUjk+5yUy5OytX6IebFT4KdeCO2ayu338yqb2t8q98elMoD5jwFVD0tpkLQ6xsYodClSGfMCVfP2zTkB7c4sHZV7tJS68CiNt7sCwz9CTNApFiSWMBxLKkKQ7VSBTy9bAn+phvW0u/maGsrRnehmsV3PVPtEsMlrqeMGwaPqIwx1l6otVQCnGRt3e8z3HoxY6AaBPaX0Z8lH2y+BxYhWTYzGhRxyyV666u/9yekTXmH53c7at7mau6Q=
+  - secure: VWnZcPA4esdaMJgh0Mui7K5O++AGZY3AYswufd0UUbAmnK60O6cDBOSelnr7hImDgZ09L2RWMXIVCt4b+UFXoIhqrvZKVitUXPldS6uNJeGT9p6quFf36o8Wf0ppKWnPd66AY6ECnE75Ujn1Maw899kb3zY2SvIvzA7HlXqtmowHCVGoJ4ou6LQxJpVEJ4hjvS2gQMF9W31uOzRzMI1JhdZioYmqe6eq9sGmRZZiYON7jBqX8f4XW0tTZoK+dVRNwYQcwyqcvQpxeI15VWDq5cqjBw3ps5XSEYTNIFUXREnEEi+vLdCuw/YRZp1ij7LiQKp6bcb2KROXaWii4VpUNWxIAflm4Nvn/8pa/3CUwqIbxTSAL+Qkb2iEzuYuNPGLr72mQgGEnlSpaqzUx0miwjJ41x3Q8mf72ihIME7YQGMDJL7TA7/GjXFeSxroPk65tbssAGmbjwGGJX67NHUzeQPW2QPA2cohCHyopKB9GqhKgKwKjenkCUaBGCaZReZz9XkSkHTXlxxSakMTmgJnA9To9d2lPOy0nppUvrd/0uAbPuxxCZqXElRvOvHKzpV1ZpKpqSxvjh63mCQRTi2rFiPn8uFaajai9mHaPoGmNwQwIUbAviNqifuIEPpc6cOuyV0MWJwdFLo1SKamJya/MwQz+IwXuY2TX7Fmv9HovdM=
+
+after_success:
+- util/publish-snapshot-on-commit.sh
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..0f5b8cf
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,29 @@
+Want to contribute? Great! First, read this page (including the small print at
+the end).
+
+### Before you contribute
+
+Before we can use your code, you must sign the [Google Individual Contributor
+License
+Agreement](https://developers.google.com/open-source/cla/individual?csw=1)
+(CLA), which you can do online. The CLA is necessary mainly because you own the
+copyright to your changes, even after your contribution becomes part of our
+codebase, so we need your permission to use and distribute your code. We also
+need to be sure of various other things—for instance that you'll tell us if you
+know that your code infringes on other people's patents. You don't have to sign
+the CLA until after you've submitted your code for review and a member has
+approved it, but you must do it before we can put your code into our codebase.
+Before you start working on a larger contribution, you should get in touch with
+us first through the issue tracker with your idea so that we can help out and
+possibly guide you. Coordinating up front makes it much easier to avoid
+frustration later on.
+
+### Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose.
+
+### The small print
+
+Contributions made by corporations are covered by a different agreement than the
+one above, the Software Grant and Corporate Contributor License Agreement.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6c4ca5a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,272 @@
+The following Apache 2.0 license applies to all code in this package except
+google-java-format-diff.py.
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
+
+------------------------------------------------------------------------------
+
+The following NCSA license applies only to google-java-format-diff.py.
+
+==============================================================================
+LLVM Release License
+==============================================================================
+University of Illinois/NCSA
+Open Source License
+
+Copyright (c) 2007-2015 University of Illinois at Urbana-Champaign.
+All rights reserved.
+
+Developed by:
+
+    LLVM Team
+
+    University of Illinois at Urbana-Champaign
+
+    http://llvm.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal with
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimers.
+
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimers in the
+      documentation and/or other materials provided with the distribution.
+
+    * Neither the names of the LLVM Team, University of Illinois at
+      Urbana-Champaign, nor the names of its contributors may be used to
+      endorse or promote products derived from this Software without specific
+      prior written permission.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
+SOFTWARE.
+
+==============================================================================
+The LLVM software contains code written by third parties.  Such software will
+have its own individual LICENSE.TXT file in the directory in which it appears.
+This file will describe the copyrights, license, and restrictions which apply
+to that code.
+
+The disclaimer of warranty in the University of Illinois Open Source License
+applies to all code in the LLVM Distribution, and nothing in any of the
+other licenses gives permission to use the names of the LLVM Team or the
+University of Illinois to endorse or promote products derived from this
+Software.
+
+The following pieces of software have additional or alternate copyrights,
+licenses, and/or restrictions:
+
+Program             Directory
+-------             ---------
+<none yet>
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c5fdee2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,152 @@
+# google-java-format
+
+`google-java-format` is a program that reformats Java source code to comply with
+[Google Java Style][].
+
+[Google Java Style]: https://google.github.io/styleguide/javaguide.html
+
+## Using the formatter
+
+### from the command-line
+
+[Download the formatter](https://github.com/google/google-java-format/releases)
+and run it with:
+
+```
+java -jar /path/to/google-java-format-1.7-all-deps.jar <options> [files...]
+```
+
+The formatter can act on whole files, on limited lines (`--lines`), on specific
+offsets (`--offset`), passing through to standard-out (default) or altered
+in-place (`--replace`).
+
+To reformat changed lines in a specific patch, use
+[`google-java-format-diff.py`](https://github.com/google/google-java-format/blob/master/scripts/google-java-format-diff.py).
+
+***Note:*** *There is no configurability as to the formatter's algorithm for
+formatting. This is a deliberate design decision to unify our code formatting on
+a single format.*
+
+### IntelliJ, Android Studio, and other JetBrains IDEs
+
+A
+[google-java-format IntelliJ plugin](https://plugins.jetbrains.com/plugin/8527)
+is available from the plugin repository. To install it, go to your IDE's
+settings and select the `Plugins` category. Click the `Marketplace` tab, search
+for the `google-java-format` plugin, and click the `Install` button.
+
+The plugin will be disabled by default. To enable it in the current project, go
+to `File→Settings...→google-java-format Settings` (or `IntelliJ
+IDEA→Preferences...→Other Settings→google-java-format Settings` on macOS) and
+check the `Enable google-java-format` checkbox. (A notification will be
+presented when you first open a project offering to do this for you.)
+
+To enable it by default in new projects, use `File→Other Settings→Default
+Settings...`.
+
+When enabled, it will replace the normal `Reformat Code` action, which can be
+triggered from the `Code` menu or with the Ctrl-Alt-L (by default) keyboard
+shortcut.
+
+The import ordering is not handled by this plugin, unfortunately. To fix the
+import order, download the
+[IntelliJ Java Google Style file](https://raw.githubusercontent.com/google/styleguide/gh-pages/intellij-java-google-style.xml)
+and import it into File→Settings→Editor→Code Style.
+
+### Eclipse
+
+A
+[google-java-format Eclipse plugin](https://github.com/google/google-java-format/releases/download/google-java-format-1.6/google-java-format-eclipse-plugin_1.6.0.jar)
+can be downloaded from the releases page. Drop it into the Eclipse
+[drop-ins folder](http://help.eclipse.org/neon/index.jsp?topic=%2Forg.eclipse.platform.doc.isv%2Freference%2Fmisc%2Fp2_dropins_format.html)
+to activate the plugin.
+
+The plugin adds a `google-java-format` formatter implementation that can be
+configured in `Window > Preferences > Java > Code Style > Formatter > Formatter
+Implementation`.
+
+### Third-party integrations
+
+*   Gradle plugins
+    *   [Spotless](https://github.com/diffplug/spotless/tree/master/plugin-gradle#applying-to-java-source-google-java-format):
+    *   [sherter/google-java-format-gradle-plugin](https://github.com/sherter/google-java-format-gradle-plugin)
+*   Apache Maven plugins
+    *   [coveo/fmt-maven-plugin](https://github.com/coveo/fmt-maven-plugin)
+    *   [talios/googleformatter-maven-plugin](https://github.com/talios/googleformatter-maven-plugin)
+    *   [Cosium/maven-git-code-format](https://github.com/Cosium/maven-git-code-format):
+        A maven plugin that automatically deploys google-java-format as a
+        pre-commit git hook.
+*   SBT plugins
+    *   [sbt/sbt-java-formatter](https://github.com/sbt/sbt-java-formatter)
+*   [maltzj/google-style-precommit-hook](https://github.com/maltzj/google-style-precommit-hook):
+    A pre-commit (pre-commit.com) hook that will automatically run GJF whenever
+    you commit code to your repository
+
+### as a library
+
+The formatter can be used in software which generates java to output more
+legible java code. Just include the library in your maven/gradle/etc.
+configuration.
+
+#### Maven
+
+```xml
+<dependency>
+  <groupId>com.google.googlejavaformat</groupId>
+  <artifactId>google-java-format</artifactId>
+  <version>1.7</version>
+</dependency>
+```
+
+#### Gradle
+
+```groovy
+dependencies {
+  compile 'com.google.googlejavaformat:google-java-format:1.7'
+}
+```
+
+You can then use the formatter through the `formatSource` methods. E.g.
+
+```java
+String formattedSource = new Formatter().formatSource(sourceString);
+```
+
+or
+
+```java
+CharSource source = ...
+CharSink output = ...
+new Formatter().formatSource(source, output);
+```
+
+Your starting point should be the instance methods of
+`com.google.googlejavaformat.java.Formatter`.
+
+## Building from source
+
+```
+mvn install
+```
+
+## Contributing
+
+Please see [the contributors guide](CONTRIBUTING.md) for details.
+
+## License
+
+```text
+Copyright 2015 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.
+```
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..3522155
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,33 @@
+os: Visual Studio 2015
+install:
+  - ps: |
+      Add-Type -AssemblyName System.IO.Compression.FileSystem
+      if (!(Test-Path -Path "C:\maven" )) {
+        (new-object System.Net.WebClient).DownloadFile(
+          'http://www.us.apache.org/dist/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.zip',
+          'C:\maven-bin.zip'
+        )
+        [System.IO.Compression.ZipFile]::ExtractToDirectory("C:\maven-bin.zip", "C:\maven")
+      }
+  - cmd: SET JAVA_HOME=C:\Program Files\Java\jdk11
+  - cmd: SET PATH=C:\maven\apache-maven-3.3.9\bin;%JAVA_HOME%\bin;%PATH%
+  - cmd: SET MAVEN_OPTS=-XX:MaxPermSize=2g -Xmx4g
+  - cmd: SET JAVA_OPTS=-XX:MaxPermSize=2g -Xmx4g
+
+build_script:
+  - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V
+
+test_script:
+  - mvn test -B
+
+cache:
+  - C:\maven\
+  - C:\Users\appveyor\.m2
+
+notifications:
+  - provider: Email
+    to:
+      - google-java-format-dev+ci@google.com
+    on_build_success: false
+    on_build_failure: true
+    on_build_status_changed: true
diff --git a/core/pom.xml b/core/pom.xml
new file mode 100644
index 0000000..88b3cf9
--- /dev/null
+++ b/core/pom.xml
@@ -0,0 +1,284 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>com.google.googlejavaformat</groupId>
+    <artifactId>google-java-format-parent</artifactId>
+    <version>1.8-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>google-java-format</artifactId>
+
+  <name>Google Java Format</name>
+
+  <description>
+    A Java source code formatter that follows Google Java Style.
+  </description>
+
+  <dependencies>
+    <!-- Required runtime dependencies -->
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+
+    <!-- Compile-time dependencies -->
+    <dependency>
+      <groupId>org.checkerframework</groupId>
+      <artifactId>checker-qual</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>com.google.errorprone</groupId>
+      <artifactId>error_prone_annotations</artifactId>
+      <optional>true</optional>
+    </dependency>
+
+
+    <!-- Test dependencies -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava-testlib</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.truth</groupId>
+      <artifactId>truth</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.testing.compile</groupId>
+      <artifactId>compile-testing</artifactId>
+      <version>0.15</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-javadoc-plugin</artifactId>
+        <configuration>
+          <source>11</source>
+          <encoding>UTF-8</encoding>
+          <docencoding>UTF-8</docencoding>
+          <charset>UTF-8</charset>
+          <links>
+            <link>https://guava.dev/releases/${guava.version}/api/docs/</link>
+            <link>https://docs.oracle.com/en/java/javase/11/docs/api</link>
+          </links>
+          <additionalJOptions>
+            <additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED,com.google.googlejavaformat</additionalJOption>
+            <additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED,com.google.googlejavaformat</additionalJOption>
+            <additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED,com.google.googlejavaformat</additionalJOption>
+            <additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED,com.google.googlejavaformat</additionalJOption>
+            <additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED,com.google.googlejavaformat</additionalJOption>
+            <additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED,com.google.googlejavaformat</additionalJOption>
+            <additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED,com.google.googlejavaformat</additionalJOption>
+          </additionalJOptions>
+        </configuration>
+        <executions>
+          <execution>
+            <id>attach-docs</id>
+            <phase>post-integration-test</phase>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <version>2.4.3</version>
+        <executions>
+          <execution>
+            <id>shade-all-deps</id>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <shadedArtifactAttached>true</shadedArtifactAttached>
+              <shadedClassifierName>all-deps</shadedClassifierName>
+              <createDependencyReducedPom>false</createDependencyReducedPom>
+              <!-- http://stackoverflow.com/a/6743609 -->
+              <filters>
+                <filter>
+                  <artifact>*:*</artifact>
+                  <excludes>
+                    <exclude>META-INF/*.SF</exclude>
+                    <exclude>META-INF/*.DSA</exclude>
+                    <exclude>META-INF/*.RSA</exclude>
+                  </excludes>
+                </filter>
+              </filters>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <configuration>
+          <archive>
+            <manifest>
+              <mainClass>com.google.googlejavaformat.java.Main</mainClass>
+              <addDefaultImplementationEntries>
+                true
+              </addDefaultImplementationEntries>
+            </manifest>
+            <manifestEntries>
+              <Automatic-Module-Name>
+                com.google.googlejavaformat
+              </Automatic-Module-Name>
+            </manifestEntries>
+          </archive>
+        </configuration>
+      </plugin>
+      <plugin>
+        <artifactId>maven-resources-plugin</artifactId>
+        <version>3.0.1</version>
+        <executions>
+          <execution>
+            <id>copy-resources</id>
+            <phase>package</phase>
+            <goals>
+              <goal>copy-resources</goal>
+            </goals>
+            <configuration>
+              <outputDirectory>../eclipse_plugin/lib</outputDirectory>
+              <resources>
+                <resource>
+                  <directory>target</directory>
+                  <include>${project.artifactId}-${project.version}.jar</include>
+                </resource>
+              </resources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <version>2.10</version>
+        <executions>
+          <execution>
+            <id>copy-dependencies</id>
+            <phase>package</phase>
+            <goals>
+              <goal>copy-dependencies</goal>
+            </goals>
+            <configuration>
+              <outputDirectory>../eclipse_plugin/lib</outputDirectory>
+              <overWriteReleases>true</overWriteReleases>
+              <overWriteSnapshots>true</overWriteSnapshots>
+              <excludeTransitive>true</excludeTransitive>
+              <excludeArtifactIds>org.eclipse.jdt.core</excludeArtifactIds>
+              <excludeScope>compile</excludeScope>
+              <excludeScope>provided</excludeScope>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>com.google.code.maven-replacer-plugin</groupId>
+        <artifactId>replacer</artifactId>
+        <version>1.5.3</version>
+        <executions>
+          <execution>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>replace</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <file>${project.basedir}/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template</file>
+          <outputFile>${project.build.directory}/generated-sources/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java</outputFile>
+          <replacements>
+            <replacement>
+              <token>%VERSION%</token>
+              <value>${project.version}</value>
+            </replacement>
+          </replacements>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <version>3.0.0</version>
+        <executions>
+          <execution>
+            <id>add-source</id>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>add-source</goal>
+            </goals>
+            <configuration>
+              <sources>
+                <source>${project.build.directory}/generated-sources/java/</source>
+              </sources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <source>11</source>
+          <target>11</target>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>jdk11</id>
+      <activation>
+        <jdk>(,14)</jdk>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <configuration>
+              <excludes>
+                <exclude>**/Java14InputAstVisitor.java</exclude>
+              </excludes>
+            </configuration>
+          </plugin>
+          <plugin>
+            <artifactId>maven-javadoc-plugin</artifactId>
+            <configuration>
+              <excludePackageNames>com.google.googlejavaformat.java.java14</excludePackageNames>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+</project>
diff --git a/core/src/main/java/com/google/googlejavaformat/CloseOp.java b/core/src/main/java/com/google/googlejavaformat/CloseOp.java
new file mode 100644
index 0000000..59e7e5a
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/CloseOp.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015 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;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * A {@code CloseOp} closes a level. It is an {@link Op} in the sequence of {@link Op}s generated by
+ * {@link OpsBuilder}. When the sequence is turned into a {@link Doc} by {@link DocBuilder}, ranges
+ * delimited by {@link OpenOp}-{@code CloseOp} pairs turn into nested {@link Doc.Level}s.
+ */
+public enum CloseOp implements Op {
+  CLOSE;
+
+  /**
+   * Make a {@code CloseOp}, returning a singleton since they are all the same.
+   *
+   * @return the singleton {@code CloseOp}
+   */
+  public static Op make() {
+    return CLOSE;
+  }
+
+  @Override
+  public void add(DocBuilder builder) {
+    builder.close();
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this).toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/CommentsHelper.java b/core/src/main/java/com/google/googlejavaformat/CommentsHelper.java
new file mode 100644
index 0000000..45e507b
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/CommentsHelper.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2015 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;
+
+/**
+ * Rewrite comments. This interface is implemented by {@link
+ * com.google.googlejavaformat.java.JavaCommentsHelper JavaCommentsHelper}.
+ */
+public interface CommentsHelper {
+  /**
+   * Try to rewrite comments, returning rewritten text.
+   *
+   * @param tok the comment's tok
+   * @param maxWidth the line length for the output
+   * @param column0 the current column
+   * @return the rewritten comment
+   */
+  String rewrite(Input.Tok tok, int maxWidth, int column0);
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/Doc.java b/core/src/main/java/com/google/googlejavaformat/Doc.java
new file mode 100644
index 0000000..e663c96
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/Doc.java
@@ -0,0 +1,770 @@
+/*
+ * Copyright 2015 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;
+
+import static com.google.common.collect.Iterables.getLast;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Range;
+import com.google.googlejavaformat.Output.BreakTag;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * {@link com.google.googlejavaformat.java.JavaInputAstVisitor JavaInputAstVisitor} outputs a
+ * sequence of {@link Op}s using {@link OpsBuilder}. This linear sequence is then transformed by
+ * {@link DocBuilder} into a tree-structured {@code Doc}. The top-level {@code Doc} is a {@link
+ * Level}, which contains a sequence of {@code Doc}s, including other {@link Level}s. Leaf {@code
+ * Doc}s are {@link Token}s, representing language-level tokens; {@link Tok}s, which may also
+ * represent non-token {@link Input.Tok}s, including comments and other white-space; {@link Space}s,
+ * representing single spaces; and {@link Break}s, which represent optional line-breaks.
+ */
+public abstract class Doc {
+  /**
+   * Each {@link Break} in a {@link Level} is either {@link FillMode#UNIFIED} or {@link
+   * FillMode#INDEPENDENT}.
+   */
+  public enum FillMode {
+    /**
+     * If a {@link Level} will not fit on one line, all of its {@code UNIFIED} {@link Break}s will
+     * be broken.
+     */
+    UNIFIED,
+
+    /**
+     * If a {@link Level} will not fit on one line, its {@code INDEPENDENT} {@link Break}s will be
+     * broken independently of each other, to fill in the {@link Level}.
+     */
+    INDEPENDENT,
+
+    /**
+     * A {@code FORCED} {@link Break} will always be broken, and a {@link Level} it appears in will
+     * not fit on one line.
+     */
+    FORCED
+  }
+
+  /** State for writing. */
+  public static final class State {
+    final int lastIndent;
+    final int indent;
+    final int column;
+    final boolean mustBreak;
+
+    State(int lastIndent, int indent, int column, boolean mustBreak) {
+      this.lastIndent = lastIndent;
+      this.indent = indent;
+      this.column = column;
+      this.mustBreak = mustBreak;
+    }
+
+    public State(int indent0, int column0) {
+      this(indent0, indent0, column0, false);
+    }
+
+    State withColumn(int column) {
+      return new State(lastIndent, indent, column, mustBreak);
+    }
+
+    State withMustBreak(boolean mustBreak) {
+      return new State(lastIndent, indent, column, mustBreak);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("lastIndent", lastIndent)
+          .add("indent", indent)
+          .add("column", column)
+          .add("mustBreak", mustBreak)
+          .toString();
+    }
+  }
+
+  private static final Range<Integer> EMPTY_RANGE = Range.closedOpen(-1, -1);
+  private static final DiscreteDomain<Integer> INTEGERS = DiscreteDomain.integers();
+
+  // Memoized width; Float.POSITIVE_INFINITY if contains forced breaks.
+  private boolean widthComputed = false;
+  private float width = 0.0F;
+
+  // Memoized flat; not defined (and never computed) if contains forced breaks.
+  private boolean flatComputed = false;
+  private String flat = "";
+
+  // Memoized Range.
+  private boolean rangeComputed = false;
+  private Range<Integer> range = EMPTY_RANGE;
+
+  /**
+   * Return the width of a {@code Doc}, or {@code Float.POSITIVE_INFINITY} if it must be broken.
+   *
+   * @return the width
+   */
+  final float getWidth() {
+    if (!widthComputed) {
+      width = computeWidth();
+      widthComputed = true;
+    }
+    return width;
+  }
+
+  /**
+   * Return a {@code Doc}'s flat-string value; not defined (and never called) if the (@code Doc}
+   * contains forced breaks.
+   *
+   * @return the flat-string value
+   */
+  final String getFlat() {
+    if (!flatComputed) {
+      flat = computeFlat();
+      flatComputed = true;
+    }
+    return flat;
+  }
+
+  /**
+   * Return the {@link Range} of a {@code Doc}.
+   *
+   * @return the {@code Doc}'s {@link Range}
+   */
+  final Range<Integer> range() {
+    if (!rangeComputed) {
+      range = computeRange();
+      rangeComputed = true;
+    }
+    return range;
+  }
+
+  /**
+   * Compute the {@code Doc}'s width.
+   *
+   * @return the width, or {@code Float.POSITIVE_INFINITY} if it must be broken
+   */
+  abstract float computeWidth();
+
+  /**
+   * Compute the {@code Doc}'s flat value. Not defined (and never called) if contains forced breaks.
+   *
+   * @return the flat value
+   */
+  abstract String computeFlat();
+
+  /**
+   * Compute the {@code Doc}'s {@link Range} of {@link Input.Token}s.
+   *
+   * @return the {@link Range}
+   */
+  abstract Range<Integer> computeRange();
+
+  /**
+   * Make breaking decisions for a {@code Doc}.
+   *
+   * @param maxWidth the maximum line width
+   * @param state the current output state
+   * @return the new output state
+   */
+  public abstract State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state);
+
+  /** Write a {@code Doc} to an {@link Output}, after breaking decisions have been made. */
+  public abstract void write(Output output);
+
+  /** A {@code Level} inside a {@link Doc}. */
+  static final class Level extends Doc {
+    private final Indent plusIndent; // The extra indent following breaks.
+    private final List<Doc> docs = new ArrayList<>(); // The elements of the level.
+
+    private Level(Indent plusIndent) {
+      this.plusIndent = plusIndent;
+    }
+
+    /**
+     * Factory method for {@code Level}s.
+     *
+     * @param plusIndent the extra indent inside the {@code Level}
+     * @return the new {@code Level}
+     */
+    static Level make(Indent plusIndent) {
+      return new Level(plusIndent);
+    }
+
+    /**
+     * Add a {@link Doc} to the {@code Level}.
+     *
+     * @param doc the {@link Doc} to add
+     */
+    void add(Doc doc) {
+      docs.add(doc);
+    }
+
+    @Override
+    float computeWidth() {
+      float thisWidth = 0.0F;
+      for (Doc doc : docs) {
+        thisWidth += doc.getWidth();
+      }
+      return thisWidth;
+    }
+
+    @Override
+    String computeFlat() {
+      StringBuilder builder = new StringBuilder();
+      for (Doc doc : docs) {
+        builder.append(doc.getFlat());
+      }
+      return builder.toString();
+    }
+
+    @Override
+    Range<Integer> computeRange() {
+      Range<Integer> docRange = EMPTY_RANGE;
+      for (Doc doc : docs) {
+        docRange = union(docRange, doc.range());
+      }
+      return docRange;
+    }
+
+    // State that needs to be preserved between calculating breaks and
+    // writing output.
+    // TODO(cushon): represent phases as separate immutable data.
+
+    /** True if the entire {@link Level} fits on one line. */
+    boolean oneLine = false;
+
+    /**
+     * Groups of {@link Doc}s that are children of the current {@link Level}, separated by {@link
+     * Break}s.
+     */
+    List<List<Doc>> splits = new ArrayList<>();
+
+    /** {@link Break}s between {@link Doc}s in the current {@link Level}. */
+    List<Break> breaks = new ArrayList<>();
+
+    @Override
+    public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
+      float thisWidth = getWidth();
+      if (state.column + thisWidth <= maxWidth) {
+        oneLine = true;
+        return state.withColumn(state.column + (int) thisWidth);
+      }
+      State broken =
+          computeBroken(
+              commentsHelper, maxWidth, new State(state.indent + plusIndent.eval(), state.column));
+      return state.withColumn(broken.column);
+    }
+
+    private static void splitByBreaks(List<Doc> docs, List<List<Doc>> splits, List<Break> breaks) {
+      splits.clear();
+      breaks.clear();
+      splits.add(new ArrayList<>());
+      for (Doc doc : docs) {
+        if (doc instanceof Break) {
+          breaks.add((Break) doc);
+          splits.add(new ArrayList<>());
+        } else {
+          getLast(splits).add(doc);
+        }
+      }
+    }
+
+    /** Compute breaks for a {@link Level} that spans multiple lines. */
+    private State computeBroken(CommentsHelper commentsHelper, int maxWidth, State state) {
+      splitByBreaks(docs, splits, breaks);
+
+      state =
+          computeBreakAndSplit(
+              commentsHelper, maxWidth, state, /* optBreakDoc= */ Optional.empty(), splits.get(0));
+
+      // Handle following breaks and split.
+      for (int i = 0; i < breaks.size(); i++) {
+        state =
+            computeBreakAndSplit(
+                commentsHelper, maxWidth, state, Optional.of(breaks.get(i)), splits.get(i + 1));
+      }
+      return state;
+    }
+
+    /** Lay out a Break-separated group of Docs in the current Level. */
+    private static State computeBreakAndSplit(
+        CommentsHelper commentsHelper,
+        int maxWidth,
+        State state,
+        Optional<Break> optBreakDoc,
+        List<Doc> split) {
+      float breakWidth = optBreakDoc.isPresent() ? optBreakDoc.get().getWidth() : 0.0F;
+      float splitWidth = getWidth(split);
+      boolean shouldBreak =
+          (optBreakDoc.isPresent() && optBreakDoc.get().fillMode == FillMode.UNIFIED)
+              || state.mustBreak
+              || state.column + breakWidth + splitWidth > maxWidth;
+
+      if (optBreakDoc.isPresent()) {
+        state = optBreakDoc.get().computeBreaks(state, state.lastIndent, shouldBreak);
+      }
+      boolean enoughRoom = state.column + splitWidth <= maxWidth;
+      state = computeSplit(commentsHelper, maxWidth, split, state.withMustBreak(false));
+      if (!enoughRoom) {
+        state = state.withMustBreak(true); // Break after, too.
+      }
+      return state;
+    }
+
+    private static State computeSplit(
+        CommentsHelper commentsHelper, int maxWidth, List<Doc> docs, State state) {
+      for (Doc doc : docs) {
+        state = doc.computeBreaks(commentsHelper, maxWidth, state);
+      }
+      return state;
+    }
+
+    @Override
+    public void write(Output output) {
+      if (oneLine) {
+        output.append(getFlat(), range()); // This is defined because width is finite.
+      } else {
+        writeFilled(output);
+      }
+    }
+
+    private void writeFilled(Output output) {
+      // Handle first split.
+      for (Doc doc : splits.get(0)) {
+        doc.write(output);
+      }
+      // Handle following breaks and split.
+      for (int i = 0; i < breaks.size(); i++) {
+        breaks.get(i).write(output);
+        for (Doc doc : splits.get(i + 1)) {
+          doc.write(output);
+        }
+      }
+    }
+
+    /**
+     * Get the width of a sequence of {@link Doc}s.
+     *
+     * @param docs the {@link Doc}s
+     * @return the width, or {@code Float.POSITIVE_INFINITY} if any {@link Doc} must be broken
+     */
+    static float getWidth(List<Doc> docs) {
+      float width = 0.0F;
+      for (Doc doc : docs) {
+        width += doc.getWidth();
+      }
+      return width;
+    }
+
+    private static Range<Integer> union(Range<Integer> x, Range<Integer> y) {
+      return x.isEmpty() ? y : y.isEmpty() ? x : x.span(y).canonical(INTEGERS);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("plusIndent", plusIndent)
+          .add("docs", docs)
+          .toString();
+    }
+  }
+
+  /** A leaf {@link Doc} for a token. */
+  public static final class Token extends Doc implements Op {
+    /** Is a Token a real token, or imaginary (e.g., a token generated incorrectly, or an EOF)? */
+    public enum RealOrImaginary {
+      REAL,
+      IMAGINARY;
+
+      boolean isReal() {
+        return this == REAL;
+      }
+    }
+
+    private final Input.Token token;
+    private final RealOrImaginary realOrImaginary;
+    private final Indent plusIndentCommentsBefore;
+    private final Optional<Indent> breakAndIndentTrailingComment;
+
+    private Token(
+        Input.Token token,
+        RealOrImaginary realOrImaginary,
+        Indent plusIndentCommentsBefore,
+        Optional<Indent> breakAndIndentTrailingComment) {
+      this.token = token;
+      this.realOrImaginary = realOrImaginary;
+      this.plusIndentCommentsBefore = plusIndentCommentsBefore;
+      this.breakAndIndentTrailingComment = breakAndIndentTrailingComment;
+    }
+
+    /**
+     * How much extra to indent comments before the {@code Token}.
+     *
+     * @return the extra indent
+     */
+    Indent getPlusIndentCommentsBefore() {
+      return plusIndentCommentsBefore;
+    }
+
+    /** Force a line break and indent trailing javadoc or block comments. */
+    Optional<Indent> breakAndIndentTrailingComment() {
+      return breakAndIndentTrailingComment;
+    }
+
+    /**
+     * Make a {@code Token}.
+     *
+     * @param token the {@link Input.Token} to wrap
+     * @param realOrImaginary did this {@link Input.Token} appear in the input, or was it generated
+     *     incorrectly?
+     * @param plusIndentCommentsBefore extra {@code plusIndent} for comments just before this token
+     * @return the new {@code Token}
+     */
+    static Op make(
+        Input.Token token,
+        Doc.Token.RealOrImaginary realOrImaginary,
+        Indent plusIndentCommentsBefore,
+        Optional<Indent> breakAndIndentTrailingComment) {
+      return new Token(
+          token, realOrImaginary, plusIndentCommentsBefore, breakAndIndentTrailingComment);
+    }
+
+    /**
+     * Return the wrapped {@link Input.Token}.
+     *
+     * @return the {@link Input.Token}
+     */
+    Input.Token getToken() {
+      return token;
+    }
+
+    /**
+     * Is the token good? That is, does it match an {@link Input.Token}?
+     *
+     * @return whether the @code Token} is good
+     */
+    RealOrImaginary realOrImaginary() {
+      return realOrImaginary;
+    }
+
+    @Override
+    public void add(DocBuilder builder) {
+      builder.add(this);
+    }
+
+    @Override
+    float computeWidth() {
+      return token.getTok().length();
+    }
+
+    @Override
+    String computeFlat() {
+      return token.getTok().getOriginalText();
+    }
+
+    @Override
+    Range<Integer> computeRange() {
+      return Range.singleton(token.getTok().getIndex()).canonical(INTEGERS);
+    }
+
+    @Override
+    public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
+      String text = token.getTok().getOriginalText();
+      return state.withColumn(state.column + text.length());
+    }
+
+    @Override
+    public void write(Output output) {
+      String text = token.getTok().getOriginalText();
+      output.append(text, range());
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("token", token)
+          .add("realOrImaginary", realOrImaginary)
+          .add("plusIndentCommentsBefore", plusIndentCommentsBefore)
+          .toString();
+    }
+  }
+
+  /** A Leaf node in a {@link Doc} for a non-breaking space. */
+  static final class Space extends Doc implements Op {
+    private static final Space SPACE = new Space();
+
+    private Space() {}
+
+    /**
+     * Factor method for {@code Space}.
+     *
+     * @return the new {@code Space}
+     */
+    static Space make() {
+      return SPACE;
+    }
+
+    @Override
+    public void add(DocBuilder builder) {
+      builder.add(this);
+    }
+
+    @Override
+    float computeWidth() {
+      return 1.0F;
+    }
+
+    @Override
+    String computeFlat() {
+      return " ";
+    }
+
+    @Override
+    Range<Integer> computeRange() {
+      return EMPTY_RANGE;
+    }
+
+    @Override
+    public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
+      return state.withColumn(state.column + 1);
+    }
+
+    @Override
+    public void write(Output output) {
+      output.append(" ", range());
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this).toString();
+    }
+  }
+
+  /** A leaf node in a {@link Doc} for an optional break. */
+  public static final class Break extends Doc implements Op {
+    private final FillMode fillMode;
+    private final String flat;
+    private final Indent plusIndent;
+    private final Optional<BreakTag> optTag;
+
+    private Break(FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optTag) {
+      this.fillMode = fillMode;
+      this.flat = flat;
+      this.plusIndent = plusIndent;
+      this.optTag = optTag;
+    }
+
+    /**
+     * Make a {@code Break}.
+     *
+     * @param fillMode the {@link FillMode}
+     * @param flat the text when not broken
+     * @param plusIndent extra indent if taken
+     * @return the new {@code Break}
+     */
+    public static Break make(FillMode fillMode, String flat, Indent plusIndent) {
+      return new Break(fillMode, flat, plusIndent, /* optTag= */ Optional.empty());
+    }
+
+    /**
+     * Make a {@code Break}.
+     *
+     * @param fillMode the {@link FillMode}
+     * @param flat the text when not broken
+     * @param plusIndent extra indent if taken
+     * @param optTag an optional tag for remembering whether the break was taken
+     * @return the new {@code Break}
+     */
+    public static Break make(
+        FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optTag) {
+      return new Break(fillMode, flat, plusIndent, optTag);
+    }
+
+    /**
+     * Make a forced {@code Break}.
+     *
+     * @return the new forced {@code Break}
+     */
+    public static Break makeForced() {
+      return make(FillMode.FORCED, "", Indent.Const.ZERO);
+    }
+
+    /**
+     * Return the {@code Break}'s extra indent.
+     *
+     * @return the extra indent
+     */
+    int getPlusIndent() {
+      return plusIndent.eval();
+    }
+
+    /**
+     * Is the {@code Break} forced?
+     *
+     * @return whether the {@code Break} is forced
+     */
+    boolean isForced() {
+      return fillMode == FillMode.FORCED;
+    }
+
+    @Override
+    public void add(DocBuilder builder) {
+      builder.breakDoc(this);
+    }
+
+    @Override
+    float computeWidth() {
+      return isForced() ? Float.POSITIVE_INFINITY : (float) flat.length();
+    }
+
+    @Override
+    String computeFlat() {
+      return flat;
+    }
+
+    @Override
+    Range<Integer> computeRange() {
+      return EMPTY_RANGE;
+    }
+
+    /** Was this break taken? */
+    boolean broken;
+
+    /** New indent after this break. */
+    int newIndent;
+
+    public State computeBreaks(State state, int lastIndent, boolean broken) {
+      if (optTag.isPresent()) {
+        optTag.get().recordBroken(broken);
+      }
+
+      if (broken) {
+        this.broken = true;
+        this.newIndent = Math.max(lastIndent + plusIndent.eval(), 0);
+        return state.withColumn(newIndent);
+      } else {
+        this.broken = false;
+        this.newIndent = -1;
+        return state.withColumn(state.column + flat.length());
+      }
+    }
+
+    @Override
+    public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
+      // Updating the state for {@link Break}s requires deciding if the break
+      // should be taken.
+      // TODO(cushon): this hierarchy is wrong, create a separate interface
+      // for unbreakable Docs?
+      throw new UnsupportedOperationException("Did you mean computeBreaks(State, int, boolean)?");
+    }
+
+    @Override
+    public void write(Output output) {
+      if (broken) {
+        output.append("\n", EMPTY_RANGE);
+        output.indent(newIndent);
+      } else {
+        output.append(flat, range());
+      }
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("fillMode", fillMode)
+          .add("flat", flat)
+          .add("plusIndent", plusIndent)
+          .add("optTag", optTag)
+          .toString();
+    }
+  }
+
+  /** A leaf node in a {@link Doc} for a non-token. */
+  static final class Tok extends Doc implements Op {
+    private final Input.Tok tok;
+
+    private Tok(Input.Tok tok) {
+      this.tok = tok;
+    }
+
+    /**
+     * Factory method for a {@code Tok}.
+     *
+     * @param tok the {@link Input.Tok} to wrap
+     * @return the new {@code Tok}
+     */
+    static Tok make(Input.Tok tok) {
+      return new Tok(tok);
+    }
+
+    @Override
+    public void add(DocBuilder builder) {
+      builder.add(this);
+    }
+
+    @Override
+    float computeWidth() {
+      int idx = Newlines.firstBreak(tok.getOriginalText());
+      // only count the first line of multi-line block comments
+      if (tok.isComment()) {
+        if (idx > 0) {
+          return idx;
+        } else if (tok.isSlashSlashComment() && !tok.getOriginalText().startsWith("// ")) {
+          // Account for line comments with missing spaces, see computeFlat.
+          return tok.length() + 1;
+        } else {
+          return tok.length();
+        }
+      }
+      return idx != -1 ? Float.POSITIVE_INFINITY : (float) tok.length();
+    }
+
+    @Override
+    String computeFlat() {
+      // TODO(cushon): commentsHelper.rewrite doesn't get called for spans that fit in a single
+      // line. That's fine for multi-line comment reflowing, but problematic for adding missing
+      // spaces in line comments.
+      if (tok.isSlashSlashComment() && !tok.getOriginalText().startsWith("// ")) {
+        return "// " + tok.getOriginalText().substring("//".length());
+      }
+      return tok.getOriginalText();
+    }
+
+    @Override
+    Range<Integer> computeRange() {
+      return Range.singleton(tok.getIndex()).canonical(INTEGERS);
+    }
+
+    String text;
+
+    @Override
+    public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
+      text = commentsHelper.rewrite(tok, maxWidth, state.column);
+      int firstLineLength = text.length() - Iterators.getLast(Newlines.lineOffsetIterator(text));
+      return state.withColumn(state.column + firstLineLength);
+    }
+
+    @Override
+    public void write(Output output) {
+      output.append(text, range());
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this).add("tok", tok).toString();
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/DocBuilder.java b/core/src/main/java/com/google/googlejavaformat/DocBuilder.java
new file mode 100644
index 0000000..31cc798
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/DocBuilder.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2015 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;
+
+import com.google.common.base.MoreObjects;
+import java.util.ArrayDeque;
+import java.util.List;
+
+/** A {@code DocBuilder} converts a sequence of {@link Op}s into a {@link Doc}. */
+public final class DocBuilder {
+  private final Doc.Level base = Doc.Level.make(Indent.Const.ZERO);
+  private final ArrayDeque<Doc.Level> stack = new ArrayDeque<>();
+
+  /**
+   * A possibly earlier {@link Doc.Level} for appending text, à la Philip Wadler.
+   *
+   * <p>Processing {@link Doc}s presents a subtle problem. Suppose we have a {@link Doc} for to an
+   * assignment node, {@code a = b}, with an optional {@link Doc.Break} following the {@code =}.
+   * Suppose we have 5 characters to write it, so that we think we don't need the break.
+   * Unfortunately, this {@link Doc} lies in an expression statement {@link Doc} for the statement
+   * {@code a = b;} and this statement does not fit in 3 characters. This is why many formatters
+   * sometimes emit lines that are too long, or cheat by using a narrower line length to avoid such
+   * problems.
+   *
+   * <p>One solution to this problem is not to decide whether a {@link Doc.Level} should be broken
+   * until later (in this case, after the semicolon has been seen). A simpler approach is to rewrite
+   * the {@link Doc} as here, so that the semicolon moves inside the inner {@link Doc}, and we can
+   * decide whether to break that {@link Doc} without seeing later text.
+   */
+  private Doc.Level appendLevel = base;
+
+  /** Start to build a {@code DocBuilder}. */
+  public DocBuilder() {
+    stack.addLast(base);
+  }
+
+  /**
+   * Add a list of {@link Op}s to the {@link OpsBuilder}.
+   *
+   * @param ops the {@link Op}s
+   * @return the {@link OpsBuilder}
+   */
+  public DocBuilder withOps(List<Op> ops) {
+    for (Op op : ops) {
+      op.add(this); // These operations call the operations below to build the doc.
+    }
+    return this;
+  }
+
+  /**
+   * Open a new {@link Doc.Level}.
+   *
+   * @param plusIndent the extra indent for the {@link Doc.Level}
+   */
+  void open(Indent plusIndent) {
+    Doc.Level level = Doc.Level.make(plusIndent);
+    stack.addLast(level);
+  }
+
+  /** Close the current {@link Doc.Level}. */
+  void close() {
+    Doc.Level top = stack.removeLast();
+    stack.peekLast().add(top);
+  }
+
+  /**
+   * Add a {@link Doc} to the current {@link Doc.Level}.
+   *
+   * @param doc the {@link Doc}
+   */
+  void add(Doc doc) {
+    appendLevel.add(doc);
+  }
+
+  /**
+   * Add a {@link Doc.Break} to the current {@link Doc.Level}.
+   *
+   * @param breakDoc the {@link Doc.Break}
+   */
+  void breakDoc(Doc.Break breakDoc) {
+    appendLevel = stack.peekLast();
+    appendLevel.add(breakDoc);
+  }
+
+  /**
+   * Return the {@link Doc}.
+   *
+   * @return the {@link Doc}
+   */
+  public Doc build() {
+    return base;
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this)
+        .add("base", base)
+        .add("stack", stack)
+        .add("appendLevel", appendLevel)
+        .toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/FormatterDiagnostic.java b/core/src/main/java/com/google/googlejavaformat/FormatterDiagnostic.java
new file mode 100644
index 0000000..be7f8a6
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/FormatterDiagnostic.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2015 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;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/** An error that prevented formatting from succeeding. */
+public class FormatterDiagnostic {
+  private final int lineNumber;
+  private final String message;
+  private final int column;
+
+  public static FormatterDiagnostic create(String message) {
+    return new FormatterDiagnostic(-1, -1, message);
+  }
+
+  public static FormatterDiagnostic create(int lineNumber, int column, String message) {
+    checkArgument(lineNumber >= 0);
+    checkArgument(column >= 0);
+    checkNotNull(message);
+    return new FormatterDiagnostic(lineNumber, column, message);
+  }
+
+  private FormatterDiagnostic(int lineNumber, int column, String message) {
+    this.lineNumber = lineNumber;
+    this.column = column;
+    this.message = message;
+  }
+
+  /**
+   * Returns the line number on which the error occurred, or {@code -1} if the error does not have a
+   * line number.
+   */
+  public int line() {
+    return lineNumber;
+  }
+
+  /**
+   * Returns the 0-indexed column number on which the error occurred, or {@code -1} if the error
+   * does not have a column.
+   */
+  public int column() {
+    return column;
+  }
+
+  /** Returns a description of the problem that prevented formatting from succeeding. */
+  public String message() {
+    return message;
+  }
+
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    if (lineNumber >= 0) {
+      sb.append(lineNumber).append(':');
+    }
+    if (column >= 0) {
+      // internal column numbers are 0-based, but diagnostics use 1-based indexing by convention
+      sb.append(column + 1).append(':');
+    }
+    if (lineNumber >= 0 || column >= 0) {
+      sb.append(' ');
+    }
+    sb.append("error: ").append(message);
+    return sb.toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/FormattingError.java b/core/src/main/java/com/google/googlejavaformat/FormattingError.java
new file mode 100644
index 0000000..50381d0
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/FormattingError.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 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;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+
+/** An unchecked formatting error. */
+public class FormattingError extends Error {
+
+  private final ImmutableList<FormatterDiagnostic> diagnostics;
+
+  public FormattingError(FormatterDiagnostic diagnostic) {
+    this(ImmutableList.of(diagnostic));
+  }
+
+  public FormattingError(Iterable<FormatterDiagnostic> diagnostics) {
+    super(Joiner.on("\n").join(diagnostics) + "\n");
+    this.diagnostics = ImmutableList.copyOf(diagnostics);
+  }
+
+  public ImmutableList<FormatterDiagnostic> diagnostics() {
+    return diagnostics;
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/Indent.java b/core/src/main/java/com/google/googlejavaformat/Indent.java
new file mode 100644
index 0000000..44b56cd
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/Indent.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2015 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;
+
+import com.google.common.base.MoreObjects;
+import com.google.googlejavaformat.Output.BreakTag;
+
+/**
+ * An indent for a {@link Doc.Level} or {@link Doc.Break}. The indent is either a constant {@code
+ * int}, or a conditional expression whose value depends on whether or not a {@link Doc.Break} has
+ * been broken.
+ */
+public abstract class Indent {
+
+  abstract int eval();
+
+  /** A constant function, returning a constant indent. */
+  public static final class Const extends Indent {
+    private final int n;
+
+    public static final Const ZERO = new Const(+0);
+
+    private Const(int n) {
+      this.n = n;
+    }
+
+    public static Const make(int n, int indentMultiplier) {
+      return new Const(n * indentMultiplier);
+    }
+
+    @Override
+    int eval() {
+      return n;
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this).add("n", n).toString();
+    }
+  }
+
+  /** A conditional function, whose value depends on whether a break was taken. */
+  public static final class If extends Indent {
+    private final BreakTag condition;
+    private final Indent thenIndent;
+    private final Indent elseIndent;
+
+    private If(BreakTag condition, Indent thenIndent, Indent elseIndent) {
+      this.condition = condition;
+      this.thenIndent = thenIndent;
+      this.elseIndent = elseIndent;
+    }
+
+    public static If make(BreakTag condition, Indent thenIndent, Indent elseIndent) {
+      return new If(condition, thenIndent, elseIndent);
+    }
+
+    @Override
+    int eval() {
+      return (condition.wasBreakTaken() ? thenIndent : elseIndent).eval();
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("condition", condition)
+          .add("thenIndent", thenIndent)
+          .add("elseIndent", elseIndent)
+          .toString();
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/Input.java b/core/src/main/java/com/google/googlejavaformat/Input.java
new file mode 100644
index 0000000..9e17c2b
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/Input.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2015 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;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableRangeMap;
+
+/** An input to the formatter. */
+public abstract class Input extends InputOutput {
+  /**
+   * A {@code Tok} ("tock") is a token, or a comment, or a newline, or a maximal string of blanks. A
+   * token {@code Tok} underlies a {@link Token}, and each other {@code Tok} is attached to a single
+   * {@code Token}. Tokens and comments have indices; white space {@code Tok}s do not.
+   */
+  public interface Tok {
+    /**
+     * Return the {@code Tok}'s index.
+     *
+     * @return its index
+     */
+    int getIndex();
+
+    /**
+     * Return the {@code Tok}'s {@code 0}-based position.
+     *
+     * @return its position
+     */
+    int getPosition();
+
+    /**
+     * Return the {@code Tok}'s {@code 0}-based column number.
+     *
+     * @return its column number
+     */
+    int getColumn();
+
+    /** The {@code Tok}'s text. */
+    String getText();
+
+    /** The {@code Tok}'s original text (before processing escapes). */
+    String getOriginalText();
+
+    /** The length of the {@code Tok}'s original text. */
+    int length();
+
+    /** Is the {@code Tok} a newline? */
+    boolean isNewline();
+
+    /** Is the {@code Tok} a "//" comment? */
+    boolean isSlashSlashComment();
+
+    /** Is the {@code Tok} a "//" comment? */
+    boolean isSlashStarComment();
+
+    /** Is the {@code Tok} a javadoc comment? */
+    boolean isJavadocComment();
+
+    /** Is the {@code Tok} a comment? */
+    boolean isComment();
+  }
+
+  /** A {@code Token} is a language-level token. */
+  public interface Token {
+    /**
+     * Get the token's {@link Tok}.
+     *
+     * @return the token's {@link Tok}
+     */
+    Tok getTok();
+
+    /**
+     * Get the earlier {@link Tok}s assigned to this {@code Token}.
+     *
+     * @return the earlier {@link Tok}s assigned to this {@code Token}
+     */
+    ImmutableList<? extends Tok> getToksBefore();
+
+    /**
+     * Get the later {@link Tok}s assigned to this {@code Token}.
+     *
+     * @return the later {@link Tok}s assigned to this {@code Token}
+     */
+    ImmutableList<? extends Tok> getToksAfter();
+  }
+
+  /**
+   * Get the input tokens.
+   *
+   * @return the input tokens
+   */
+  public abstract ImmutableList<? extends Token> getTokens();
+
+  /** A map from [start, end] position ranges to {@link Token}s. */
+  public abstract ImmutableRangeMap<Integer, ? extends Token> getPositionTokenMap();
+
+  public abstract ImmutableMap<Integer, Integer> getPositionToColumnMap();
+
+  public abstract String getText();
+
+  /**
+   * Get the number of toks.
+   *
+   * @return the number of toks, including the EOF tok
+   */
+  public abstract int getkN();
+
+  /**
+   * Get the Token by index.
+   *
+   * @param k the token index
+   */
+  public abstract Token getToken(int k);
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this).add("super", super.toString()).toString();
+  }
+
+  /** Converts a character offset in the input to a line number. */
+  public abstract int getLineNumber(int inputPosition);
+
+  /** Converts a character offset in the input to a 0-based column number. */
+  public abstract int getColumnNumber(int inputPosition);
+
+  /**
+   * Construct a diagnostic. Populates the input filename, and converts character offsets to
+   * numbers.
+   */
+  public FormatterDiagnostic createDiagnostic(int inputPosition, String message) {
+    return FormatterDiagnostic.create(
+        getLineNumber(inputPosition), getColumnNumber(inputPosition), message);
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/InputOutput.java b/core/src/main/java/com/google/googlejavaformat/InputOutput.java
new file mode 100644
index 0000000..46dc70b
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/InputOutput.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2015 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;
+
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Range;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** This interface defines methods common to an {@link Input} or an {@link Output}. */
+public abstract class InputOutput {
+  private ImmutableList<String> lines = ImmutableList.of();
+
+  protected static final Range<Integer> EMPTY_RANGE = Range.closedOpen(-1, -1);
+  private static final DiscreteDomain<Integer> INTEGERS = DiscreteDomain.integers();
+
+  /** Set the lines. */
+  protected final void setLines(ImmutableList<String> lines) {
+    this.lines = lines;
+  }
+
+  /**
+   * Get the line count.
+   *
+   * @return the line count
+   */
+  public final int getLineCount() {
+    return lines.size();
+  }
+
+  /**
+   * Get a line.
+   *
+   * @param lineI the line number
+   * @return the line
+   */
+  public final String getLine(int lineI) {
+    return lines.get(lineI);
+  }
+
+  /** The {@link Range}s of the tokens or comments lying on each line, in any part. */
+  protected final List<Range<Integer>> ranges = new ArrayList<>();
+
+  private static void addToRanges(List<Range<Integer>> ranges, int i, int k) {
+    while (ranges.size() <= i) {
+      ranges.add(EMPTY_RANGE);
+    }
+    Range<Integer> oldValue = ranges.get(i);
+    ranges.set(i, Range.closedOpen(oldValue.isEmpty() ? k : oldValue.lowerEndpoint(), k + 1));
+  }
+
+  protected final void computeRanges(List<? extends Input.Tok> toks) {
+    int lineI = 0;
+    for (Input.Tok tok : toks) {
+      String txt = tok.getOriginalText();
+      int lineI0 = lineI;
+      lineI += Newlines.count(txt);
+      int k = tok.getIndex();
+      if (k >= 0) {
+        for (int i = lineI0; i <= lineI; i++) {
+          addToRanges(ranges, i, k);
+        }
+      }
+    }
+  }
+
+  /**
+   * Given an {@code InputOutput}, compute the map from tok indices to line ranges.
+   *
+   * @param put the {@code InputOutput}
+   * @return the map from {@code com.google.googlejavaformat.java.JavaInput.Tok} indices to line
+   *     ranges in this {@code put}
+   */
+  public static Map<Integer, Range<Integer>> makeKToIJ(InputOutput put) {
+    Map<Integer, Range<Integer>> map = new HashMap<>();
+    int ijN = put.getLineCount();
+    for (int ij = 0; ij <= ijN; ij++) {
+      Range<Integer> range = put.getRanges(ij).canonical(INTEGERS);
+      for (int k = range.lowerEndpoint(); k < range.upperEndpoint(); k++) {
+        if (map.containsKey(k)) {
+          map.put(k, Range.closedOpen(map.get(k).lowerEndpoint(), ij + 1));
+        } else {
+          map.put(k, Range.closedOpen(ij, ij + 1));
+        }
+      }
+    }
+    return map;
+  }
+
+  /**
+   * Get the {@link Range} of {@link Input.Tok}s lying in any part on a line.
+   *
+   * @param lineI the line number
+   * @return the {@link Range} of {@link Input.Tok}s on the specified line
+   */
+  public final Range<Integer> getRanges(int lineI) {
+    return 0 <= lineI && lineI < ranges.size() ? ranges.get(lineI) : EMPTY_RANGE;
+  }
+
+  @Override
+  public String toString() {
+    return "InputOutput{" + "lines=" + lines + ", ranges=" + ranges + '}';
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/Newlines.java b/core/src/main/java/com/google/googlejavaformat/Newlines.java
new file mode 100644
index 0000000..dbb82d3
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/Newlines.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2016 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;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterators;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/** Platform-independent newline handling. */
+public class Newlines {
+
+  /** Returns the number of line breaks in the input. */
+  public static int count(String input) {
+    return Iterators.size(lineOffsetIterator(input)) - 1;
+  }
+
+  /** Returns the index of the first break in the input, or {@code -1}. */
+  public static int firstBreak(String input) {
+    Iterator<Integer> it = lineOffsetIterator(input);
+    it.next();
+    return it.hasNext() ? it.next() : -1;
+  }
+
+  private static final ImmutableSet<String> BREAKS = ImmutableSet.of("\r\n", "\n", "\r");
+
+  /** Returns true if the entire input string is a recognized line break. */
+  public static boolean isNewline(String input) {
+    return BREAKS.contains(input);
+  }
+
+  /** Returns the length of the newline sequence at the current offset, or {@code -1}. */
+  public static int hasNewlineAt(String input, int idx) {
+    for (String b : BREAKS) {
+      if (input.startsWith(b, idx)) {
+        return b.length();
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * Returns the terminating line break in the input, or {@code null} if the input does not end in a
+   * break.
+   */
+  public static String getLineEnding(String input) {
+    for (String b : BREAKS) {
+      if (input.endsWith(b)) {
+        return b;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Returns the first line separator in the text, or {@code "\n"} if the text does not contain a
+   * single line separator.
+   */
+  public static String guessLineSeparator(String text) {
+    for (int i = 0; i < text.length(); i++) {
+      char c = text.charAt(i);
+      switch (c) {
+        case '\r':
+          if (i + 1 < text.length() && text.charAt(i + 1) == '\n') {
+            return "\r\n";
+          }
+          return "\r";
+        case '\n':
+          return "\n";
+        default:
+          break;
+      }
+    }
+    return "\n";
+  }
+
+  /** Returns true if the input contains any line breaks. */
+  public static boolean containsBreaks(String text) {
+    return CharMatcher.anyOf("\n\r").matchesAnyOf(text);
+  }
+
+  /** Returns an iterator over the start offsets of lines in the input. */
+  public static Iterator<Integer> lineOffsetIterator(String input) {
+    return new LineOffsetIterator(input);
+  }
+
+  /** Returns an iterator over lines in the input, including trailing whitespace. */
+  public static Iterator<String> lineIterator(String input) {
+    return new LineIterator(input);
+  }
+
+  private static class LineOffsetIterator implements Iterator<Integer> {
+
+    private int curr = 0;
+    private int idx = 0;
+    private final String input;
+
+    private LineOffsetIterator(String input) {
+      this.input = input;
+    }
+
+    @Override
+    public boolean hasNext() {
+      return curr != -1;
+    }
+
+    @Override
+    public Integer next() {
+      if (curr == -1) {
+        throw new NoSuchElementException();
+      }
+      int result = curr;
+      advance();
+      return result;
+    }
+
+    private void advance() {
+      for (; idx < input.length(); idx++) {
+        char c = input.charAt(idx);
+        switch (c) {
+          case '\r':
+            if (idx + 1 < input.length() && input.charAt(idx + 1) == '\n') {
+              idx++;
+            }
+            // falls through
+          case '\n':
+            idx++;
+            curr = idx;
+            return;
+          default:
+            break;
+        }
+      }
+      curr = -1;
+    }
+
+    @Override
+    public void remove() {
+      throw new UnsupportedOperationException("remove");
+    }
+  }
+
+  private static class LineIterator implements Iterator<String> {
+
+    int idx;
+    String curr;
+
+    private final String input;
+    private final Iterator<Integer> indices;
+
+    private LineIterator(String input) {
+      this.input = input;
+      this.indices = lineOffsetIterator(input);
+      idx = indices.next(); // read leading 0
+    }
+
+    private void advance() {
+      int last = idx;
+      if (indices.hasNext()) {
+        idx = indices.next();
+      } else if (hasNext()) {
+        // no terminal line break
+        idx = input.length();
+      } else {
+        throw new NoSuchElementException();
+      }
+      curr = input.substring(last, idx);
+    }
+
+    @Override
+    public boolean hasNext() {
+      return idx < input.length();
+    }
+
+    @Override
+    public String next() {
+      advance();
+      return curr;
+    }
+
+    @Override
+    public void remove() {
+      throw new UnsupportedOperationException("remove");
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/Op.java b/core/src/main/java/com/google/googlejavaformat/Op.java
new file mode 100644
index 0000000..a484541
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/Op.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2015 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;
+
+/**
+ * An {@code Op} is a member of the sequence of formatting operations emitted by {@link OpsBuilder}
+ * and transformed by {@link DocBuilder} into a {@link Doc}. Leaf subclasses of {@link Doc}
+ * implement {@code Op}; {@link Doc.Level} is the only non-leaf, and is represented by paired {@link
+ * OpenOp}-{@link CloseOp} {@code Op}s.
+ */
+public interface Op {
+  /**
+   * Add an {@code Op} to a {@link DocBuilder}.
+   *
+   * @param builder the {@link DocBuilder}
+   */
+  void add(DocBuilder builder);
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/OpenOp.java b/core/src/main/java/com/google/googlejavaformat/OpenOp.java
new file mode 100644
index 0000000..6498201
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/OpenOp.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2015 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;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * An {@code OpenOp} opens a level. It is an {@link Op} in the sequence of {@link Op}s generated by
+ * {@link OpsBuilder}. When the sequence is turned into a {@link Doc} by {@link DocBuilder}, {@link
+ * Input.Tok}s delimited by {@code OpenOp}-{@link CloseOp} pairs turn into nested {@link
+ * Doc.Level}s.
+ */
+public final class OpenOp implements Op {
+  private final Indent plusIndent;
+
+  private OpenOp(Indent plusIndent) {
+    this.plusIndent = plusIndent;
+  }
+
+  /**
+   * Make an ordinary {@code OpenOp}.
+   *
+   * @param plusIndent the indent for breaks at this level
+   * @return the {@code OpenOp}
+   */
+  public static Op make(Indent plusIndent) {
+    return new OpenOp(plusIndent);
+  }
+
+  @Override
+  public void add(DocBuilder builder) {
+    builder.open(plusIndent);
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this).add("plusIndent", plusIndent).toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java b/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java
new file mode 100644
index 0000000..e8e100f
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java
@@ -0,0 +1,631 @@
+/*
+ * Copyright 2015 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;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import com.google.googlejavaformat.Indent.Const;
+import com.google.googlejavaformat.Input.Tok;
+import com.google.googlejavaformat.Input.Token;
+import com.google.googlejavaformat.Output.BreakTag;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * An {@code OpsBuilder} creates a list of {@link Op}s, which is turned into a {@link Doc} by {@link
+ * DocBuilder}.
+ */
+public final class OpsBuilder {
+
+  /** @return the actual size of the AST node at position, including comments. */
+  public int actualSize(int position, int length) {
+    Token startToken = input.getPositionTokenMap().get(position);
+    int start = startToken.getTok().getPosition();
+    for (Tok tok : startToken.getToksBefore()) {
+      if (tok.isComment()) {
+        start = Math.min(start, tok.getPosition());
+      }
+    }
+    Token endToken = input.getPositionTokenMap().get(position + length - 1);
+    int end = endToken.getTok().getPosition() + endToken.getTok().length();
+    for (Tok tok : endToken.getToksAfter()) {
+      if (tok.isComment()) {
+        end = Math.max(end, tok.getPosition() + tok.length());
+      }
+    }
+    return end - start;
+  }
+
+  /** @return the start column of the token at {@code position}, including leading comments. */
+  public Integer actualStartColumn(int position) {
+    Token startToken = input.getPositionTokenMap().get(position);
+    int start = startToken.getTok().getPosition();
+    int line0 = input.getLineNumber(start);
+    for (Tok tok : startToken.getToksBefore()) {
+      if (line0 != input.getLineNumber(tok.getPosition())) {
+        return start;
+      }
+      if (tok.isComment()) {
+        start = Math.min(start, tok.getPosition());
+      }
+    }
+    return start;
+  }
+
+  /** A request to add or remove a blank line in the output. */
+  public abstract static class BlankLineWanted {
+
+    /** Always emit a blank line. */
+    public static final BlankLineWanted YES = new SimpleBlankLine(Optional.of(true));
+
+    /** Never emit a blank line. */
+    public static final BlankLineWanted NO = new SimpleBlankLine(Optional.of(false));
+
+    /**
+     * Explicitly preserve blank lines from the input (e.g. before the first member in a class
+     * declaration). Overrides conditional blank lines.
+     */
+    public static final BlankLineWanted PRESERVE =
+        new SimpleBlankLine(/* wanted= */ Optional.empty());
+
+    /** Is the blank line wanted? */
+    public abstract Optional<Boolean> wanted();
+
+    /** Merge this blank line request with another. */
+    public abstract BlankLineWanted merge(BlankLineWanted wanted);
+
+    /** Emit a blank line if the given break is taken. */
+    public static BlankLineWanted conditional(BreakTag breakTag) {
+      return new ConditionalBlankLine(ImmutableList.of(breakTag));
+    }
+
+    private static final class SimpleBlankLine extends BlankLineWanted {
+      private final Optional<Boolean> wanted;
+
+      SimpleBlankLine(Optional<Boolean> wanted) {
+        this.wanted = wanted;
+      }
+
+      @Override
+      public Optional<Boolean> wanted() {
+        return wanted;
+      }
+
+      @Override
+      public BlankLineWanted merge(BlankLineWanted other) {
+        return this;
+      }
+    }
+
+    private static final class ConditionalBlankLine extends BlankLineWanted {
+
+      private final ImmutableList<BreakTag> tags;
+
+      ConditionalBlankLine(Iterable<BreakTag> tags) {
+        this.tags = ImmutableList.copyOf(tags);
+      }
+
+      @Override
+      public Optional<Boolean> wanted() {
+        for (BreakTag tag : tags) {
+          if (tag.wasBreakTaken()) {
+            return Optional.of(true);
+          }
+        }
+        return Optional.empty();
+      }
+
+      @Override
+      public BlankLineWanted merge(BlankLineWanted other) {
+        if (!(other instanceof ConditionalBlankLine)) {
+          return other;
+        }
+        return new ConditionalBlankLine(
+            Iterables.concat(this.tags, ((ConditionalBlankLine) other).tags));
+      }
+    }
+  }
+
+  private final Input input;
+  private final List<Op> ops = new ArrayList<>();
+  private final Output output;
+  private static final Indent.Const ZERO = Indent.Const.ZERO;
+
+  private int tokenI = 0;
+  private int inputPosition = Integer.MIN_VALUE;
+
+  /** The number of unclosed open ops in the input stream. */
+  int depth = 0;
+
+  /** Add an {@link Op}, and record open/close ops for later validation of unclosed levels. */
+  private void add(Op op) {
+    if (op instanceof OpenOp) {
+      depth++;
+    } else if (op instanceof CloseOp) {
+      depth--;
+      if (depth < 0) {
+        throw new AssertionError();
+      }
+    }
+    ops.add(op);
+  }
+
+  /** Add a list of {@link Op}s. */
+  public final void addAll(List<Op> ops) {
+    for (Op op : ops) {
+      add(op);
+    }
+  }
+
+  /**
+   * The {@code OpsBuilder} constructor.
+   *
+   * @param input the {@link Input}, used for retrieve information from the AST
+   * @param output the {@link Output}, used here only to record blank-line information
+   */
+  public OpsBuilder(Input input, Output output) {
+    this.input = input;
+    this.output = output;
+  }
+
+  /** Get the {@code OpsBuilder}'s {@link Input}. */
+  public final Input getInput() {
+    return input;
+  }
+
+  /** Returns the number of unclosed open ops in the input stream. */
+  public int depth() {
+    return depth;
+  }
+
+  /**
+   * Checks that all open ops in the op stream have matching close ops.
+   *
+   * @throws FormattingError if any ops were unclosed
+   */
+  public void checkClosed(int previous) {
+    if (depth != previous) {
+      throw new FormattingError(diagnostic(String.format("saw %d unclosed ops", depth)));
+    }
+  }
+
+  /** Create a {@link FormatterDiagnostic} at the current position. */
+  public FormatterDiagnostic diagnostic(String message) {
+    return input.createDiagnostic(inputPosition, message);
+  }
+
+  /**
+   * Sync to position in the input. If we've skipped outputting any tokens that were present in the
+   * input tokens, output them here and optionally complain.
+   *
+   * @param inputPosition the {@code 0}-based input position
+   */
+  public final void sync(int inputPosition) {
+    if (inputPosition > this.inputPosition) {
+      ImmutableList<? extends Input.Token> tokens = input.getTokens();
+      int tokensN = tokens.size();
+      this.inputPosition = inputPosition;
+      if (tokenI < tokensN && inputPosition > tokens.get(tokenI).getTok().getPosition()) {
+        // Found a missing input token. Insert it and mark it missing (usually not good).
+        Input.Token token = tokens.get(tokenI++);
+        throw new FormattingError(
+            diagnostic(String.format("did not generate token \"%s\"", token.getTok().getText())));
+      }
+    }
+  }
+
+  /** Output any remaining tokens from the input stream (e.g. terminal whitespace). */
+  public final void drain() {
+    int inputPosition = input.getText().length() + 1;
+    if (inputPosition > this.inputPosition) {
+      ImmutableList<? extends Input.Token> tokens = input.getTokens();
+      int tokensN = tokens.size();
+      while (tokenI < tokensN && inputPosition > tokens.get(tokenI).getTok().getPosition()) {
+        Input.Token token = tokens.get(tokenI++);
+        add(
+            Doc.Token.make(
+                token,
+                Doc.Token.RealOrImaginary.IMAGINARY,
+                ZERO,
+                /* breakAndIndentTrailingComment= */ Optional.empty()));
+      }
+    }
+    this.inputPosition = inputPosition;
+    checkClosed(0);
+  }
+
+  /**
+   * Open a new level by emitting an {@link OpenOp}.
+   *
+   * @param plusIndent the extra indent for the new level
+   */
+  public final void open(Indent plusIndent) {
+    add(OpenOp.make(plusIndent));
+  }
+
+  /** Close the current level, by emitting a {@link CloseOp}. */
+  public final void close() {
+    add(CloseOp.make());
+  }
+
+  /** Return the text of the next {@link Input.Token}, or absent if there is none. */
+  public final Optional<String> peekToken() {
+    return peekToken(0);
+  }
+
+  /** Return the text of an upcoming {@link Input.Token}, or absent if there is none. */
+  public final Optional<String> peekToken(int skip) {
+    ImmutableList<? extends Input.Token> tokens = input.getTokens();
+    int idx = tokenI + skip;
+    return idx < tokens.size()
+        ? Optional.of(tokens.get(idx).getTok().getOriginalText())
+        : Optional.empty();
+  }
+
+  /**
+   * Emit an optional token iff it exists on the input. This is used to emit tokens whose existence
+   * has been lost in the AST.
+   *
+   * @param token the optional token
+   */
+  public final void guessToken(String token) {
+    token(
+        token,
+        Doc.Token.RealOrImaginary.IMAGINARY,
+        ZERO,
+        /* breakAndIndentTrailingComment=  */ Optional.empty());
+  }
+
+  public final void token(
+      String token,
+      Doc.Token.RealOrImaginary realOrImaginary,
+      Indent plusIndentCommentsBefore,
+      Optional<Indent> breakAndIndentTrailingComment) {
+    ImmutableList<? extends Input.Token> tokens = input.getTokens();
+    if (token.equals(peekToken().orElse(null))) { // Found the input token. Output it.
+      add(
+          Doc.Token.make(
+              tokens.get(tokenI++),
+              Doc.Token.RealOrImaginary.REAL,
+              plusIndentCommentsBefore,
+              breakAndIndentTrailingComment));
+    } else {
+      /*
+       * Generated a "bad" token, which doesn't exist on the input. Drop it, and complain unless
+       * (for example) we're guessing at an optional token.
+       */
+      if (realOrImaginary.isReal()) {
+        throw new FormattingError(
+            diagnostic(
+                String.format(
+                    "expected token: '%s'; generated %s instead",
+                    peekToken().orElse(null), token)));
+      }
+    }
+  }
+
+  /**
+   * Emit a single- or multi-character op by breaking it into single-character {@link Doc.Token}s.
+   *
+   * @param op the operator to emit
+   */
+  public final void op(String op) {
+    int opN = op.length();
+    for (int i = 0; i < opN; i++) {
+      token(
+          op.substring(i, i + 1),
+          Doc.Token.RealOrImaginary.REAL,
+          ZERO,
+          /* breakAndIndentTrailingComment=  */ Optional.empty());
+    }
+  }
+
+  /** Emit a {@link Doc.Space}. */
+  public final void space() {
+    add(Doc.Space.make());
+  }
+
+  /** Emit a {@link Doc.Break}. */
+  public final void breakOp() {
+    breakOp(Doc.FillMode.UNIFIED, "", ZERO);
+  }
+
+  /**
+   * Emit a {@link Doc.Break}.
+   *
+   * @param plusIndent extra indent if taken
+   */
+  public final void breakOp(Indent plusIndent) {
+    breakOp(Doc.FillMode.UNIFIED, "", plusIndent);
+  }
+
+  /** Emit a filled {@link Doc.Break}. */
+  public final void breakToFill() {
+    breakOp(Doc.FillMode.INDEPENDENT, "", ZERO);
+  }
+
+  /** Emit a forced {@link Doc.Break}. */
+  public final void forcedBreak() {
+    breakOp(Doc.FillMode.FORCED, "", ZERO);
+  }
+
+  /**
+   * Emit a forced {@link Doc.Break}.
+   *
+   * @param plusIndent extra indent if taken
+   */
+  public final void forcedBreak(Indent plusIndent) {
+    breakOp(Doc.FillMode.FORCED, "", plusIndent);
+  }
+
+  /**
+   * Emit a {@link Doc.Break}, with a specified {@code flat} value (e.g., {@code " "}).
+   *
+   * @param flat the {@link Doc.Break} when not broken
+   */
+  public final void breakOp(String flat) {
+    breakOp(Doc.FillMode.UNIFIED, flat, ZERO);
+  }
+
+  /**
+   * Emit a {@link Doc.Break}, with a specified {@code flat} value (e.g., {@code " "}).
+   *
+   * @param flat the {@link Doc.Break} when not broken
+   */
+  public final void breakToFill(String flat) {
+    breakOp(Doc.FillMode.INDEPENDENT, flat, ZERO);
+  }
+
+  /**
+   * Emit a generic {@link Doc.Break}.
+   *
+   * @param fillMode the {@link Doc.FillMode}
+   * @param flat the {@link Doc.Break} when not broken
+   * @param plusIndent extra indent if taken
+   */
+  public final void breakOp(Doc.FillMode fillMode, String flat, Indent plusIndent) {
+    breakOp(fillMode, flat, plusIndent, /* optionalTag=  */ Optional.empty());
+  }
+
+  /**
+   * Emit a generic {@link Doc.Break}.
+   *
+   * @param fillMode the {@link Doc.FillMode}
+   * @param flat the {@link Doc.Break} when not broken
+   * @param plusIndent extra indent if taken
+   * @param optionalTag an optional tag for remembering whether the break was taken
+   */
+  public final void breakOp(
+      Doc.FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optionalTag) {
+    add(Doc.Break.make(fillMode, flat, plusIndent, optionalTag));
+  }
+
+  private int lastPartialFormatBoundary = -1;
+
+  /**
+   * Make the boundary of a region that can be partially formatted. The boundary will be included in
+   * the following region, e.g.: [[boundary0, boundary1), [boundary1, boundary2), ...].
+   */
+  public void markForPartialFormat() {
+    if (lastPartialFormatBoundary == -1) {
+      lastPartialFormatBoundary = tokenI;
+      return;
+    }
+    if (tokenI == lastPartialFormatBoundary) {
+      return;
+    }
+    Token start = input.getTokens().get(lastPartialFormatBoundary);
+    Token end = input.getTokens().get(tokenI - 1);
+    output.markForPartialFormat(start, end);
+    lastPartialFormatBoundary = tokenI;
+  }
+
+  /**
+   * Force or suppress a blank line here in the output.
+   *
+   * @param wanted whether to force ({@code true}) or suppress {@code false}) the blank line
+   */
+  public final void blankLineWanted(BlankLineWanted wanted) {
+    output.blankLine(getI(input.getTokens().get(tokenI)), wanted);
+  }
+
+  private static int getI(Input.Token token) {
+    for (Input.Tok tok : token.getToksBefore()) {
+      if (tok.getIndex() >= 0) {
+        return tok.getIndex();
+      }
+    }
+    return token.getTok().getIndex();
+  }
+
+  private static final Doc.Space SPACE = Doc.Space.make();
+
+  /**
+   * Build a list of {@link Op}s from the {@code OpsBuilder}.
+   *
+   * @return the list of {@link Op}s
+   */
+  public final ImmutableList<Op> build() {
+    markForPartialFormat();
+    // Rewrite the ops to insert comments.
+    Multimap<Integer, Op> tokOps = ArrayListMultimap.create();
+    int opsN = ops.size();
+    for (int i = 0; i < opsN; i++) {
+      Op op = ops.get(i);
+      if (op instanceof Doc.Token) {
+        /*
+         * Token ops can have associated non-tokens, including comments, which we need to insert.
+         * They can also cause line breaks, so we insert them before or after the current level,
+         * when possible.
+         */
+        Doc.Token tokenOp = (Doc.Token) op;
+        Input.Token token = tokenOp.getToken();
+        int j = i; // Where to insert toksBefore before.
+        while (0 < j && ops.get(j - 1) instanceof OpenOp) {
+          --j;
+        }
+        int k = i; // Where to insert toksAfter after.
+        while (k + 1 < opsN && ops.get(k + 1) instanceof CloseOp) {
+          ++k;
+        }
+        if (tokenOp.realOrImaginary().isReal()) {
+          /*
+           * Regular input token. Copy out toksBefore before token, and toksAfter after it. Insert
+           * this token's toksBefore at position j.
+           */
+          int newlines = 0; // Count of newlines in a row.
+          boolean space = false; // Do we need an extra space after a previous "/*" comment?
+          boolean lastWasComment = false; // Was the last thing we output a comment?
+          boolean allowBlankAfterLastComment = false;
+          for (Input.Tok tokBefore : token.getToksBefore()) {
+            if (tokBefore.isNewline()) {
+              newlines++;
+            } else if (tokBefore.isComment()) {
+              tokOps.put(
+                  j,
+                  Doc.Break.make(
+                      tokBefore.isSlashSlashComment() ? Doc.FillMode.FORCED : Doc.FillMode.UNIFIED,
+                      "",
+                      tokenOp.getPlusIndentCommentsBefore()));
+              tokOps.putAll(j, makeComment(tokBefore));
+              space = tokBefore.isSlashStarComment();
+              newlines = 0;
+              lastWasComment = true;
+              if (tokBefore.isJavadocComment()) {
+                tokOps.put(j, Doc.Break.makeForced());
+              }
+              allowBlankAfterLastComment =
+                  tokBefore.isSlashSlashComment()
+                      || (tokBefore.isSlashStarComment() && !tokBefore.isJavadocComment());
+            }
+          }
+          if (allowBlankAfterLastComment && newlines > 1) {
+            // Force a line break after two newlines in a row following a line or block comment
+            output.blankLine(token.getTok().getIndex(), BlankLineWanted.YES);
+          }
+          if (lastWasComment && newlines > 0) {
+            tokOps.put(j, Doc.Break.makeForced());
+          } else if (space) {
+            tokOps.put(j, SPACE);
+          }
+          // Now we've seen the Token; output the toksAfter.
+          for (Input.Tok tokAfter : token.getToksAfter()) {
+            if (tokAfter.isComment()) {
+              boolean breakAfter =
+                  tokAfter.isJavadocComment()
+                      || (tokAfter.isSlashStarComment()
+                          && tokenOp.breakAndIndentTrailingComment().isPresent());
+              if (breakAfter) {
+                tokOps.put(
+                    k + 1,
+                    Doc.Break.make(
+                        Doc.FillMode.FORCED,
+                        "",
+                        tokenOp.breakAndIndentTrailingComment().orElse(Const.ZERO)));
+              } else {
+                tokOps.put(k + 1, SPACE);
+              }
+              tokOps.putAll(k + 1, makeComment(tokAfter));
+              if (breakAfter) {
+                tokOps.put(k + 1, Doc.Break.make(Doc.FillMode.FORCED, "", ZERO));
+              }
+            }
+          }
+        } else {
+          /*
+           * This input token was mistakenly not generated for output. As no whitespace or comments
+           * were generated (presumably), copy all input non-tokens literally, even spaces and
+           * newlines.
+           */
+          int newlines = 0;
+          boolean lastWasComment = false;
+          for (Input.Tok tokBefore : token.getToksBefore()) {
+            if (tokBefore.isNewline()) {
+              newlines++;
+            } else if (tokBefore.isComment()) {
+              newlines = 0;
+              lastWasComment = tokBefore.isComment();
+            }
+            if (lastWasComment && newlines > 0) {
+              tokOps.put(j, Doc.Break.makeForced());
+            }
+            tokOps.put(j, Doc.Tok.make(tokBefore));
+          }
+          for (Input.Tok tokAfter : token.getToksAfter()) {
+            tokOps.put(k + 1, Doc.Tok.make(tokAfter));
+          }
+        }
+      }
+    }
+    /*
+     * Construct new list of ops, splicing in the comments. If a comment is inserted immediately
+     * before a space, suppress the space.
+     */
+    ImmutableList.Builder<Op> newOps = ImmutableList.builder();
+    boolean afterForcedBreak = false; // Was the last Op a forced break? If so, suppress spaces.
+    for (int i = 0; i < opsN; i++) {
+      for (Op op : tokOps.get(i)) {
+        if (!(afterForcedBreak && op instanceof Doc.Space)) {
+          newOps.add(op);
+          afterForcedBreak = isForcedBreak(op);
+        }
+      }
+      Op op = ops.get(i);
+      if (afterForcedBreak
+          && (op instanceof Doc.Space
+              || (op instanceof Doc.Break
+                  && ((Doc.Break) op).getPlusIndent() == 0
+                  && " ".equals(((Doc) op).getFlat())))) {
+        continue;
+      }
+      newOps.add(op);
+      if (!(op instanceof OpenOp)) {
+        afterForcedBreak = isForcedBreak(op);
+      }
+    }
+    for (Op op : tokOps.get(opsN)) {
+      if (!(afterForcedBreak && op instanceof Doc.Space)) {
+        newOps.add(op);
+        afterForcedBreak = isForcedBreak(op);
+      }
+    }
+    return newOps.build();
+  }
+
+  private static boolean isForcedBreak(Op op) {
+    return op instanceof Doc.Break && ((Doc.Break) op).isForced();
+  }
+
+  private static List<Op> makeComment(Input.Tok comment) {
+    return comment.isSlashStarComment()
+        ? ImmutableList.of(Doc.Tok.make(comment))
+        : ImmutableList.of(Doc.Tok.make(comment), Doc.Break.makeForced());
+  }
+
+  @Override
+  public final String toString() {
+    return MoreObjects.toStringHelper(this)
+        .add("input", input)
+        .add("ops", ops)
+        .add("output", output)
+        .add("tokenI", tokenI)
+        .add("inputPosition", inputPosition)
+        .toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/Output.java b/core/src/main/java/com/google/googlejavaformat/Output.java
new file mode 100644
index 0000000..ea039fa
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/Output.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2015 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;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.Range;
+import com.google.googlejavaformat.OpsBuilder.BlankLineWanted;
+import java.util.Optional;
+
+/** An output from the formatter. */
+public abstract class Output extends InputOutput {
+  /** Unique identifier for a break. */
+  public static final class BreakTag {
+
+    Optional<Boolean> taken = Optional.empty();
+
+    public void recordBroken(boolean broken) {
+      // TODO(cushon): enforce invariants.
+      // Currently we rely on setting Breaks multiple times, e.g. when deciding
+      // whether a Level should be flowed. Using separate data structures
+      // instead of mutation or adding an explicit 'reset' step would allow
+      // a useful invariant to be enforced here.
+      taken = Optional.of(broken);
+    }
+
+    public boolean wasBreakTaken() {
+      return taken.orElse(false);
+    }
+  }
+
+  /**
+   * Indent by outputting {@code indent} spaces.
+   *
+   * @param indent the current indent
+   */
+  public abstract void indent(int indent);
+
+  /**
+   * Output a string.
+   *
+   * @param text the string
+   * @param range the {@link Range} corresponding to the string
+   */
+  public abstract void append(String text, Range<Integer> range);
+
+  /**
+   * A blank line is or is not wanted here.
+   *
+   * @param k the {@link Input.Tok} index
+   * @param wanted whether a blank line is wanted here
+   */
+  public abstract void blankLine(int k, BlankLineWanted wanted);
+
+  /** Marks a region that can be partially formatted. */
+  public abstract void markForPartialFormat(Input.Token start, Input.Token end);
+
+  /**
+   * Get the {@link CommentsHelper}.
+   *
+   * @return the {@link CommentsHelper}
+   */
+  public abstract CommentsHelper getCommentsHelper();
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this).add("super", super.toString()).toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java
new file mode 100644
index 0000000..5a23328
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2016 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 com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableRangeSet;
+import java.util.Optional;
+
+/**
+ * Command line options for google-java-format.
+ *
+ * <p>google-java-format doesn't depend on AutoValue, to allow AutoValue to depend on
+ * google-java-format.
+ */
+final class CommandLineOptions {
+
+  private final ImmutableList<String> files;
+  private final boolean inPlace;
+  private final ImmutableRangeSet<Integer> lines;
+  private final ImmutableList<Integer> offsets;
+  private final ImmutableList<Integer> lengths;
+  private final boolean aosp;
+  private final boolean version;
+  private final boolean help;
+  private final boolean stdin;
+  private final boolean fixImportsOnly;
+  private final boolean sortImports;
+  private final boolean removeUnusedImports;
+  private final boolean dryRun;
+  private final boolean setExitIfChanged;
+  private final Optional<String> assumeFilename;
+  private final boolean reflowLongStrings;
+  private final boolean formatJavadoc;
+
+  CommandLineOptions(
+      ImmutableList<String> files,
+      boolean inPlace,
+      ImmutableRangeSet<Integer> lines,
+      ImmutableList<Integer> offsets,
+      ImmutableList<Integer> lengths,
+      boolean aosp,
+      boolean version,
+      boolean help,
+      boolean stdin,
+      boolean fixImportsOnly,
+      boolean sortImports,
+      boolean removeUnusedImports,
+      boolean dryRun,
+      boolean setExitIfChanged,
+      Optional<String> assumeFilename,
+      boolean reflowLongStrings,
+      boolean formatJavadoc) {
+    this.files = files;
+    this.inPlace = inPlace;
+    this.lines = lines;
+    this.offsets = offsets;
+    this.lengths = lengths;
+    this.aosp = aosp;
+    this.version = version;
+    this.help = help;
+    this.stdin = stdin;
+    this.fixImportsOnly = fixImportsOnly;
+    this.sortImports = sortImports;
+    this.removeUnusedImports = removeUnusedImports;
+    this.dryRun = dryRun;
+    this.setExitIfChanged = setExitIfChanged;
+    this.assumeFilename = assumeFilename;
+    this.reflowLongStrings = reflowLongStrings;
+    this.formatJavadoc = formatJavadoc;
+  }
+
+  /** The files to format. */
+  ImmutableList<String> files() {
+    return files;
+  }
+
+  /** Format files in place. */
+  boolean inPlace() {
+    return inPlace;
+  }
+
+  /** Line ranges to format. */
+  ImmutableRangeSet<Integer> lines() {
+    return lines;
+  }
+
+  /** Character offsets for partial formatting, paired with {@code lengths}. */
+  ImmutableList<Integer> offsets() {
+    return offsets;
+  }
+
+  /** Partial formatting region lengths, paired with {@code offsets}. */
+  ImmutableList<Integer> lengths() {
+    return lengths;
+  }
+
+  /** Use AOSP style instead of Google Style (4-space indentation). */
+  boolean aosp() {
+    return aosp;
+  }
+
+  /** Print the version. */
+  boolean version() {
+    return version;
+  }
+
+  /** Print usage information. */
+  boolean help() {
+    return help;
+  }
+
+  /** Format input from stdin. */
+  boolean stdin() {
+    return stdin;
+  }
+
+  /** Fix imports, but do no formatting. */
+  boolean fixImportsOnly() {
+    return fixImportsOnly;
+  }
+
+  /** Sort imports. */
+  boolean sortImports() {
+    return sortImports;
+  }
+
+  /** Remove unused imports. */
+  boolean removeUnusedImports() {
+    return removeUnusedImports;
+  }
+
+  /**
+   * Print the paths of the files whose contents would change if the formatter were run normally.
+   */
+  boolean dryRun() {
+    return dryRun;
+  }
+
+  /** Return exit code 1 if there are any formatting changes. */
+  boolean setExitIfChanged() {
+    return setExitIfChanged;
+  }
+
+  /** Return the name to use for diagnostics when formatting standard input. */
+  Optional<String> assumeFilename() {
+    return assumeFilename;
+  }
+
+  boolean reflowLongStrings() {
+    return reflowLongStrings;
+  }
+
+  /** Returns true if partial formatting was selected. */
+  boolean isSelection() {
+    return !lines().isEmpty() || !offsets().isEmpty() || !lengths().isEmpty();
+  }
+
+  boolean formatJavadoc() {
+    return formatJavadoc;
+  }
+
+  static Builder builder() {
+    return new Builder();
+  }
+
+  static class Builder {
+
+    private final ImmutableList.Builder<String> files = ImmutableList.builder();
+    private final ImmutableRangeSet.Builder<Integer> lines = ImmutableRangeSet.builder();
+    private final ImmutableList.Builder<Integer> offsets = ImmutableList.builder();
+    private final ImmutableList.Builder<Integer> lengths = ImmutableList.builder();
+    private boolean inPlace = false;
+    private boolean aosp = false;
+    private boolean version = false;
+    private boolean help = false;
+    private boolean stdin = false;
+    private boolean fixImportsOnly = false;
+    private boolean sortImports = true;
+    private boolean removeUnusedImports = true;
+    private boolean dryRun = false;
+    private boolean setExitIfChanged = false;
+    private Optional<String> assumeFilename = Optional.empty();
+    private boolean reflowLongStrings = true;
+    private boolean formatJavadoc = true;
+
+    ImmutableList.Builder<String> filesBuilder() {
+      return files;
+    }
+
+    Builder inPlace(boolean inPlace) {
+      this.inPlace = inPlace;
+      return this;
+    }
+
+    ImmutableRangeSet.Builder<Integer> linesBuilder() {
+      return lines;
+    }
+
+    Builder addOffset(Integer offset) {
+      offsets.add(offset);
+      return this;
+    }
+
+    Builder addLength(Integer length) {
+      lengths.add(length);
+      return this;
+    }
+
+    Builder aosp(boolean aosp) {
+      this.aosp = aosp;
+      return this;
+    }
+
+    Builder version(boolean version) {
+      this.version = version;
+      return this;
+    }
+
+    Builder help(boolean help) {
+      this.help = help;
+      return this;
+    }
+
+    Builder stdin(boolean stdin) {
+      this.stdin = stdin;
+      return this;
+    }
+
+    Builder fixImportsOnly(boolean fixImportsOnly) {
+      this.fixImportsOnly = fixImportsOnly;
+      return this;
+    }
+
+    Builder sortImports(boolean sortImports) {
+      this.sortImports = sortImports;
+      return this;
+    }
+
+    Builder removeUnusedImports(boolean removeUnusedImports) {
+      this.removeUnusedImports = removeUnusedImports;
+      return this;
+    }
+
+    Builder dryRun(boolean dryRun) {
+      this.dryRun = dryRun;
+      return this;
+    }
+
+    Builder setExitIfChanged(boolean setExitIfChanged) {
+      this.setExitIfChanged = setExitIfChanged;
+      return this;
+    }
+
+    Builder assumeFilename(String assumeFilename) {
+      this.assumeFilename = Optional.of(assumeFilename);
+      return this;
+    }
+
+    Builder reflowLongStrings(boolean reflowLongStrings) {
+      this.reflowLongStrings = reflowLongStrings;
+      return this;
+    }
+
+    Builder formatJavadoc(boolean formatJavadoc) {
+      this.formatJavadoc = formatJavadoc;
+      return this;
+    }
+
+    CommandLineOptions build() {
+      return new CommandLineOptions(
+          files.build(),
+          inPlace,
+          lines.build(),
+          offsets.build(),
+          lengths.build(),
+          aosp,
+          version,
+          help,
+          stdin,
+          fixImportsOnly,
+          sortImports,
+          removeUnusedImports,
+          dryRun,
+          setExitIfChanged,
+          assumeFilename,
+          reflowLongStrings,
+          formatJavadoc);
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java
new file mode 100644
index 0000000..2023826
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2016 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 java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableRangeSet;
+import com.google.common.collect.Range;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/** A parser for {@link CommandLineOptions}. */
+final class CommandLineOptionsParser {
+
+  private static final Splitter COMMA_SPLITTER = Splitter.on(',');
+  private static final Splitter COLON_SPLITTER = Splitter.on(':');
+  private static final Splitter ARG_SPLITTER =
+      Splitter.on(CharMatcher.breakingWhitespace()).omitEmptyStrings().trimResults();
+
+  /** Parses {@link CommandLineOptions}. */
+  static CommandLineOptions parse(Iterable<String> options) {
+    CommandLineOptions.Builder optionsBuilder = CommandLineOptions.builder();
+    List<String> expandedOptions = new ArrayList<>();
+    expandParamsFiles(options, expandedOptions);
+    Iterator<String> it = expandedOptions.iterator();
+    while (it.hasNext()) {
+      String option = it.next();
+      if (!option.startsWith("-")) {
+        optionsBuilder.filesBuilder().add(option).addAll(it);
+        break;
+      }
+      String flag;
+      String value;
+      int idx = option.indexOf('=');
+      if (idx >= 0) {
+        flag = option.substring(0, idx);
+        value = option.substring(idx + 1, option.length());
+      } else {
+        flag = option;
+        value = null;
+      }
+      // NOTE: update usage information in UsageException when new flags are added
+      switch (flag) {
+        case "-i":
+        case "-r":
+        case "-replace":
+        case "--replace":
+          optionsBuilder.inPlace(true);
+          break;
+        case "--lines":
+        case "-lines":
+        case "--line":
+        case "-line":
+          parseRangeSet(optionsBuilder.linesBuilder(), getValue(flag, it, value));
+          break;
+        case "--offset":
+        case "-offset":
+          optionsBuilder.addOffset(parseInteger(it, flag, value));
+          break;
+        case "--length":
+        case "-length":
+          optionsBuilder.addLength(parseInteger(it, flag, value));
+          break;
+        case "--aosp":
+        case "-aosp":
+        case "-a":
+          optionsBuilder.aosp(true);
+          break;
+        case "--version":
+        case "-version":
+        case "-v":
+          optionsBuilder.version(true);
+          break;
+        case "--help":
+        case "-help":
+        case "-h":
+          optionsBuilder.help(true);
+          break;
+        case "--fix-imports-only":
+          optionsBuilder.fixImportsOnly(true);
+          break;
+        case "--skip-sorting-imports":
+          optionsBuilder.sortImports(false);
+          break;
+        case "--skip-removing-unused-imports":
+          optionsBuilder.removeUnusedImports(false);
+          break;
+        case "--skip-reflowing-long-strings":
+          optionsBuilder.reflowLongStrings(false);
+          break;
+        case "--skip-javadoc-formatting":
+          optionsBuilder.formatJavadoc(false);
+          break;
+        case "-":
+          optionsBuilder.stdin(true);
+          break;
+        case "-n":
+        case "--dry-run":
+          optionsBuilder.dryRun(true);
+          break;
+        case "--set-exit-if-changed":
+          optionsBuilder.setExitIfChanged(true);
+          break;
+        case "-assume-filename":
+        case "--assume-filename":
+          optionsBuilder.assumeFilename(getValue(flag, it, value));
+          break;
+        default:
+          throw new IllegalArgumentException("unexpected flag: " + flag);
+      }
+    }
+    return optionsBuilder.build();
+  }
+
+  private static Integer parseInteger(Iterator<String> it, String flag, String value) {
+    try {
+      return Integer.valueOf(getValue(flag, it, value));
+    } catch (NumberFormatException e) {
+      throw new IllegalArgumentException(
+          String.format("invalid integer value for %s: %s", flag, value), e);
+    }
+  }
+
+  private static String getValue(String flag, Iterator<String> it, String value) {
+    if (value != null) {
+      return value;
+    }
+    if (!it.hasNext()) {
+      throw new IllegalArgumentException("required value was not provided for: " + flag);
+    }
+    return it.next();
+  }
+
+  /**
+   * Parse multiple --lines flags, like {"1:12,14,20:36", "40:45,50"}. Multiple ranges can be given
+   * with multiple --lines flags or separated by commas. A single line can be set by a single
+   * number. Line numbers are {@code 1}-based, but are converted to the {@code 0}-based numbering
+   * used internally by google-java-format.
+   */
+  private static void parseRangeSet(ImmutableRangeSet.Builder<Integer> result, String ranges) {
+    for (String range : COMMA_SPLITTER.split(ranges)) {
+      result.add(parseRange(range));
+    }
+  }
+
+  /**
+   * Parse a range, as in "1:12" or "42". Line numbers provided are {@code 1}-based, but are
+   * converted here to {@code 0}-based.
+   */
+  private static Range<Integer> parseRange(String arg) {
+    List<String> args = COLON_SPLITTER.splitToList(arg);
+    switch (args.size()) {
+      case 1:
+        int line = Integer.parseInt(args.get(0)) - 1;
+        return Range.closedOpen(line, line + 1);
+      case 2:
+        int line0 = Integer.parseInt(args.get(0)) - 1;
+        int line1 = Integer.parseInt(args.get(1)) - 1;
+        return Range.closedOpen(line0, line1 + 1);
+      default:
+        throw new IllegalArgumentException(arg);
+    }
+  }
+
+  /**
+   * Pre-processes an argument list, expanding arguments of the form {@code @filename} by reading
+   * the content of the file and appending whitespace-delimited options to {@code arguments}.
+   */
+  private static void expandParamsFiles(Iterable<String> args, List<String> expanded) {
+    for (String arg : args) {
+      if (arg.isEmpty()) {
+        continue;
+      }
+      if (!arg.startsWith("@")) {
+        expanded.add(arg);
+      } else if (arg.startsWith("@@")) {
+        expanded.add(arg.substring(1));
+      } else {
+        Path path = Paths.get(arg.substring(1));
+        try {
+          String sequence = new String(Files.readAllBytes(path), UTF_8);
+          expandParamsFiles(ARG_SPLITTER.split(sequence), expanded);
+        } catch (IOException e) {
+          throw new UncheckedIOException(path + ": could not read file: " + e.getMessage(), e);
+        }
+      }
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/DimensionHelpers.java b/core/src/main/java/com/google/googlejavaformat/java/DimensionHelpers.java
new file mode 100644
index 0000000..4bd19be
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/DimensionHelpers.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2016 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 com.google.common.collect.ImmutableList;
+import com.sun.source.tree.AnnotatedTypeTree;
+import com.sun.source.tree.AnnotationTree;
+import com.sun.source.tree.ArrayTypeTree;
+import com.sun.source.tree.Tree;
+import com.sun.tools.javac.tree.JCTree;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.List;
+
+/**
+ * Utilities for working with array dimensions.
+ *
+ * <p>javac's parser does not preserve concrete syntax for mixed-notation arrays, so we have to
+ * re-lex the input to extra it.
+ *
+ * <p>For example, {@code int [] a;} cannot be distinguished from {@code int [] a [];} in the AST.
+ */
+class DimensionHelpers {
+
+  /** The array dimension specifiers (including any type annotations) associated with a type. */
+  static class TypeWithDims {
+    final Tree node;
+    final ImmutableList<List<AnnotationTree>> dims;
+
+    public TypeWithDims(Tree node, ImmutableList<List<AnnotationTree>> dims) {
+      this.node = node;
+      this.dims = dims;
+    }
+  }
+
+  enum SortedDims {
+    YES,
+    NO
+  }
+
+  /** Returns a (possibly re-ordered) {@link TypeWithDims} for the given type. */
+  static TypeWithDims extractDims(Tree node, SortedDims sorted) {
+    Deque<List<AnnotationTree>> builder = new ArrayDeque<>();
+    node = extractDims(builder, node);
+    Iterable<List<AnnotationTree>> dims;
+    if (sorted == SortedDims.YES) {
+      dims = reorderBySourcePosition(builder);
+    } else {
+      dims = builder;
+    }
+    return new TypeWithDims(node, ImmutableList.copyOf(dims));
+  }
+
+  /**
+   * Rotate the list of dimension specifiers until all dimensions with type annotations appear in
+   * source order.
+   *
+   * <p>javac reorders dimension specifiers in method declarations with mixed-array notation, which
+   * means that any type annotations don't appear in source order.
+   *
+   * <p>For example, the type of {@code int @A [] f() @B [] {}} is parsed as {@code @B [] @A []}.
+   *
+   * <p>This doesn't handle cases with un-annotated dimension specifiers, so the formatting logic
+   * checks the token stream to figure out which side of the method name they appear on.
+   */
+  private static Iterable<List<AnnotationTree>> reorderBySourcePosition(
+      Deque<List<AnnotationTree>> dims) {
+    int lastAnnotation = -1;
+    int lastPos = -1;
+    int idx = 0;
+    for (List<AnnotationTree> dim : dims) {
+      if (!dim.isEmpty()) {
+        int pos = ((JCTree) dim.get(0)).getStartPosition();
+        if (pos < lastPos) {
+          List<List<AnnotationTree>> list = new ArrayList<>(dims);
+          Collections.rotate(list, -(lastAnnotation + 1));
+          return list;
+        }
+        lastPos = pos;
+        lastAnnotation = idx;
+      }
+      idx++;
+    }
+    return dims;
+  }
+
+  /**
+   * Accumulates a flattened list of array dimensions specifiers with type annotations, and returns
+   * the base type.
+   *
+   * <p>Given {@code int @A @B [][] @C []}, adds {@code [[@A, @B], [@C]]} to dims and returns {@code
+   * int}.
+   */
+  private static Tree extractDims(Deque<List<AnnotationTree>> dims, Tree node) {
+    switch (node.getKind()) {
+      case ARRAY_TYPE:
+        return extractDims(dims, ((ArrayTypeTree) node).getType());
+      case ANNOTATED_TYPE:
+        AnnotatedTypeTree annotatedTypeTree = (AnnotatedTypeTree) node;
+        if (annotatedTypeTree.getUnderlyingType().getKind() != Tree.Kind.ARRAY_TYPE) {
+          return node;
+        }
+        node = extractDims(dims, annotatedTypeTree.getUnderlyingType());
+        dims.addFirst(ImmutableList.copyOf(annotatedTypeTree.getAnnotations()));
+        return node;
+      default:
+        return node;
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java b/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java
new file mode 100644
index 0000000..9d8ae41
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015 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 com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
+import java.util.concurrent.Callable;
+
+/**
+ * Encapsulates information about a file to be formatted, including which parts of the file to
+ * format.
+ */
+class FormatFileCallable implements Callable<String> {
+  private final String input;
+  private final CommandLineOptions parameters;
+  private final JavaFormatterOptions options;
+
+  public FormatFileCallable(
+      CommandLineOptions parameters, String input, JavaFormatterOptions options) {
+    this.input = input;
+    this.parameters = parameters;
+    this.options = options;
+  }
+
+  @Override
+  public String call() throws FormatterException {
+    if (parameters.fixImportsOnly()) {
+      return fixImports(input);
+    }
+
+    Formatter formatter = new Formatter(options);
+    String formatted = formatter.formatSource(input, characterRanges(input).asRanges());
+    formatted = fixImports(formatted);
+    if (parameters.reflowLongStrings()) {
+      formatted = StringWrapper.wrap(Formatter.MAX_LINE_LENGTH, formatted, formatter);
+    }
+    return formatted;
+  }
+
+  private String fixImports(String input) throws FormatterException {
+    if (parameters.removeUnusedImports()) {
+      input = RemoveUnusedImports.removeUnusedImports(input);
+    }
+    if (parameters.sortImports()) {
+      input = ImportOrderer.reorderImports(input, options.style());
+    }
+    return input;
+  }
+
+  private RangeSet<Integer> characterRanges(String input) {
+    final RangeSet<Integer> characterRanges = TreeRangeSet.create();
+
+    if (parameters.lines().isEmpty() && parameters.offsets().isEmpty()) {
+      characterRanges.add(Range.closedOpen(0, input.length()));
+      return characterRanges;
+    }
+
+    characterRanges.addAll(Formatter.lineRangesToCharRanges(input, parameters.lines()));
+
+    for (int i = 0; i < parameters.offsets().size(); i++) {
+      Integer length = parameters.lengths().get(i);
+      if (length == 0) {
+        // 0 stands for "format the line under the cursor"
+        length = 1;
+      }
+      characterRanges.add(
+          Range.closedOpen(parameters.offsets().get(i), parameters.offsets().get(i) + length));
+    }
+
+    return characterRanges;
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
new file mode 100644
index 0000000..3e97395
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2015 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.base.StandardSystemProperty.JAVA_CLASS_VERSION;
+import static com.google.common.base.StandardSystemProperty.JAVA_SPECIFICATION_VERSION;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
+import com.google.common.io.CharSink;
+import com.google.common.io.CharSource;
+import com.google.errorprone.annotations.Immutable;
+import com.google.googlejavaformat.Doc;
+import com.google.googlejavaformat.DocBuilder;
+import com.google.googlejavaformat.FormattingError;
+import com.google.googlejavaformat.Newlines;
+import com.google.googlejavaformat.Op;
+import com.google.googlejavaformat.OpsBuilder;
+import com.sun.tools.javac.file.JavacFileManager;
+import com.sun.tools.javac.parser.JavacParser;
+import com.sun.tools.javac.parser.ParserFactory;
+import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
+import com.sun.tools.javac.util.Context;
+import com.sun.tools.javac.util.Log;
+import com.sun.tools.javac.util.Options;
+import java.io.IOError;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.DiagnosticListener;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * This is google-java-format, a new Java formatter that follows the Google Java Style Guide quite
+ * precisely---to the letter and to the spirit.
+ *
+ * <p>This formatter uses the javac parser to generate an AST. Because the AST loses information
+ * about the non-tokens in the input (including newlines, comments, etc.), and even some tokens
+ * (e.g., optional commas or semicolons), this formatter lexes the input again and follows along in
+ * the resulting list of tokens. Its lexer splits all multi-character operators (like "&gt;&gt;")
+ * into multiple single-character operators. Each non-token is assigned to a token---non-tokens
+ * following a token on the same line go with that token; those following go with the next token---
+ * and there is a final EOF token to hold final comments.
+ *
+ * <p>The formatter walks the AST to generate a Greg Nelson/Derek Oppen-style list of formatting
+ * {@link Op}s [1--2] that then generates a structured {@link Doc}. Each AST node type has a visitor
+ * to emit a sequence of {@link Op}s for the node.
+ *
+ * <p>Some data-structure operations are easier in the list of {@link Op}s, while others become
+ * easier in the {@link Doc}. The {@link Op}s are walked to attach the comments. As the {@link Op}s
+ * are generated, missing input tokens are inserted and incorrect output tokens are dropped,
+ * ensuring that the output matches the input even in the face of formatter errors. Finally, the
+ * formatter walks the {@link Doc} to format it in the given width.
+ *
+ * <p>This formatter also produces data structures of which tokens and comments appear where on the
+ * input, and on the output, to help output a partial reformatting of a slightly edited input.
+ *
+ * <p>Instances of the formatter are immutable and thread-safe.
+ *
+ * <p>[1] Nelson, Greg, and John DeTreville. Personal communication.
+ *
+ * <p>[2] Oppen, Derek C. "Prettyprinting". ACM Transactions on Programming Languages and Systems,
+ * Volume 2 Issue 4, Oct. 1980, pp. 465–483.
+ */
+@Immutable
+public final class Formatter {
+
+  public static final int MAX_LINE_LENGTH = 100;
+
+  static final Range<Integer> EMPTY_RANGE = Range.closedOpen(-1, -1);
+
+  private final JavaFormatterOptions options;
+
+  /** A new Formatter instance with default options. */
+  public Formatter() {
+    this(JavaFormatterOptions.defaultOptions());
+  }
+
+  public Formatter(JavaFormatterOptions options) {
+    this.options = options;
+  }
+
+  /**
+   * Construct a {@code Formatter} given a Java compilation unit. Parses the code; builds a {@link
+   * JavaInput} and the corresponding {@link JavaOutput}.
+   *
+   * @param javaInput the input, a Java compilation unit
+   * @param javaOutput the {@link JavaOutput}
+   * @param options the {@link JavaFormatterOptions}
+   */
+  static void format(final JavaInput javaInput, JavaOutput javaOutput, JavaFormatterOptions options)
+      throws FormatterException {
+    Context context = new Context();
+    DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
+    context.put(DiagnosticListener.class, diagnostics);
+    Options.instance(context).put("allowStringFolding", "false");
+    Options.instance(context).put("--enable-preview", "true");
+    JCCompilationUnit unit;
+    JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8);
+    try {
+      fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
+    } catch (IOException e) {
+      // impossible
+      throw new IOError(e);
+    }
+    SimpleJavaFileObject source =
+        new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
+          @Override
+          public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+            return javaInput.getText();
+          }
+        };
+    Log.instance(context).useSource(source);
+    ParserFactory parserFactory = ParserFactory.instance(context);
+    JavacParser parser =
+        parserFactory.newParser(
+            javaInput.getText(),
+            /*keepDocComments=*/ true,
+            /*keepEndPos=*/ true,
+            /*keepLineMap=*/ true);
+    unit = parser.parseCompilationUnit();
+    unit.sourcefile = source;
+
+    javaInput.setCompilationUnit(unit);
+    Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics =
+        Iterables.filter(diagnostics.getDiagnostics(), Formatter::errorDiagnostic);
+    if (!Iterables.isEmpty(errorDiagnostics)) {
+      throw FormatterException.fromJavacDiagnostics(errorDiagnostics);
+    }
+    OpsBuilder builder = new OpsBuilder(javaInput, javaOutput);
+    // Output the compilation unit.
+    JavaInputAstVisitor visitor;
+    if (getMajor() >= 14) {
+      try {
+        visitor =
+            Class.forName("com.google.googlejavaformat.java.java14.Java14InputAstVisitor")
+                .asSubclass(JavaInputAstVisitor.class)
+                .getConstructor(OpsBuilder.class, int.class)
+                .newInstance(builder, options.indentationMultiplier());
+      } catch (ReflectiveOperationException e) {
+        throw new LinkageError(e.getMessage(), e);
+      }
+    } else {
+      visitor = new JavaInputAstVisitor(builder, options.indentationMultiplier());
+    }
+    visitor.scan(unit, null);
+    builder.sync(javaInput.getText().length());
+    builder.drain();
+    Doc doc = new DocBuilder().withOps(builder.build()).build();
+    doc.computeBreaks(javaOutput.getCommentsHelper(), MAX_LINE_LENGTH, new Doc.State(+0, 0));
+    doc.write(javaOutput);
+    javaOutput.flush();
+  }
+
+  // Runtime.Version was added in JDK 9, so use reflection to access it to preserve source
+  // compatibility with Java 8.
+  private static int getMajor() {
+    try {
+      Method versionMethod = Runtime.class.getMethod("version");
+      Object version = versionMethod.invoke(null);
+      return (int) version.getClass().getMethod("major").invoke(version);
+    } catch (Exception e) {
+      // continue below
+    }
+    int version = (int) Double.parseDouble(JAVA_CLASS_VERSION.value());
+    if (49 <= version && version <= 52) {
+      return version - (49 - 5);
+    }
+    throw new IllegalStateException("Unknown Java version: " + JAVA_SPECIFICATION_VERSION.value());
+  }
+
+  static boolean errorDiagnostic(Diagnostic<?> input) {
+    if (input.getKind() != Diagnostic.Kind.ERROR) {
+      return false;
+    }
+    switch (input.getCode()) {
+      case "compiler.err.invalid.meth.decl.ret.type.req":
+        // accept constructor-like method declarations that don't match the name of their
+        // enclosing class
+        return false;
+      default:
+        break;
+    }
+    return true;
+  }
+
+  /**
+   * Format the given input (a Java compilation unit) into the output stream.
+   *
+   * @throws FormatterException if the input cannot be parsed
+   */
+  public void formatSource(CharSource input, CharSink output)
+      throws FormatterException, IOException {
+    // TODO(cushon): proper support for streaming input/output. Input may
+    // not be feasible (parsing) but output should be easier.
+    output.write(formatSource(input.read()));
+  }
+
+  /**
+   * Format an input string (a Java compilation unit) into an output string.
+   *
+   * <p>Leaves import statements untouched.
+   *
+   * @param input the input string
+   * @return the output string
+   * @throws FormatterException if the input string cannot be parsed
+   */
+  public String formatSource(String input) throws FormatterException {
+    return formatSource(input, ImmutableList.of(Range.closedOpen(0, input.length())));
+  }
+
+  /**
+   * Formats an input string (a Java compilation unit) and fixes imports.
+   *
+   * <p>Fixing imports includes ordering, spacing, and removal of unused import statements.
+   *
+   * @param input the input string
+   * @return the output string
+   * @throws FormatterException if the input string cannot be parsed
+   * @see <a
+   *     href="https://google.github.io/styleguide/javaguide.html#s3.3.3-import-ordering-and-spacing">
+   *     Google Java Style Guide - 3.3.3 Import ordering and spacing</a>
+   */
+  public String formatSourceAndFixImports(String input) throws FormatterException {
+    input = ImportOrderer.reorderImports(input, options.style());
+    input = RemoveUnusedImports.removeUnusedImports(input);
+    String formatted = formatSource(input);
+    formatted = StringWrapper.wrap(formatted, this);
+    return formatted;
+  }
+
+  /**
+   * Format an input string (a Java compilation unit), for only the specified character ranges.
+   * These ranges are extended as necessary (e.g., to encompass whole lines).
+   *
+   * @param input the input string
+   * @param characterRanges the character ranges to be reformatted
+   * @return the output string
+   * @throws FormatterException if the input string cannot be parsed
+   */
+  public String formatSource(String input, Collection<Range<Integer>> characterRanges)
+      throws FormatterException {
+    return JavaOutput.applyReplacements(input, getFormatReplacements(input, characterRanges));
+  }
+
+  /**
+   * Emit a list of {@link Replacement}s to convert from input to output.
+   *
+   * @param input the input compilation unit
+   * @param characterRanges the character ranges to reformat
+   * @return a list of {@link Replacement}s, sorted from low index to high index, without overlaps
+   * @throws FormatterException if the input string cannot be parsed
+   */
+  public ImmutableList<Replacement> getFormatReplacements(
+      String input, Collection<Range<Integer>> characterRanges) throws FormatterException {
+    JavaInput javaInput = new JavaInput(input);
+
+    // TODO(cushon): this is only safe because the modifier ordering doesn't affect whitespace,
+    // and doesn't change the replacements that are output. This is not true in general for
+    // 'de-linting' changes (e.g. import ordering).
+    javaInput = ModifierOrderer.reorderModifiers(javaInput, characterRanges);
+
+    String lineSeparator = Newlines.guessLineSeparator(input);
+    JavaOutput javaOutput =
+        new JavaOutput(lineSeparator, javaInput, new JavaCommentsHelper(lineSeparator, options));
+    try {
+      format(javaInput, javaOutput, options);
+    } catch (FormattingError e) {
+      throw new FormatterException(e.diagnostics());
+    }
+    RangeSet<Integer> tokenRangeSet = javaInput.characterRangesToTokenRanges(characterRanges);
+    return javaOutput.getFormatReplacements(tokenRangeSet);
+  }
+
+  /**
+   * Converts zero-indexed, [closed, open) line ranges in the given source file to character ranges.
+   */
+  public static RangeSet<Integer> lineRangesToCharRanges(
+      String input, RangeSet<Integer> lineRanges) {
+    List<Integer> lines = new ArrayList<>();
+    Iterators.addAll(lines, Newlines.lineOffsetIterator(input));
+    lines.add(input.length() + 1);
+
+    final RangeSet<Integer> characterRanges = TreeRangeSet.create();
+    for (Range<Integer> lineRange :
+        lineRanges.subRangeSet(Range.closedOpen(0, lines.size() - 1)).asRanges()) {
+      int lineStart = lines.get(lineRange.lowerEndpoint());
+      // Exclude the trailing newline. This isn't strictly necessary, but handling blank lines
+      // as empty ranges is convenient.
+      int lineEnd = lines.get(lineRange.upperEndpoint()) - 1;
+      Range<Integer> range = Range.closedOpen(lineStart, lineEnd);
+      characterRanges.add(range);
+    }
+    return characterRanges;
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java b/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java
new file mode 100644
index 0000000..3ccb44a
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2015 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 java.util.Locale.ENGLISH;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.googlejavaformat.FormatterDiagnostic;
+import java.util.List;
+import javax.tools.Diagnostic;
+import javax.tools.JavaFileObject;
+
+/** Checked exception class for formatter errors. */
+public final class FormatterException extends Exception {
+
+  private ImmutableList<FormatterDiagnostic> diagnostics;
+
+  public FormatterException(String message) {
+    this(FormatterDiagnostic.create(message));
+  }
+
+  public FormatterException(FormatterDiagnostic diagnostic) {
+    this(ImmutableList.of(diagnostic));
+  }
+
+  public FormatterException(Iterable<FormatterDiagnostic> diagnostics) {
+    super(diagnostics.iterator().next().toString());
+    this.diagnostics = ImmutableList.copyOf(diagnostics);
+  }
+
+  public List<FormatterDiagnostic> diagnostics() {
+    return diagnostics;
+  }
+
+  public static FormatterException fromJavacDiagnostics(
+      Iterable<Diagnostic<? extends JavaFileObject>> diagnostics) {
+    return new FormatterException(Iterables.transform(diagnostics, d -> toFormatterDiagnostic(d)));
+  }
+
+  private static FormatterDiagnostic toFormatterDiagnostic(Diagnostic<?> input) {
+    return FormatterDiagnostic.create(
+        (int) input.getLineNumber(), (int) input.getColumnNumber(), input.getMessage(ENGLISH));
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template
new file mode 100644
index 0000000..88706fb
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2015 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;
+
+class GoogleJavaFormatVersion {
+
+  static String version() {
+    return "%VERSION%";
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java b/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java
new file mode 100644
index 0000000..a82715e
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java
@@ -0,0 +1,479 @@
+/*
+ * Copyright 2016 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.Iterables.getLast;
+import static com.google.common.primitives.Booleans.trueFirst;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.googlejavaformat.Newlines;
+import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
+import com.google.googlejavaformat.java.JavaInput.Tok;
+import com.sun.tools.javac.parser.Tokens.TokenKind;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.stream.Stream;
+
+/** Orders imports in Java source code. */
+public class ImportOrderer {
+
+  private static final Splitter DOT_SPLITTER = Splitter.on('.');
+
+  /**
+   * Reorder the inputs in {@code text}, a complete Java program. On success, another complete Java
+   * program is returned, which is the same as the original except the imports are in order.
+   *
+   * @throws FormatterException if the input could not be parsed.
+   */
+  public static String reorderImports(String text, Style style) throws FormatterException {
+    ImmutableList<Tok> toks = JavaInput.buildToks(text, CLASS_START);
+    return new ImportOrderer(text, toks, style).reorderImports();
+  }
+
+  /**
+   * Reorder the inputs in {@code text}, a complete Java program, in Google style. On success,
+   * another complete Java program is returned, which is the same as the original except the imports
+   * are in order.
+   *
+   * @deprecated Use {@link #reorderImports(String, Style)} instead
+   * @throws FormatterException if the input could not be parsed.
+   */
+  @Deprecated
+  public static String reorderImports(String text) throws FormatterException {
+    return reorderImports(text, Style.GOOGLE);
+  }
+
+  private String reorderImports() throws FormatterException {
+    int firstImportStart;
+    Optional<Integer> maybeFirstImport = findIdentifier(0, IMPORT_OR_CLASS_START);
+    if (!maybeFirstImport.isPresent() || !tokenAt(maybeFirstImport.get()).equals("import")) {
+      // No imports, so nothing to do.
+      return text;
+    }
+    firstImportStart = maybeFirstImport.get();
+    int unindentedFirstImportStart = unindent(firstImportStart);
+
+    ImportsAndIndex imports = scanImports(firstImportStart);
+    int afterLastImport = imports.index;
+
+    // Make sure there are no more imports before the next class (etc) definition.
+    Optional<Integer> maybeLaterImport = findIdentifier(afterLastImport, IMPORT_OR_CLASS_START);
+    if (maybeLaterImport.isPresent() && tokenAt(maybeLaterImport.get()).equals("import")) {
+      throw new FormatterException("Imports not contiguous (perhaps a comment separates them?)");
+    }
+
+    StringBuilder result = new StringBuilder();
+    String prefix = tokString(0, unindentedFirstImportStart);
+    result.append(prefix);
+    if (!prefix.isEmpty() && Newlines.getLineEnding(prefix) == null) {
+      result.append(lineSeparator).append(lineSeparator);
+    }
+    result.append(reorderedImportsString(imports.imports));
+
+    List<String> tail = new ArrayList<>();
+    tail.add(CharMatcher.whitespace().trimLeadingFrom(tokString(afterLastImport, toks.size())));
+    if (!toks.isEmpty()) {
+      Tok lastTok = getLast(toks);
+      int tailStart = lastTok.getPosition() + lastTok.length();
+      tail.add(text.substring(tailStart));
+    }
+    if (tail.stream().anyMatch(s -> !s.isEmpty())) {
+      result.append(lineSeparator);
+      tail.forEach(result::append);
+    }
+
+    return result.toString();
+  }
+
+  /**
+   * {@link TokenKind}s that indicate the start of a type definition. We use this to avoid scanning
+   * the whole file, since we know that imports must precede any type definition.
+   */
+  private static final ImmutableSet<TokenKind> CLASS_START =
+      ImmutableSet.of(TokenKind.CLASS, TokenKind.INTERFACE, TokenKind.ENUM);
+
+  /**
+   * We use this set to find the first import, and again to check that there are no imports after
+   * the place we stopped gathering them. An annotation definition ({@code @interface}) is two
+   * tokens, the second which is {@code interface}, so we don't need a separate entry for that.
+   */
+  private static final ImmutableSet<String> IMPORT_OR_CLASS_START =
+      ImmutableSet.of("import", "class", "interface", "enum");
+
+  /**
+   * A {@link Comparator} that orders {@link Import}s by Google Style, defined at
+   * https://google.github.io/styleguide/javaguide.html#s3.3.3-import-ordering-and-spacing.
+   */
+  private static final Comparator<Import> GOOGLE_IMPORT_COMPARATOR =
+      Comparator.comparing(Import::isStatic, trueFirst()).thenComparing(Import::imported);
+
+  /**
+   * A {@link Comparator} that orders {@link Import}s by AOSP Style, defined at
+   * https://source.android.com/setup/contribute/code-style#order-import-statements and implemented
+   * in IntelliJ at
+   * https://android.googlesource.com/platform/development/+/master/ide/intellij/codestyles/AndroidStyle.xml.
+   */
+  private static final Comparator<Import> AOSP_IMPORT_COMPARATOR =
+      Comparator.comparing(Import::isStatic, trueFirst())
+          .thenComparing(Import::isAndroid, trueFirst())
+          .thenComparing(Import::isThirdParty, trueFirst())
+          .thenComparing(Import::isJava, trueFirst())
+          .thenComparing(Import::imported);
+
+  /**
+   * Determines whether to insert a blank line between the {@code prev} and {@code curr} {@link
+   * Import}s based on Google style.
+   */
+  private static boolean shouldInsertBlankLineGoogle(Import prev, Import curr) {
+    return prev.isStatic() && !curr.isStatic();
+  }
+
+  /**
+   * Determines whether to insert a blank line between the {@code prev} and {@code curr} {@link
+   * Import}s based on AOSP style.
+   */
+  private static boolean shouldInsertBlankLineAosp(Import prev, Import curr) {
+    if (prev.isStatic() && !curr.isStatic()) {
+      return true;
+    }
+    // insert blank line between "com.android" from "com.anythingelse"
+    if (prev.isAndroid() && !curr.isAndroid()) {
+      return true;
+    }
+    return !prev.topLevel().equals(curr.topLevel());
+  }
+
+  private final String text;
+  private final ImmutableList<Tok> toks;
+  private final String lineSeparator;
+  private final Comparator<Import> importComparator;
+  private final BiFunction<Import, Import, Boolean> shouldInsertBlankLineFn;
+
+  private ImportOrderer(String text, ImmutableList<Tok> toks, Style style) {
+    this.text = text;
+    this.toks = toks;
+    this.lineSeparator = Newlines.guessLineSeparator(text);
+    if (style.equals(Style.GOOGLE)) {
+      this.importComparator = GOOGLE_IMPORT_COMPARATOR;
+      this.shouldInsertBlankLineFn = ImportOrderer::shouldInsertBlankLineGoogle;
+    } else if (style.equals(Style.AOSP)) {
+      this.importComparator = AOSP_IMPORT_COMPARATOR;
+      this.shouldInsertBlankLineFn = ImportOrderer::shouldInsertBlankLineAosp;
+    } else {
+      throw new IllegalArgumentException("Unsupported code style: " + style);
+    }
+  }
+
+  /** An import statement. */
+  class Import {
+    private final String imported;
+    private final boolean isStatic;
+    private final String trailing;
+
+    Import(String imported, String trailing, boolean isStatic) {
+      this.imported = imported;
+      this.trailing = trailing;
+      this.isStatic = isStatic;
+    }
+
+    /** The name being imported, for example {@code java.util.List}. */
+    String imported() {
+      return imported;
+    }
+
+    /** True if this is {@code import static}. */
+    boolean isStatic() {
+      return isStatic;
+    }
+
+    /** The top-level package of the import. */
+    String topLevel() {
+      return DOT_SPLITTER.split(imported()).iterator().next();
+    }
+
+    /** True if this is an Android import per AOSP style. */
+    boolean isAndroid() {
+      return Stream.of("android.", "androidx.", "dalvik.", "libcore.", "com.android.")
+          .anyMatch(imported::startsWith);
+    }
+
+    /** True if this is a Java import per AOSP style. */
+    boolean isJava() {
+      switch (topLevel()) {
+        case "java":
+        case "javax":
+          return true;
+        default:
+          return false;
+      }
+    }
+
+    /**
+     * The {@code //} comment lines after the final {@code ;}, up to and including the line
+     * terminator of the last one. Note: In case two imports were separated by a space (which is
+     * disallowed by the style guide), the trailing whitespace of the first import does not include
+     * a line terminator.
+     */
+    String trailing() {
+      return trailing;
+    }
+
+    /** True if this is a third-party import per AOSP style. */
+    public boolean isThirdParty() {
+      return !(isAndroid() || isJava());
+    }
+
+    // One or multiple lines, the import itself and following comments, including the line
+    // terminator.
+    @Override
+    public String toString() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("import ");
+      if (isStatic()) {
+        sb.append("static ");
+      }
+      sb.append(imported()).append(';');
+      if (trailing().trim().isEmpty()) {
+        sb.append(lineSeparator);
+      } else {
+        sb.append(trailing());
+      }
+      return sb.toString();
+    }
+  }
+
+  private String tokString(int start, int end) {
+    StringBuilder sb = new StringBuilder();
+    for (int i = start; i < end; i++) {
+      sb.append(toks.get(i).getOriginalText());
+    }
+    return sb.toString();
+  }
+
+  private static class ImportsAndIndex {
+    final ImmutableSortedSet<Import> imports;
+    final int index;
+
+    ImportsAndIndex(ImmutableSortedSet<Import> imports, int index) {
+      this.imports = imports;
+      this.index = index;
+    }
+  }
+
+  /**
+   * Scans a sequence of import lines. The parsing uses this approximate grammar:
+   *
+   * <pre>{@code
+   * <imports> -> (<end-of-line> | <import>)*
+   * <import> -> "import" <whitespace> ("static" <whitespace>)?
+   *    <identifier> ("." <identifier>)* ("." "*")? <whitespace>? ";"
+   *    <whitespace>? <end-of-line>? (<line-comment> <end-of-line>)*
+   * }</pre>
+   *
+   * @param i the index to start parsing at.
+   * @return the result of parsing the imports.
+   * @throws FormatterException if imports could not parsed according to the grammar.
+   */
+  private ImportsAndIndex scanImports(int i) throws FormatterException {
+    int afterLastImport = i;
+    ImmutableSortedSet.Builder<Import> imports = ImmutableSortedSet.orderedBy(importComparator);
+    // JavaInput.buildToks appends a zero-width EOF token after all tokens. It won't match any
+    // of our tests here and protects us from running off the end of the toks list. Since it is
+    // zero-width it doesn't matter if we include it in our string concatenation at the end.
+    while (i < toks.size() && tokenAt(i).equals("import")) {
+      i++;
+      if (isSpaceToken(i)) {
+        i++;
+      }
+      boolean isStatic = tokenAt(i).equals("static");
+      if (isStatic) {
+        i++;
+        if (isSpaceToken(i)) {
+          i++;
+        }
+      }
+      if (!isIdentifierToken(i)) {
+        throw new FormatterException("Unexpected token after import: " + tokenAt(i));
+      }
+      StringAndIndex imported = scanImported(i);
+      String importedName = imported.string;
+      i = imported.index;
+      if (isSpaceToken(i)) {
+        i++;
+      }
+      if (!tokenAt(i).equals(";")) {
+        throw new FormatterException("Expected ; after import");
+      }
+      while (tokenAt(i).equals(";")) {
+        // Extra semicolons are not allowed by the JLS but are accepted by javac.
+        i++;
+      }
+      StringBuilder trailing = new StringBuilder();
+      if (isSpaceToken(i)) {
+        trailing.append(tokenAt(i));
+        i++;
+      }
+      if (isNewlineToken(i)) {
+        trailing.append(tokenAt(i));
+        i++;
+      }
+      // Gather (if any) all single line comments and accompanied line terminators following this
+      // import
+      while (isSlashSlashCommentToken(i)) {
+        trailing.append(tokenAt(i));
+        i++;
+        if (isNewlineToken(i)) {
+          trailing.append(tokenAt(i));
+          i++;
+        }
+      }
+      imports.add(new Import(importedName, trailing.toString(), isStatic));
+      // Remember the position just after the import we just saw, before skipping blank lines.
+      // If the next thing after the blank lines is not another import then we don't want to
+      // include those blank lines in the text to be replaced.
+      afterLastImport = i;
+      while (isNewlineToken(i) || isSpaceToken(i)) {
+        i++;
+      }
+    }
+    return new ImportsAndIndex(imports.build(), afterLastImport);
+  }
+
+  // Produces the sorted output based on the imports we have scanned.
+  private String reorderedImportsString(ImmutableSortedSet<Import> imports) {
+    Preconditions.checkArgument(!imports.isEmpty(), "imports");
+
+    // Pretend that the first import was preceded by another import of the same kind, so we don't
+    // insert a newline there.
+    Import prevImport = imports.iterator().next();
+
+    StringBuilder sb = new StringBuilder();
+    for (Import currImport : imports) {
+      if (shouldInsertBlankLineFn.apply(prevImport, currImport)) {
+        // Blank line between static and non-static imports.
+        sb.append(lineSeparator);
+      }
+      sb.append(currImport);
+      prevImport = currImport;
+    }
+    return sb.toString();
+  }
+
+  private static class StringAndIndex {
+    private final String string;
+    private final int index;
+
+    StringAndIndex(String string, int index) {
+      this.string = string;
+      this.index = index;
+    }
+  }
+
+  /**
+   * Scans the imported thing, the dot-separated name that comes after import [static] and before
+   * the semicolon. We don't allow spaces inside the dot-separated name. Wildcard imports are
+   * supported: if the input is {@code import java.util.*;} then the returned string will be {@code
+   * java.util.*}.
+   *
+   * @param start the index of the start of the identifier. If the import is {@code import
+   *     java.util.List;} then this index points to the token {@code java}.
+   * @return the parsed import ({@code java.util.List} in the example) and the index of the first
+   *     token after the imported thing ({@code ;} in the example).
+   * @throws FormatterException if the imported name could not be parsed.
+   */
+  private StringAndIndex scanImported(int start) throws FormatterException {
+    int i = start;
+    StringBuilder imported = new StringBuilder();
+    // At the start of each iteration of this loop, i points to an identifier.
+    // On exit from the loop, i points to a token after an identifier or after *.
+    while (true) {
+      Preconditions.checkState(isIdentifierToken(i));
+      imported.append(tokenAt(i));
+      i++;
+      if (!tokenAt(i).equals(".")) {
+        return new StringAndIndex(imported.toString(), i);
+      }
+      imported.append('.');
+      i++;
+      if (tokenAt(i).equals("*")) {
+        imported.append('*');
+        return new StringAndIndex(imported.toString(), i + 1);
+      } else if (!isIdentifierToken(i)) {
+        throw new FormatterException("Could not parse imported name, at: " + tokenAt(i));
+      }
+    }
+  }
+
+  /**
+   * Returns the index of the first place where one of the given identifiers occurs, or {@code
+   * Optional.empty()} if there is none.
+   *
+   * @param start the index to start looking at
+   * @param identifiers the identifiers to look for
+   */
+  private Optional<Integer> findIdentifier(int start, ImmutableSet<String> identifiers) {
+    for (int i = start; i < toks.size(); i++) {
+      if (isIdentifierToken(i)) {
+        String id = tokenAt(i);
+        if (identifiers.contains(id)) {
+          return Optional.of(i);
+        }
+      }
+    }
+    return Optional.empty();
+  }
+
+  /** Returns the given token, or the preceding token if it is a whitespace token. */
+  private int unindent(int i) {
+    if (i > 0 && isSpaceToken(i - 1)) {
+      return i - 1;
+    } else {
+      return i;
+    }
+  }
+
+  private String tokenAt(int i) {
+    return toks.get(i).getOriginalText();
+  }
+
+  private boolean isIdentifierToken(int i) {
+    String s = tokenAt(i);
+    return !s.isEmpty() && Character.isJavaIdentifierStart(s.codePointAt(0));
+  }
+
+  private boolean isSpaceToken(int i) {
+    String s = tokenAt(i);
+    if (s.isEmpty()) {
+      return false;
+    } else {
+      return " \t\f".indexOf(s.codePointAt(0)) >= 0;
+    }
+  }
+
+  private boolean isSlashSlashCommentToken(int i) {
+    return toks.get(i).isSlashSlashComment();
+  }
+
+  private boolean isNewlineToken(int i) {
+    return toks.get(i).isNewline();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java b/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java
new file mode 100644
index 0000000..346324a
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2015 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 com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
+import com.google.googlejavaformat.CommentsHelper;
+import com.google.googlejavaformat.Input.Tok;
+import com.google.googlejavaformat.Newlines;
+import com.google.googlejavaformat.java.javadoc.JavadocFormatter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** {@code JavaCommentsHelper} extends {@link CommentsHelper} to rewrite Java comments. */
+public final class JavaCommentsHelper implements CommentsHelper {
+
+  private final String lineSeparator;
+  private final JavaFormatterOptions options;
+
+  public JavaCommentsHelper(String lineSeparator, JavaFormatterOptions options) {
+    this.lineSeparator = lineSeparator;
+    this.options = options;
+  }
+
+  @Override
+  public String rewrite(Tok tok, int maxWidth, int column0) {
+    if (!tok.isComment()) {
+      return tok.getOriginalText();
+    }
+    String text = tok.getOriginalText();
+    if (tok.isJavadocComment() && options.formatJavadoc()) {
+      text = JavadocFormatter.formatJavadoc(text, column0);
+    }
+    List<String> lines = new ArrayList<>();
+    Iterator<String> it = Newlines.lineIterator(text);
+    while (it.hasNext()) {
+      lines.add(CharMatcher.whitespace().trimTrailingFrom(it.next()));
+    }
+    if (tok.isSlashSlashComment()) {
+      return indentLineComments(lines, column0);
+    } else if (javadocShaped(lines)) {
+      return indentJavadoc(lines, column0);
+    } else {
+      return preserveIndentation(lines, column0);
+    }
+  }
+
+  // For non-javadoc-shaped block comments, shift the entire block to the correct
+  // column, but do not adjust relative indentation.
+  private String preserveIndentation(List<String> lines, int column0) {
+    StringBuilder builder = new StringBuilder();
+
+    // find the leftmost non-whitespace character in all trailing lines
+    int startCol = -1;
+    for (int i = 1; i < lines.size(); i++) {
+      int lineIdx = CharMatcher.whitespace().negate().indexIn(lines.get(i));
+      if (lineIdx >= 0 && (startCol == -1 || lineIdx < startCol)) {
+        startCol = lineIdx;
+      }
+    }
+
+    // output the first line at the current column
+    builder.append(lines.get(0));
+
+    // output all trailing lines with plausible indentation
+    for (int i = 1; i < lines.size(); ++i) {
+      builder.append(lineSeparator).append(Strings.repeat(" ", column0));
+      // check that startCol is valid index, e.g. for blank lines
+      if (lines.get(i).length() >= startCol) {
+        builder.append(lines.get(i).substring(startCol));
+      } else {
+        builder.append(lines.get(i));
+      }
+    }
+    return builder.toString();
+  }
+
+  // Wraps and re-indents line comments.
+  private String indentLineComments(List<String> lines, int column0) {
+    lines = wrapLineComments(lines, column0);
+    StringBuilder builder = new StringBuilder();
+    builder.append(lines.get(0).trim());
+    String indentString = Strings.repeat(" ", column0);
+    for (int i = 1; i < lines.size(); ++i) {
+      builder.append(lineSeparator).append(indentString).append(lines.get(i).trim());
+    }
+    return builder.toString();
+  }
+
+  // Preserve special `//noinspection` and `//$NON-NLS-x$` comments used by IDEs, which cannot
+  // contain leading spaces.
+  private static final Pattern LINE_COMMENT_MISSING_SPACE_PREFIX =
+      Pattern.compile("^(//+)(?!noinspection|\\$NON-NLS-\\d+\\$)[^\\s/]");
+
+  private List<String> wrapLineComments(List<String> lines, int column0) {
+    List<String> result = new ArrayList<>();
+    for (String line : lines) {
+      // Add missing leading spaces to line comments: `//foo` -> `// foo`.
+      Matcher matcher = LINE_COMMENT_MISSING_SPACE_PREFIX.matcher(line);
+      if (matcher.find()) {
+        int length = matcher.group(1).length();
+        line = Strings.repeat("/", length) + " " + line.substring(length);
+      }
+      if (line.startsWith("// MOE:")) {
+        // don't wrap comments for https://github.com/google/MOE
+        result.add(line);
+        continue;
+      }
+      while (line.length() + column0 > Formatter.MAX_LINE_LENGTH) {
+        int idx = Formatter.MAX_LINE_LENGTH - column0;
+        // only break on whitespace characters, and ignore the leading `// `
+        while (idx >= 2 && !CharMatcher.whitespace().matches(line.charAt(idx))) {
+          idx--;
+        }
+        if (idx <= 2) {
+          break;
+        }
+        result.add(line.substring(0, idx));
+        line = "//" + line.substring(idx);
+      }
+      result.add(line);
+    }
+    return result;
+  }
+
+  // Remove leading whitespace (trailing was already removed), and re-indent.
+  // Add a +1 indent before '*', and add the '*' if necessary.
+  private String indentJavadoc(List<String> lines, int column0) {
+    StringBuilder builder = new StringBuilder();
+    builder.append(lines.get(0).trim());
+    int indent = column0 + 1;
+    String indentString = Strings.repeat(" ", indent);
+    for (int i = 1; i < lines.size(); ++i) {
+      builder.append(lineSeparator).append(indentString);
+      String line = lines.get(i).trim();
+      if (!line.startsWith("*")) {
+        builder.append("* ");
+      }
+      builder.append(line);
+    }
+    return builder.toString();
+  }
+
+  // Returns true if the comment looks like javadoc
+  private static boolean javadocShaped(List<String> lines) {
+    Iterator<String> it = lines.iterator();
+    if (!it.hasNext()) {
+      return false;
+    }
+    String first = it.next().trim();
+    // if it's actually javadoc, we're done
+    if (first.startsWith("/**")) {
+      return true;
+    }
+    // if it's a block comment, check all trailing lines for '*'
+    if (!first.startsWith("/*")) {
+      return false;
+    }
+    while (it.hasNext()) {
+      if (!it.next().trim().startsWith("*")) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
+
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java b/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java
new file mode 100644
index 0000000..4d3d30d
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2016 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 com.google.errorprone.annotations.Immutable;
+
+/**
+ * Options for a google-java-format invocation.
+ *
+ * <p>Like gofmt, the google-java-format CLI exposes <em>no</em> configuration options (aside from
+ * {@code --aosp}).
+ *
+ * <p>The goal of google-java-format is to provide consistent formatting, and to free developers
+ * from arguments over style choices. It is an explicit non-goal to support developers' individual
+ * preferences, and in fact it would work directly against our primary goals.
+ */
+@Immutable
+public class JavaFormatterOptions {
+
+  public enum Style {
+    /** The default Google Java Style configuration. */
+    GOOGLE(1),
+
+    /** The AOSP-compliant configuration. */
+    AOSP(2);
+
+    private final int indentationMultiplier;
+
+    Style(int indentationMultiplier) {
+      this.indentationMultiplier = indentationMultiplier;
+    }
+
+    int indentationMultiplier() {
+      return indentationMultiplier;
+    }
+  }
+
+  private final Style style;
+  private final boolean formatJavadoc;
+
+  private JavaFormatterOptions(Style style, boolean formatJavadoc) {
+    this.style = style;
+    this.formatJavadoc = formatJavadoc;
+  }
+
+  /** Returns the multiplier for the unit of indent. */
+  public int indentationMultiplier() {
+    return style.indentationMultiplier();
+  }
+
+  boolean formatJavadoc() {
+    return formatJavadoc;
+  }
+
+  /** Returns the code style. */
+  public Style style() {
+    return style;
+  }
+
+  /** Returns the default formatting options. */
+  public static JavaFormatterOptions defaultOptions() {
+    return builder().build();
+  }
+
+  /** Returns a builder for {@link JavaFormatterOptions}. */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** A builder for {@link JavaFormatterOptions}. */
+  public static class Builder {
+    private Style style = Style.GOOGLE;
+    private boolean formatJavadoc = true;
+
+    private Builder() {}
+
+    public Builder style(Style style) {
+      this.style = style;
+      return this;
+    }
+
+    Builder formatJavadoc(boolean formatJavadoc) {
+      this.formatJavadoc = formatJavadoc;
+      return this;
+    }
+
+    public JavaFormatterOptions build() {
+      return new JavaFormatterOptions(style, formatJavadoc);
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java b/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java
new file mode 100644
index 0000000..999c8fb
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java
@@ -0,0 +1,676 @@
+/*
+ * Copyright 2015 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.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Iterables.getLast;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Verify;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableRangeMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
+import com.google.googlejavaformat.Input;
+import com.google.googlejavaformat.Newlines;
+import com.google.googlejavaformat.java.JavacTokens.RawTok;
+import com.sun.tools.javac.file.JavacFileManager;
+import com.sun.tools.javac.parser.Tokens.TokenKind;
+import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
+import com.sun.tools.javac.util.Context;
+import com.sun.tools.javac.util.Log;
+import com.sun.tools.javac.util.Log.DeferredDiagnosticHandler;
+import com.sun.tools.javac.util.Options;
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.DiagnosticListener;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import javax.tools.SimpleJavaFileObject;
+
+/** {@code JavaInput} extends {@link Input} to represent a Java input document. */
+public final class JavaInput extends Input {
+  /**
+   * A {@code JavaInput} is a sequence of {@link Tok}s that cover the Java input. A {@link Tok} is
+   * either a token (if {@code isToken()}), or a non-token, which is a comment (if {@code
+   * isComment()}) or a newline (if {@code isNewline()}) or a maximal sequence of other whitespace
+   * characters (if {@code isSpaces()}). Each {@link Tok} contains a sequence of characters, an
+   * index (sequential starting at {@code 0} for tokens and comments, else {@code -1}), and a
+   * ({@code 0}-origin) position in the input. The concatenation of the texts of all the {@link
+   * Tok}s equals the input. Each Input ends with a token EOF {@link Tok}, with empty text.
+   *
+   * <p>A {@code /*} comment possibly contains newlines; a {@code //} comment does not contain the
+   * terminating newline character, but is followed by a newline {@link Tok}.
+   */
+  static final class Tok implements Input.Tok {
+    private final int index;
+    private final String originalText;
+    private final String text;
+    private final int position;
+    private final int columnI;
+    private final boolean isToken;
+    private final TokenKind kind;
+
+    /**
+     * The {@code Tok} constructor.
+     *
+     * @param index its index
+     * @param originalText its original text, before removing Unicode escapes
+     * @param text its text after removing Unicode escapes
+     * @param position its {@code 0}-origin position in the input
+     * @param columnI its {@code 0}-origin column number in the input
+     * @param isToken whether the {@code Tok} is a token
+     * @param kind the token kind
+     */
+    Tok(
+        int index,
+        String originalText,
+        String text,
+        int position,
+        int columnI,
+        boolean isToken,
+        TokenKind kind) {
+      this.index = index;
+      this.originalText = originalText;
+      this.text = text;
+      this.position = position;
+      this.columnI = columnI;
+      this.isToken = isToken;
+      this.kind = kind;
+    }
+
+    @Override
+    public int getIndex() {
+      return index;
+    }
+
+    @Override
+    public String getText() {
+      return text;
+    }
+
+    @Override
+    public String getOriginalText() {
+      return originalText;
+    }
+
+    @Override
+    public int length() {
+      return originalText.length();
+    }
+
+    @Override
+    public int getPosition() {
+      return position;
+    }
+
+    @Override
+    public int getColumn() {
+      return columnI;
+    }
+
+    boolean isToken() {
+      return isToken;
+    }
+
+    @Override
+    public boolean isNewline() {
+      return Newlines.isNewline(text);
+    }
+
+    @Override
+    public boolean isSlashSlashComment() {
+      return text.startsWith("//");
+    }
+
+    @Override
+    public boolean isSlashStarComment() {
+      return text.startsWith("/*");
+    }
+
+    @Override
+    public boolean isJavadocComment() {
+      // comments like `/***` are also javadoc, but their formatting probably won't be improved
+      // by the javadoc formatter
+      return text.startsWith("/**") && text.charAt("/**".length()) != '*' && text.length() > 4;
+    }
+
+    @Override
+    public boolean isComment() {
+      return isSlashSlashComment() || isSlashStarComment();
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("index", index)
+          .add("text", text)
+          .add("position", position)
+          .add("columnI", columnI)
+          .add("isToken", isToken)
+          .toString();
+    }
+
+    public TokenKind kind() {
+      return kind;
+    }
+  }
+
+  /**
+   * A {@link Token} contains a token {@link Tok} and its associated non-tokens; each non-token
+   * {@link Tok} belongs to one {@link Token}. Each {@link Token} has an immutable list of its
+   * non-tokens that appear before it, and another list of its non-tokens that appear after it. The
+   * concatenation of the texts of all the {@link Token}s' {@link Tok}s, each preceded by the texts
+   * of its {@code toksBefore} and followed by the texts of its {@code toksAfter}, equals the input.
+   */
+  static final class Token implements Input.Token {
+    private final Tok tok;
+    private final ImmutableList<Tok> toksBefore;
+    private final ImmutableList<Tok> toksAfter;
+
+    /**
+     * Token constructor.
+     *
+     * @param toksBefore the earlier non-token {link Tok}s assigned to this {@code Token}
+     * @param tok this token {@link Tok}
+     * @param toksAfter the later non-token {link Tok}s assigned to this {@code Token}
+     */
+    Token(List<Tok> toksBefore, Tok tok, List<Tok> toksAfter) {
+      this.toksBefore = ImmutableList.copyOf(toksBefore);
+      this.tok = tok;
+      this.toksAfter = ImmutableList.copyOf(toksAfter);
+    }
+
+    /**
+     * Get the token's {@link Tok}.
+     *
+     * @return the token's {@link Tok}
+     */
+    @Override
+    public Tok getTok() {
+      return tok;
+    }
+
+    /**
+     * Get the earlier {@link Tok}s assigned to this {@code Token}.
+     *
+     * @return the earlier {@link Tok}s assigned to this {@code Token}
+     */
+    @Override
+    public ImmutableList<? extends Input.Tok> getToksBefore() {
+      return toksBefore;
+    }
+
+    /**
+     * Get the later {@link Tok}s assigned to this {@code Token}.
+     *
+     * @return the later {@link Tok}s assigned to this {@code Token}
+     */
+    @Override
+    public ImmutableList<? extends Input.Tok> getToksAfter() {
+      return toksAfter;
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("tok", tok)
+          .add("toksBefore", toksBefore)
+          .add("toksAfter", toksAfter)
+          .toString();
+    }
+  }
+
+  private final String text; // The input.
+  private int kN; // The number of numbered toks (tokens or comments), excluding the EOF.
+
+  /*
+   * The following lists record the sequential indices of the {@code Tok}s on each input line. (Only
+   * tokens and comments have sequential indices.) Tokens and {@code //} comments lie on just one
+   * line; {@code /*} comments can lie on multiple lines. These data structures (along with
+   * equivalent ones for the formatted output) let us compute correspondences between the input and
+   * output.
+   */
+
+  private final ImmutableMap<Integer, Integer> positionToColumnMap; // Map Tok position to column.
+  private final ImmutableList<Token> tokens; // The Tokens for this input.
+  private final ImmutableRangeMap<Integer, Token> positionTokenMap; // Map position to Token.
+
+  /** Map from Tok index to the associated Token. */
+  private final Token[] kToToken;
+
+  /**
+   * Input constructor.
+   *
+   * @param text the input text
+   * @throws FormatterException if the input cannot be parsed
+   */
+  public JavaInput(String text) throws FormatterException {
+    this.text = checkNotNull(text);
+    setLines(ImmutableList.copyOf(Newlines.lineIterator(text)));
+    ImmutableList<Tok> toks = buildToks(text);
+    positionToColumnMap = makePositionToColumnMap(toks);
+    tokens = buildTokens(toks);
+    ImmutableRangeMap.Builder<Integer, Token> tokenLocations = ImmutableRangeMap.builder();
+    for (Token token : tokens) {
+      Input.Tok end = JavaOutput.endTok(token);
+      int upper = end.getPosition();
+      if (!end.getText().isEmpty()) {
+        upper += end.length() - 1;
+      }
+      tokenLocations.put(Range.closed(JavaOutput.startTok(token).getPosition(), upper), token);
+    }
+    positionTokenMap = tokenLocations.build();
+
+    // adjust kN for EOF
+    kToToken = new Token[kN + 1];
+    for (Token token : tokens) {
+      for (Input.Tok tok : token.getToksBefore()) {
+        if (tok.getIndex() < 0) {
+          continue;
+        }
+        kToToken[tok.getIndex()] = token;
+      }
+      kToToken[token.getTok().getIndex()] = token;
+      for (Input.Tok tok : token.getToksAfter()) {
+        if (tok.getIndex() < 0) {
+          continue;
+        }
+        kToToken[tok.getIndex()] = token;
+      }
+    }
+  }
+
+  private static ImmutableMap<Integer, Integer> makePositionToColumnMap(List<Tok> toks) {
+    ImmutableMap.Builder<Integer, Integer> builder = ImmutableMap.builder();
+    for (Tok tok : toks) {
+      builder.put(tok.getPosition(), tok.getColumn());
+    }
+    return builder.build();
+  }
+
+  /**
+   * Get the input text.
+   *
+   * @return the input text
+   */
+  @Override
+  public String getText() {
+    return text;
+  }
+
+  @Override
+  public ImmutableMap<Integer, Integer> getPositionToColumnMap() {
+    return positionToColumnMap;
+  }
+
+  /** Lex the input and build the list of toks. */
+  private ImmutableList<Tok> buildToks(String text) throws FormatterException {
+    ImmutableList<Tok> toks = buildToks(text, ImmutableSet.of());
+    kN = getLast(toks).getIndex();
+    computeRanges(toks);
+    return toks;
+  }
+
+  /**
+   * Lex the input and build the list of toks.
+   *
+   * @param text the text to be lexed.
+   * @param stopTokens a set of tokens which should cause lexing to stop. If one of these is found,
+   *     the returned list will include tokens up to but not including that token.
+   */
+  static ImmutableList<Tok> buildToks(String text, ImmutableSet<TokenKind> stopTokens)
+      throws FormatterException {
+    stopTokens = ImmutableSet.<TokenKind>builder().addAll(stopTokens).add(TokenKind.EOF).build();
+    Context context = new Context();
+    Options.instance(context).put("--enable-preview", "true");
+    new JavacFileManager(context, true, UTF_8);
+    DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
+    context.put(DiagnosticListener.class, diagnosticCollector);
+    Log log = Log.instance(context);
+    log.useSource(
+        new SimpleJavaFileObject(URI.create("Source.java"), Kind.SOURCE) {
+          @Override
+          public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+            return text;
+          }
+        });
+    DeferredDiagnosticHandler diagnostics = new DeferredDiagnosticHandler(log);
+    ImmutableList<RawTok> rawToks = JavacTokens.getTokens(text, context, stopTokens);
+    if (diagnostics.getDiagnostics().stream().anyMatch(d -> d.getKind() == Diagnostic.Kind.ERROR)) {
+      return ImmutableList.of(new Tok(0, "", "", 0, 0, true, null)); // EOF
+    }
+    int kN = 0;
+    List<Tok> toks = new ArrayList<>();
+    int charI = 0;
+    int columnI = 0;
+    for (RawTok t : rawToks) {
+      if (stopTokens.contains(t.kind())) {
+        break;
+      }
+      int charI0 = t.pos();
+      // Get string, possibly with Unicode escapes.
+      String originalTokText = text.substring(charI0, t.endPos());
+      String tokText =
+          t.kind() == TokenKind.STRINGLITERAL
+              ? t.stringVal() // Unicode escapes removed.
+              : originalTokText;
+      char tokText0 = tokText.charAt(0); // The token's first character.
+      final boolean isToken; // Is this tok a token?
+      final boolean isNumbered; // Is this tok numbered? (tokens and comments)
+      String extraNewline = null; // Extra newline at end?
+      List<String> strings = new ArrayList<>();
+      if (Character.isWhitespace(tokText0)) {
+        isToken = false;
+        isNumbered = false;
+        Iterator<String> it = Newlines.lineIterator(originalTokText);
+        while (it.hasNext()) {
+          String line = it.next();
+          String newline = Newlines.getLineEnding(line);
+          if (newline != null) {
+            String spaces = line.substring(0, line.length() - newline.length());
+            if (!spaces.isEmpty()) {
+              strings.add(spaces);
+            }
+            strings.add(newline);
+          } else if (!line.isEmpty()) {
+            strings.add(line);
+          }
+        }
+      } else if (tokText.startsWith("'") || tokText.startsWith("\"")) {
+        isToken = true;
+        isNumbered = true;
+        strings.add(originalTokText);
+      } else if (tokText.startsWith("//") || tokText.startsWith("/*")) {
+        // For compatibility with an earlier lexer, the newline after a // comment is its own tok.
+        if (tokText.startsWith("//")
+            && (originalTokText.endsWith("\n") || originalTokText.endsWith("\r"))) {
+          extraNewline = Newlines.getLineEnding(originalTokText);
+          tokText = tokText.substring(0, tokText.length() - extraNewline.length());
+          originalTokText =
+              originalTokText.substring(0, originalTokText.length() - extraNewline.length());
+        }
+        isToken = false;
+        isNumbered = true;
+        strings.add(originalTokText);
+      } else if (Character.isJavaIdentifierStart(tokText0)
+          || Character.isDigit(tokText0)
+          || (tokText0 == '.' && tokText.length() > 1 && Character.isDigit(tokText.charAt(1)))) {
+        // Identifier, keyword, or numeric literal (a dot may begin a number, as in .2D).
+        isToken = true;
+        isNumbered = true;
+        strings.add(tokText);
+      } else {
+        // Other tokens ("+" or "++" or ">>" are broken into one-character toks, because ">>"
+        // cannot be lexed without syntactic knowledge. This implementation fails if the token
+        // contains Unicode escapes.
+        isToken = true;
+        isNumbered = true;
+        for (char c : tokText.toCharArray()) {
+          strings.add(String.valueOf(c));
+        }
+      }
+      if (strings.size() == 1) {
+        toks.add(
+            new Tok(
+                isNumbered ? kN++ : -1,
+                originalTokText,
+                tokText,
+                charI,
+                columnI,
+                isToken,
+                t.kind()));
+        charI += originalTokText.length();
+        columnI = updateColumn(columnI, originalTokText);
+
+      } else {
+        if (strings.size() != 1 && !tokText.equals(originalTokText)) {
+          throw new FormatterException(
+              "Unicode escapes not allowed in whitespace or multi-character operators");
+        }
+        for (String str : strings) {
+          toks.add(new Tok(isNumbered ? kN++ : -1, str, str, charI, columnI, isToken, null));
+          charI += str.length();
+          columnI = updateColumn(columnI, originalTokText);
+        }
+      }
+      if (extraNewline != null) {
+        toks.add(new Tok(-1, extraNewline, extraNewline, charI, columnI, false, null));
+        columnI = 0;
+        charI += extraNewline.length();
+      }
+    }
+    toks.add(new Tok(kN, "", "", charI, columnI, true, null)); // EOF tok.
+    return ImmutableList.copyOf(toks);
+  }
+
+  private static int updateColumn(int columnI, String originalTokText) {
+    Integer last = Iterators.getLast(Newlines.lineOffsetIterator(originalTokText));
+    if (last > 0) {
+      columnI = originalTokText.length() - last;
+    } else {
+      columnI += originalTokText.length();
+    }
+    return columnI;
+  }
+
+  private static ImmutableList<Token> buildTokens(List<Tok> toks) {
+    ImmutableList.Builder<Token> tokens = ImmutableList.builder();
+    int k = 0;
+    int kN = toks.size();
+
+    // Remaining non-tokens before the token go here.
+    ImmutableList.Builder<Tok> toksBefore = ImmutableList.builder();
+
+    OUTERMOST:
+    while (k < kN) {
+      while (!toks.get(k).isToken()) {
+        Tok tok = toks.get(k++);
+        toksBefore.add(tok);
+        if (isParamComment(tok)) {
+          while (toks.get(k).isNewline()) {
+            // drop newlines after parameter comments
+            k++;
+          }
+        }
+      }
+      Tok tok = toks.get(k++);
+
+      // Non-tokens starting on the same line go here too.
+      ImmutableList.Builder<Tok> toksAfter = ImmutableList.builder();
+      OUTER:
+      while (k < kN && !toks.get(k).isToken()) {
+        // Don't attach inline comments to certain leading tokens, e.g. for `f(/*flag1=*/true).
+        //
+        // Attaching inline comments to the right token is hard, and this barely
+        // scratches the surface. But it's enough to do a better job with parameter
+        // name comments.
+        //
+        // TODO(cushon): find a better strategy.
+        if (toks.get(k).isSlashStarComment()) {
+          switch (tok.getText()) {
+            case "(":
+            case "<":
+            case ".":
+              break OUTER;
+            default:
+              break;
+          }
+        }
+        if (toks.get(k).isJavadocComment()) {
+          switch (tok.getText()) {
+            case ";":
+              break OUTER;
+            default:
+              break;
+          }
+        }
+        if (isParamComment(toks.get(k))) {
+          tokens.add(new Token(toksBefore.build(), tok, toksAfter.build()));
+          toksBefore = ImmutableList.<Tok>builder().add(toks.get(k++));
+          // drop newlines after parameter comments
+          while (toks.get(k).isNewline()) {
+            k++;
+          }
+          continue OUTERMOST;
+        }
+        Tok nonTokenAfter = toks.get(k++);
+        toksAfter.add(nonTokenAfter);
+        if (Newlines.containsBreaks(nonTokenAfter.getText())) {
+          break;
+        }
+      }
+      tokens.add(new Token(toksBefore.build(), tok, toksAfter.build()));
+      toksBefore = ImmutableList.builder();
+    }
+    return tokens.build();
+  }
+
+  private static boolean isParamComment(Tok tok) {
+    return tok.isSlashStarComment()
+        && tok.getText().matches("\\/\\*[A-Za-z0-9\\s_\\-]+=\\s*\\*\\/");
+  }
+
+  /**
+   * Convert from an offset and length flag pair to a token range.
+   *
+   * @param offset the {@code 0}-based offset in characters
+   * @param length the length in characters
+   * @return the {@code 0}-based {@link Range} of tokens
+   * @throws FormatterException if offset + length is outside the file
+   */
+  Range<Integer> characterRangeToTokenRange(int offset, int length) throws FormatterException {
+    int requiredLength = offset + length;
+    if (requiredLength > text.length()) {
+      throw new FormatterException(
+          String.format(
+              "error: invalid length %d, offset + length (%d) is outside the file",
+              length, requiredLength));
+    }
+    if (length < 0) {
+      return EMPTY_RANGE;
+    }
+    if (length == 0) {
+      // 0 stands for "format the line under the cursor"
+      length = 1;
+    }
+    ImmutableCollection<Token> enclosed =
+        getPositionTokenMap()
+            .subRangeMap(Range.closedOpen(offset, offset + length))
+            .asMapOfRanges()
+            .values();
+    if (enclosed.isEmpty()) {
+      return EMPTY_RANGE;
+    }
+    return Range.closedOpen(
+        enclosed.iterator().next().getTok().getIndex(), getLast(enclosed).getTok().getIndex() + 1);
+  }
+
+  /**
+   * Get the number of toks.
+   *
+   * @return the number of toks, including the EOF tok
+   */
+  @Override
+  public int getkN() {
+    return kN;
+  }
+
+  /**
+   * Get the Token by index.
+   *
+   * @param k the token index
+   */
+  @Override
+  public Token getToken(int k) {
+    return kToToken[k];
+  }
+
+  /**
+   * Get the input tokens.
+   *
+   * @return the input tokens
+   */
+  @Override
+  public ImmutableList<? extends Input.Token> getTokens() {
+    return tokens;
+  }
+
+  /**
+   * Get the navigable map from position to {@link Token}. Used to look for tokens following a given
+   * one, and to implement the --offset and --length flags to reformat a character range in the
+   * input file.
+   *
+   * @return the navigable map from position to {@link Token}
+   */
+  @Override
+  public ImmutableRangeMap<Integer, Token> getPositionTokenMap() {
+    return positionTokenMap;
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this)
+        .add("tokens", tokens)
+        .add("super", super.toString())
+        .toString();
+  }
+
+  private JCCompilationUnit unit;
+
+  @Override
+  public int getLineNumber(int inputPosition) {
+    Verify.verifyNotNull(unit, "Expected compilation unit to be set.");
+    return unit.getLineMap().getLineNumber(inputPosition);
+  }
+
+  @Override
+  public int getColumnNumber(int inputPosition) {
+    Verify.verifyNotNull(unit, "Expected compilation unit to be set.");
+    return unit.getLineMap().getColumnNumber(inputPosition);
+  }
+
+  // TODO(cushon): refactor JavaInput so the CompilationUnit can be passed into
+  // the constructor.
+  public void setCompilationUnit(JCCompilationUnit unit) {
+    this.unit = unit;
+  }
+
+  public RangeSet<Integer> characterRangesToTokenRanges(Collection<Range<Integer>> characterRanges)
+      throws FormatterException {
+    RangeSet<Integer> tokenRangeSet = TreeRangeSet.create();
+    for (Range<Integer> characterRange0 : characterRanges) {
+      Range<Integer> characterRange = characterRange0.canonical(DiscreteDomain.integers());
+      tokenRangeSet.add(
+          characterRangeToTokenRange(
+              characterRange.lowerEndpoint(),
+              characterRange.upperEndpoint() - characterRange.lowerEndpoint()));
+    }
+    return tokenRangeSet;
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java b/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java
new file mode 100644
index 0000000..6ce0f66
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java
@@ -0,0 +1,3676 @@
+/*
+ * Copyright 2015 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.Iterables.getLast;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.googlejavaformat.Doc.FillMode.INDEPENDENT;
+import static com.google.googlejavaformat.Doc.FillMode.UNIFIED;
+import static com.google.googlejavaformat.Indent.If.make;
+import static com.google.googlejavaformat.OpsBuilder.BlankLineWanted.PRESERVE;
+import static com.google.googlejavaformat.OpsBuilder.BlankLineWanted.YES;
+import static com.google.googlejavaformat.java.Trees.getEndPosition;
+import static com.google.googlejavaformat.java.Trees.getLength;
+import static com.google.googlejavaformat.java.Trees.getMethodName;
+import static com.google.googlejavaformat.java.Trees.getSourceForNode;
+import static com.google.googlejavaformat.java.Trees.getStartPosition;
+import static com.google.googlejavaformat.java.Trees.operatorName;
+import static com.google.googlejavaformat.java.Trees.precedence;
+import static com.google.googlejavaformat.java.Trees.skipParen;
+import static com.sun.source.tree.Tree.Kind.ANNOTATION;
+import static com.sun.source.tree.Tree.Kind.ARRAY_ACCESS;
+import static com.sun.source.tree.Tree.Kind.ASSIGNMENT;
+import static com.sun.source.tree.Tree.Kind.BLOCK;
+import static com.sun.source.tree.Tree.Kind.EXTENDS_WILDCARD;
+import static com.sun.source.tree.Tree.Kind.IF;
+import static com.sun.source.tree.Tree.Kind.METHOD_INVOCATION;
+import static com.sun.source.tree.Tree.Kind.NEW_ARRAY;
+import static com.sun.source.tree.Tree.Kind.NEW_CLASS;
+import static com.sun.source.tree.Tree.Kind.STRING_LITERAL;
+import static com.sun.source.tree.Tree.Kind.UNION_TYPE;
+import static com.sun.source.tree.Tree.Kind.VARIABLE;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Predicate;
+import com.google.common.base.Throwables;
+import com.google.common.base.Verify;
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Multiset;
+import com.google.common.collect.PeekingIterator;
+import com.google.common.collect.Streams;
+import com.google.googlejavaformat.CloseOp;
+import com.google.googlejavaformat.Doc;
+import com.google.googlejavaformat.Doc.FillMode;
+import com.google.googlejavaformat.FormattingError;
+import com.google.googlejavaformat.Indent;
+import com.google.googlejavaformat.Input;
+import com.google.googlejavaformat.Op;
+import com.google.googlejavaformat.OpenOp;
+import com.google.googlejavaformat.OpsBuilder;
+import com.google.googlejavaformat.OpsBuilder.BlankLineWanted;
+import com.google.googlejavaformat.Output.BreakTag;
+import com.google.googlejavaformat.java.DimensionHelpers.SortedDims;
+import com.google.googlejavaformat.java.DimensionHelpers.TypeWithDims;
+import com.sun.source.tree.AnnotatedTypeTree;
+import com.sun.source.tree.AnnotationTree;
+import com.sun.source.tree.ArrayAccessTree;
+import com.sun.source.tree.ArrayTypeTree;
+import com.sun.source.tree.AssertTree;
+import com.sun.source.tree.AssignmentTree;
+import com.sun.source.tree.BinaryTree;
+import com.sun.source.tree.BlockTree;
+import com.sun.source.tree.BreakTree;
+import com.sun.source.tree.CaseTree;
+import com.sun.source.tree.CatchTree;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.CompilationUnitTree;
+import com.sun.source.tree.CompoundAssignmentTree;
+import com.sun.source.tree.ConditionalExpressionTree;
+import com.sun.source.tree.ContinueTree;
+import com.sun.source.tree.DirectiveTree;
+import com.sun.source.tree.DoWhileLoopTree;
+import com.sun.source.tree.EmptyStatementTree;
+import com.sun.source.tree.EnhancedForLoopTree;
+import com.sun.source.tree.ExportsTree;
+import com.sun.source.tree.ExpressionStatementTree;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.ForLoopTree;
+import com.sun.source.tree.IdentifierTree;
+import com.sun.source.tree.IfTree;
+import com.sun.source.tree.ImportTree;
+import com.sun.source.tree.InstanceOfTree;
+import com.sun.source.tree.IntersectionTypeTree;
+import com.sun.source.tree.LabeledStatementTree;
+import com.sun.source.tree.LambdaExpressionTree;
+import com.sun.source.tree.LiteralTree;
+import com.sun.source.tree.MemberReferenceTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.source.tree.MethodTree;
+import com.sun.source.tree.ModifiersTree;
+import com.sun.source.tree.ModuleTree;
+import com.sun.source.tree.NewArrayTree;
+import com.sun.source.tree.NewClassTree;
+import com.sun.source.tree.OpensTree;
+import com.sun.source.tree.ParameterizedTypeTree;
+import com.sun.source.tree.ParenthesizedTree;
+import com.sun.source.tree.PrimitiveTypeTree;
+import com.sun.source.tree.ProvidesTree;
+import com.sun.source.tree.RequiresTree;
+import com.sun.source.tree.ReturnTree;
+import com.sun.source.tree.StatementTree;
+import com.sun.source.tree.SwitchTree;
+import com.sun.source.tree.SynchronizedTree;
+import com.sun.source.tree.ThrowTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.TryTree;
+import com.sun.source.tree.TypeCastTree;
+import com.sun.source.tree.TypeParameterTree;
+import com.sun.source.tree.UnaryTree;
+import com.sun.source.tree.UnionTypeTree;
+import com.sun.source.tree.UsesTree;
+import com.sun.source.tree.VariableTree;
+import com.sun.source.tree.WhileLoopTree;
+import com.sun.source.tree.WildcardTree;
+import com.sun.source.util.TreePath;
+import com.sun.source.util.TreePathScanner;
+import com.sun.tools.javac.code.Flags;
+import com.sun.tools.javac.tree.JCTree;
+import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
+import com.sun.tools.javac.tree.TreeScanner;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import javax.lang.model.element.Name;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * An AST visitor that builds a stream of {@link Op}s to format from the given {@link
+ * CompilationUnitTree}.
+ */
+public class JavaInputAstVisitor extends TreePathScanner<Void, Void> {
+
+  /** Direction for Annotations (usually VERTICAL). */
+  protected enum Direction {
+    VERTICAL,
+    HORIZONTAL;
+
+    boolean isVertical() {
+      return this == VERTICAL;
+    }
+  }
+
+  /** Whether to break or not. */
+  enum BreakOrNot {
+    YES,
+    NO;
+
+    boolean isYes() {
+      return this == YES;
+    }
+  }
+
+  /** Whether to collapse empty blocks. */
+  enum CollapseEmptyOrNot {
+    YES,
+    NO;
+
+    static CollapseEmptyOrNot valueOf(boolean b) {
+      return b ? YES : NO;
+    }
+
+    boolean isYes() {
+      return this == YES;
+    }
+  }
+
+  /** Whether to allow leading blank lines in blocks. */
+  enum AllowLeadingBlankLine {
+    YES,
+    NO;
+
+    static AllowLeadingBlankLine valueOf(boolean b) {
+      return b ? YES : NO;
+    }
+  }
+
+  /** Whether to allow trailing blank lines in blocks. */
+  enum AllowTrailingBlankLine {
+    YES,
+    NO;
+
+    static AllowTrailingBlankLine valueOf(boolean b) {
+      return b ? YES : NO;
+    }
+  }
+
+  /** Whether to include braces. */
+  protected enum BracesOrNot {
+    YES,
+    NO;
+
+    boolean isYes() {
+      return this == YES;
+    }
+  }
+
+  /** Whether or not to include dimensions. */
+  enum DimensionsOrNot {
+    YES,
+    NO;
+
+    boolean isYes() {
+      return this == YES;
+    }
+  }
+
+  /** Whether or not the declaration is Varargs. */
+  enum VarArgsOrNot {
+    YES,
+    NO;
+
+    static VarArgsOrNot valueOf(boolean b) {
+      return b ? YES : NO;
+    }
+
+    boolean isYes() {
+      return this == YES;
+    }
+
+    static VarArgsOrNot fromVariable(VariableTree node) {
+      return valueOf((((JCTree.JCVariableDecl) node).mods.flags & Flags.VARARGS) == Flags.VARARGS);
+    }
+  }
+
+  /** Whether the formal parameter declaration is a receiver. */
+  enum ReceiverParameter {
+    YES,
+    NO;
+
+    boolean isYes() {
+      return this == YES;
+    }
+  }
+
+  /** Whether these declarations are the first in the block. */
+  protected enum FirstDeclarationsOrNot {
+    YES,
+    NO;
+
+    boolean isYes() {
+      return this == YES;
+    }
+  }
+
+  protected final OpsBuilder builder;
+
+  protected static final Indent.Const ZERO = Indent.Const.ZERO;
+  protected final int indentMultiplier;
+  protected final Indent.Const minusTwo;
+  protected final Indent.Const minusFour;
+  protected final Indent.Const plusTwo;
+  protected final Indent.Const plusFour;
+
+  private static final ImmutableList<Op> breakList(Optional<BreakTag> breakTag) {
+    return ImmutableList.of(Doc.Break.make(Doc.FillMode.UNIFIED, " ", ZERO, breakTag));
+  }
+
+  private static final ImmutableList<Op> breakFillList(Optional<BreakTag> breakTag) {
+    return ImmutableList.of(
+        OpenOp.make(ZERO),
+        Doc.Break.make(Doc.FillMode.INDEPENDENT, " ", ZERO, breakTag),
+        CloseOp.make());
+  }
+
+  private static final ImmutableList<Op> forceBreakList(Optional<BreakTag> breakTag) {
+    return ImmutableList.of(Doc.Break.make(FillMode.FORCED, "", Indent.Const.ZERO, breakTag));
+  }
+
+  private static final ImmutableList<Op> EMPTY_LIST = ImmutableList.of();
+
+  /**
+   * Allow multi-line filling (of array initializers, argument lists, and boolean expressions) for
+   * items with length less than or equal to this threshold.
+   */
+  private static final int MAX_ITEM_LENGTH_FOR_FILLING = 10;
+
+  /**
+   * The {@code Visitor} constructor.
+   *
+   * @param builder the {@link OpsBuilder}
+   */
+  public JavaInputAstVisitor(OpsBuilder builder, int indentMultiplier) {
+    this.builder = builder;
+    this.indentMultiplier = indentMultiplier;
+    minusTwo = Indent.Const.make(-2, indentMultiplier);
+    minusFour = Indent.Const.make(-4, indentMultiplier);
+    plusTwo = Indent.Const.make(+2, indentMultiplier);
+    plusFour = Indent.Const.make(+4, indentMultiplier);
+  }
+
+  /** A record of whether we have visited into an expression. */
+  private final Deque<Boolean> inExpression = new ArrayDeque<>(ImmutableList.of(false));
+
+  private boolean inExpression() {
+    return inExpression.peekLast();
+  }
+
+  @Override
+  public Void scan(Tree tree, Void unused) {
+    inExpression.addLast(tree instanceof ExpressionTree || inExpression.peekLast());
+    int previous = builder.depth();
+    try {
+      super.scan(tree, null);
+    } catch (FormattingError e) {
+      throw e;
+    } catch (Throwable t) {
+      throw new FormattingError(builder.diagnostic(Throwables.getStackTraceAsString(t)));
+    } finally {
+      inExpression.removeLast();
+    }
+    builder.checkClosed(previous);
+    return null;
+  }
+
+  @Override
+  public Void visitCompilationUnit(CompilationUnitTree node, Void unused) {
+    boolean first = true;
+    if (node.getPackageName() != null) {
+      markForPartialFormat();
+      visitPackage(node.getPackageName(), node.getPackageAnnotations());
+      builder.forcedBreak();
+      first = false;
+    }
+    dropEmptyDeclarations();
+    if (!node.getImports().isEmpty()) {
+      if (!first) {
+        builder.blankLineWanted(BlankLineWanted.YES);
+      }
+      for (ImportTree importDeclaration : node.getImports()) {
+        markForPartialFormat();
+        builder.blankLineWanted(PRESERVE);
+        scan(importDeclaration, null);
+        builder.forcedBreak();
+      }
+      first = false;
+    }
+    dropEmptyDeclarations();
+    for (Tree type : node.getTypeDecls()) {
+      if (type.getKind() == Tree.Kind.IMPORT) {
+        // javac treats extra semicolons in the import list as type declarations
+        // TODO(cushon): remove this if https://bugs.openjdk.java.net/browse/JDK-8027682 is fixed
+        continue;
+      }
+      if (!first) {
+        builder.blankLineWanted(BlankLineWanted.YES);
+      }
+      markForPartialFormat();
+      scan(type, null);
+      builder.forcedBreak();
+      first = false;
+      dropEmptyDeclarations();
+    }
+    // set a partial format marker at EOF to make sure we can format the entire file
+    markForPartialFormat();
+    return null;
+  }
+
+  /** Skips over extra semi-colons at the top-level, or in a class member declaration lists. */
+  protected void dropEmptyDeclarations() {
+    if (builder.peekToken().equals(Optional.of(";"))) {
+      while (builder.peekToken().equals(Optional.of(";"))) {
+        builder.forcedBreak();
+        markForPartialFormat();
+        token(";");
+      }
+    }
+  }
+
+  @Override
+  public Void visitClass(ClassTree tree, Void unused) {
+    switch (tree.getKind()) {
+      case ANNOTATION_TYPE:
+        visitAnnotationType(tree);
+        break;
+      case CLASS:
+      case INTERFACE:
+        visitClassDeclaration(tree);
+        break;
+      case ENUM:
+        visitEnumDeclaration(tree);
+        break;
+      default:
+        throw new AssertionError(tree.getKind());
+    }
+    return null;
+  }
+
+  public void visitAnnotationType(ClassTree node) {
+    sync(node);
+    builder.open(ZERO);
+    visitAndBreakModifiers(
+        node.getModifiers(),
+        Direction.VERTICAL,
+        /* declarationAnnotationBreak= */ Optional.empty());
+    builder.open(ZERO);
+    token("@");
+    token("interface");
+    builder.breakOp(" ");
+    visit(node.getSimpleName());
+    builder.close();
+    builder.close();
+    if (node.getMembers() == null) {
+      builder.open(plusFour);
+      token(";");
+      builder.close();
+    } else {
+      addBodyDeclarations(node.getMembers(), BracesOrNot.YES, FirstDeclarationsOrNot.YES);
+    }
+    builder.guessToken(";");
+  }
+
+  @Override
+  public Void visitArrayAccess(ArrayAccessTree node, Void unused) {
+    sync(node);
+    visitDot(node);
+    return null;
+  }
+
+  @Override
+  public Void visitNewArray(NewArrayTree node, Void unused) {
+    if (node.getType() != null) {
+      builder.open(plusFour);
+      token("new");
+      builder.space();
+
+      TypeWithDims extractedDims = DimensionHelpers.extractDims(node.getType(), SortedDims.YES);
+      Tree base = extractedDims.node;
+
+      Deque<ExpressionTree> dimExpressions = new ArrayDeque<>(node.getDimensions());
+
+      Deque<List<? extends AnnotationTree>> annotations = new ArrayDeque<>();
+      annotations.add(ImmutableList.copyOf(node.getAnnotations()));
+      annotations.addAll(node.getDimAnnotations());
+      annotations.addAll(extractedDims.dims);
+
+      scan(base, null);
+      builder.open(ZERO);
+      maybeAddDims(dimExpressions, annotations);
+      builder.close();
+      builder.close();
+    }
+    if (node.getInitializers() != null) {
+      if (node.getType() != null) {
+        builder.space();
+      }
+      visitArrayInitializer(node.getInitializers());
+    }
+    return null;
+  }
+
+  public boolean visitArrayInitializer(List<? extends ExpressionTree> expressions) {
+    int cols;
+    if (expressions.isEmpty()) {
+      tokenBreakTrailingComment("{", plusTwo);
+      if (builder.peekToken().equals(Optional.of(","))) {
+        token(",");
+      }
+      token("}", plusTwo);
+    } else if ((cols = argumentsAreTabular(expressions)) != -1) {
+      builder.open(plusTwo);
+      token("{");
+      builder.forcedBreak();
+      boolean first = true;
+      for (Iterable<? extends ExpressionTree> row : Iterables.partition(expressions, cols)) {
+        if (!first) {
+          builder.forcedBreak();
+        }
+        builder.open(row.iterator().next().getKind() == NEW_ARRAY || cols == 1 ? ZERO : plusFour);
+        boolean firstInRow = true;
+        for (ExpressionTree item : row) {
+          if (!firstInRow) {
+            token(",");
+            builder.breakToFill(" ");
+          }
+          scan(item, null);
+          firstInRow = false;
+        }
+        builder.guessToken(",");
+        builder.close();
+        first = false;
+      }
+      builder.breakOp(minusTwo);
+      builder.close();
+      token("}", plusTwo);
+    } else {
+      // Special-case the formatting of array initializers inside annotations
+      // to more eagerly use a one-per-line layout.
+      boolean inMemberValuePair = false;
+      // walk up past the enclosing NewArrayTree (and maybe an enclosing AssignmentTree)
+      TreePath path = getCurrentPath();
+      for (int i = 0; i < 2; i++) {
+        if (path == null) {
+          break;
+        }
+        if (path.getLeaf().getKind() == ANNOTATION) {
+          inMemberValuePair = true;
+          break;
+        }
+        path = path.getParentPath();
+      }
+      boolean shortItems = hasOnlyShortItems(expressions);
+      boolean allowFilledElementsOnOwnLine = shortItems || !inMemberValuePair;
+
+      builder.open(plusTwo);
+      tokenBreakTrailingComment("{", plusTwo);
+      boolean hasTrailingComma = hasTrailingToken(builder.getInput(), expressions, ",");
+      builder.breakOp(hasTrailingComma ? FillMode.FORCED : FillMode.UNIFIED, "", ZERO);
+      if (allowFilledElementsOnOwnLine) {
+        builder.open(ZERO);
+      }
+      boolean first = true;
+      FillMode fillMode = shortItems ? FillMode.INDEPENDENT : FillMode.UNIFIED;
+      for (ExpressionTree expression : expressions) {
+        if (!first) {
+          token(",");
+          builder.breakOp(fillMode, " ", ZERO);
+        }
+        scan(expression, null);
+        first = false;
+      }
+      builder.guessToken(",");
+      if (allowFilledElementsOnOwnLine) {
+        builder.close();
+      }
+      builder.breakOp(minusTwo);
+      builder.close();
+      token("}", plusTwo);
+    }
+    return false;
+  }
+
+  private boolean hasOnlyShortItems(List<? extends ExpressionTree> expressions) {
+    for (ExpressionTree expression : expressions) {
+      int startPosition = getStartPosition(expression);
+      if (builder.actualSize(
+              startPosition, getEndPosition(expression, getCurrentPath()) - startPosition)
+          >= MAX_ITEM_LENGTH_FOR_FILLING) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public Void visitArrayType(ArrayTypeTree node, Void unused) {
+    sync(node);
+    visitAnnotatedArrayType(node);
+    return null;
+  }
+
+  private void visitAnnotatedArrayType(Tree node) {
+    TypeWithDims extractedDims = DimensionHelpers.extractDims(node, SortedDims.YES);
+    builder.open(plusFour);
+    scan(extractedDims.node, null);
+    Deque<List<? extends AnnotationTree>> dims = new ArrayDeque<>(extractedDims.dims);
+    maybeAddDims(dims);
+    Verify.verify(dims.isEmpty());
+    builder.close();
+  }
+
+  @Override
+  public Void visitAssert(AssertTree node, Void unused) {
+    sync(node);
+    builder.open(ZERO);
+    token("assert");
+    builder.space();
+    builder.open(node.getDetail() == null ? ZERO : plusFour);
+    scan(node.getCondition(), null);
+    if (node.getDetail() != null) {
+      builder.breakOp(" ");
+      token(":");
+      builder.space();
+      scan(node.getDetail(), null);
+    }
+    builder.close();
+    builder.close();
+    token(";");
+    return null;
+  }
+
+  @Override
+  public Void visitAssignment(AssignmentTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    scan(node.getVariable(), null);
+    builder.space();
+    splitToken(operatorName(node));
+    builder.breakOp(" ");
+    scan(node.getExpression(), null);
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitBlock(BlockTree node, Void unused) {
+    visitBlock(node, CollapseEmptyOrNot.NO, AllowLeadingBlankLine.NO, AllowTrailingBlankLine.NO);
+    return null;
+  }
+
+  @Override
+  public Void visitCompoundAssignment(CompoundAssignmentTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    scan(node.getVariable(), null);
+    builder.space();
+    splitToken(operatorName(node));
+    builder.breakOp(" ");
+    scan(node.getExpression(), null);
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitBreak(BreakTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    token("break");
+    if (node.getLabel() != null) {
+      builder.breakOp(" ");
+      visit(node.getLabel());
+    }
+    builder.close();
+    token(";");
+    return null;
+  }
+
+  @Override
+  public Void visitTypeCast(TypeCastTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    token("(");
+    scan(node.getType(), null);
+    token(")");
+    builder.breakOp(" ");
+    scan(node.getExpression(), null);
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitNewClass(NewClassTree node, Void unused) {
+    sync(node);
+    builder.open(ZERO);
+    if (node.getEnclosingExpression() != null) {
+      scan(node.getEnclosingExpression(), null);
+      builder.breakOp();
+      token(".");
+    }
+    token("new");
+    builder.space();
+    addTypeArguments(node.getTypeArguments(), plusFour);
+    if (node.getClassBody() != null) {
+      builder.addAll(
+          visitModifiers(
+              node.getClassBody().getModifiers(), Direction.HORIZONTAL, Optional.empty()));
+    }
+    scan(node.getIdentifier(), null);
+    addArguments(node.getArguments(), plusFour);
+    builder.close();
+    if (node.getClassBody() != null) {
+      addBodyDeclarations(
+          node.getClassBody().getMembers(), BracesOrNot.YES, FirstDeclarationsOrNot.YES);
+    }
+    return null;
+  }
+
+  @Override
+  public Void visitConditionalExpression(ConditionalExpressionTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    scan(node.getCondition(), null);
+    builder.breakOp(" ");
+    token("?");
+    builder.space();
+    scan(node.getTrueExpression(), null);
+    builder.breakOp(" ");
+    token(":");
+    builder.space();
+    scan(node.getFalseExpression(), null);
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitContinue(ContinueTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    token("continue");
+    if (node.getLabel() != null) {
+      builder.breakOp(" ");
+      visit(node.getLabel());
+    }
+    token(";");
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitDoWhileLoop(DoWhileLoopTree node, Void unused) {
+    sync(node);
+    token("do");
+    visitStatement(
+        node.getStatement(),
+        CollapseEmptyOrNot.YES,
+        AllowLeadingBlankLine.YES,
+        AllowTrailingBlankLine.YES);
+    if (node.getStatement().getKind() == BLOCK) {
+      builder.space();
+    } else {
+      builder.breakOp(" ");
+    }
+    token("while");
+    builder.space();
+    token("(");
+    scan(skipParen(node.getCondition()), null);
+    token(")");
+    token(";");
+    return null;
+  }
+
+  @Override
+  public Void visitEmptyStatement(EmptyStatementTree node, Void unused) {
+    sync(node);
+    dropEmptyDeclarations();
+    return null;
+  }
+
+  @Override
+  public Void visitEnhancedForLoop(EnhancedForLoopTree node, Void unused) {
+    sync(node);
+    builder.open(ZERO);
+    token("for");
+    builder.space();
+    token("(");
+    builder.open(ZERO);
+    visitToDeclare(
+        DeclarationKind.NONE,
+        Direction.HORIZONTAL,
+        node.getVariable(),
+        Optional.of(node.getExpression()),
+        ":",
+        /* trailing= */ Optional.empty());
+    builder.close();
+    token(")");
+    builder.close();
+    visitStatement(
+        node.getStatement(),
+        CollapseEmptyOrNot.YES,
+        AllowLeadingBlankLine.YES,
+        AllowTrailingBlankLine.NO);
+    return null;
+  }
+
+  private void visitEnumConstantDeclaration(VariableTree enumConstant) {
+    for (AnnotationTree annotation : enumConstant.getModifiers().getAnnotations()) {
+      scan(annotation, null);
+      builder.forcedBreak();
+    }
+    visit(enumConstant.getName());
+    NewClassTree init = ((NewClassTree) enumConstant.getInitializer());
+    if (init.getArguments().isEmpty()) {
+      builder.guessToken("(");
+      builder.guessToken(")");
+    } else {
+      addArguments(init.getArguments(), plusFour);
+    }
+    if (init.getClassBody() != null) {
+      addBodyDeclarations(
+          init.getClassBody().getMembers(), BracesOrNot.YES, FirstDeclarationsOrNot.YES);
+    }
+  }
+
+  public boolean visitEnumDeclaration(ClassTree node) {
+    sync(node);
+    builder.open(ZERO);
+    visitAndBreakModifiers(
+        node.getModifiers(),
+        Direction.VERTICAL,
+        /* declarationAnnotationBreak= */ Optional.empty());
+    builder.open(plusFour);
+    token("enum");
+    builder.breakOp(" ");
+    visit(node.getSimpleName());
+    builder.close();
+    builder.close();
+    if (!node.getImplementsClause().isEmpty()) {
+      builder.open(plusFour);
+      builder.breakOp(" ");
+      builder.open(plusFour);
+      token("implements");
+      builder.breakOp(" ");
+      builder.open(ZERO);
+      boolean first = true;
+      for (Tree superInterfaceType : node.getImplementsClause()) {
+        if (!first) {
+          token(",");
+          builder.breakToFill(" ");
+        }
+        scan(superInterfaceType, null);
+        first = false;
+      }
+      builder.close();
+      builder.close();
+      builder.close();
+    }
+    builder.space();
+    tokenBreakTrailingComment("{", plusTwo);
+    ArrayList<VariableTree> enumConstants = new ArrayList<>();
+    ArrayList<Tree> members = new ArrayList<>();
+    for (Tree member : node.getMembers()) {
+      if (member instanceof JCTree.JCVariableDecl) {
+        JCTree.JCVariableDecl variableDecl = (JCTree.JCVariableDecl) member;
+        if ((variableDecl.mods.flags & Flags.ENUM) == Flags.ENUM) {
+          enumConstants.add(variableDecl);
+          continue;
+        }
+      }
+      members.add(member);
+    }
+    if (enumConstants.isEmpty() && members.isEmpty()) {
+      if (builder.peekToken().equals(Optional.of(";"))) {
+        builder.open(plusTwo);
+        builder.forcedBreak();
+        token(";");
+        builder.forcedBreak();
+        dropEmptyDeclarations();
+        builder.close();
+        builder.open(ZERO);
+        builder.forcedBreak();
+        builder.blankLineWanted(BlankLineWanted.NO);
+        token("}", plusTwo);
+        builder.close();
+      } else {
+        builder.open(ZERO);
+        builder.blankLineWanted(BlankLineWanted.NO);
+        token("}");
+        builder.close();
+      }
+    } else {
+      builder.open(plusTwo);
+      builder.blankLineWanted(BlankLineWanted.NO);
+      builder.forcedBreak();
+      builder.open(ZERO);
+      boolean first = true;
+      for (VariableTree enumConstant : enumConstants) {
+        if (!first) {
+          token(",");
+          builder.forcedBreak();
+          builder.blankLineWanted(BlankLineWanted.PRESERVE);
+        }
+        markForPartialFormat();
+        visitEnumConstantDeclaration(enumConstant);
+        first = false;
+      }
+      if (builder.peekToken().orElse("").equals(",")) {
+        token(",");
+        builder.forcedBreak(); // The ";" goes on its own line.
+      }
+      builder.close();
+      builder.close();
+      if (builder.peekToken().equals(Optional.of(";"))) {
+        builder.open(plusTwo);
+        token(";");
+        builder.forcedBreak();
+        dropEmptyDeclarations();
+        builder.close();
+      }
+      builder.open(ZERO);
+      addBodyDeclarations(members, BracesOrNot.NO, FirstDeclarationsOrNot.NO);
+      builder.forcedBreak();
+      builder.blankLineWanted(BlankLineWanted.NO);
+      token("}", plusTwo);
+      builder.close();
+    }
+    builder.guessToken(";");
+    return false;
+  }
+
+  @Override
+  public Void visitMemberReference(MemberReferenceTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    scan(node.getQualifierExpression(), null);
+    builder.breakOp();
+    builder.op("::");
+    addTypeArguments(node.getTypeArguments(), plusFour);
+    switch (node.getMode()) {
+      case INVOKE:
+        visit(node.getName());
+        break;
+      case NEW:
+        token("new");
+        break;
+      default:
+        throw new AssertionError(node.getMode());
+    }
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitExpressionStatement(ExpressionStatementTree node, Void unused) {
+    sync(node);
+    scan(node.getExpression(), null);
+    token(";");
+    return null;
+  }
+
+  @Override
+  public Void visitVariable(VariableTree node, Void unused) {
+    sync(node);
+    visitVariables(
+        ImmutableList.of(node),
+        DeclarationKind.NONE,
+        fieldAnnotationDirection(node.getModifiers()));
+    return null;
+  }
+
+  void visitVariables(
+      List<VariableTree> fragments,
+      DeclarationKind declarationKind,
+      Direction annotationDirection) {
+    if (fragments.size() == 1) {
+      VariableTree fragment = fragments.get(0);
+      declareOne(
+          declarationKind,
+          annotationDirection,
+          Optional.of(fragment.getModifiers()),
+          fragment.getType(),
+          /* name= */ fragment.getName(),
+          "",
+          "=",
+          Optional.ofNullable(fragment.getInitializer()),
+          Optional.of(";"),
+          /* receiverExpression= */ Optional.empty(),
+          Optional.ofNullable(variableFragmentDims(true, 0, fragment.getType())));
+
+    } else {
+      declareMany(fragments, annotationDirection);
+    }
+  }
+
+  private TypeWithDims variableFragmentDims(boolean first, int leadingDims, Tree type) {
+    if (type == null) {
+      return null;
+    }
+    if (first) {
+      return DimensionHelpers.extractDims(type, SortedDims.YES);
+    }
+    TypeWithDims dims = DimensionHelpers.extractDims(type, SortedDims.NO);
+    return new TypeWithDims(
+        null, leadingDims > 0 ? dims.dims.subList(0, dims.dims.size() - leadingDims) : dims.dims);
+  }
+
+  @Override
+  public Void visitForLoop(ForLoopTree node, Void unused) {
+    sync(node);
+    token("for");
+    builder.space();
+    token("(");
+    builder.open(plusFour);
+    builder.open(
+        node.getInitializer().size() > 1
+                && node.getInitializer().get(0).getKind() == Tree.Kind.EXPRESSION_STATEMENT
+            ? plusFour
+            : ZERO);
+    if (!node.getInitializer().isEmpty()) {
+      if (node.getInitializer().get(0).getKind() == VARIABLE) {
+        PeekingIterator<StatementTree> it =
+            Iterators.peekingIterator(node.getInitializer().iterator());
+        visitVariables(
+            variableFragments(it, it.next()), DeclarationKind.NONE, Direction.HORIZONTAL);
+      } else {
+        boolean first = true;
+        builder.open(ZERO);
+        for (StatementTree t : node.getInitializer()) {
+          if (!first) {
+            token(",");
+            builder.breakOp(" ");
+          }
+          scan(((ExpressionStatementTree) t).getExpression(), null);
+          first = false;
+        }
+        token(";");
+        builder.close();
+      }
+    } else {
+      token(";");
+    }
+    builder.close();
+    builder.breakOp(" ");
+    if (node.getCondition() != null) {
+      scan(node.getCondition(), null);
+    }
+    token(";");
+    if (!node.getUpdate().isEmpty()) {
+      builder.breakOp(" ");
+      builder.open(node.getUpdate().size() <= 1 ? ZERO : plusFour);
+      boolean firstUpdater = true;
+      for (ExpressionStatementTree updater : node.getUpdate()) {
+        if (!firstUpdater) {
+          token(",");
+          builder.breakToFill(" ");
+        }
+        scan(updater.getExpression(), null);
+        firstUpdater = false;
+      }
+      builder.guessToken(";");
+      builder.close();
+    } else {
+      builder.space();
+    }
+    builder.close();
+    token(")");
+    visitStatement(
+        node.getStatement(),
+        CollapseEmptyOrNot.YES,
+        AllowLeadingBlankLine.YES,
+        AllowTrailingBlankLine.NO);
+    return null;
+  }
+
+  @Override
+  public Void visitIf(IfTree node, Void unused) {
+    sync(node);
+    // Collapse chains of else-ifs.
+    List<ExpressionTree> expressions = new ArrayList<>();
+    List<StatementTree> statements = new ArrayList<>();
+    while (true) {
+      expressions.add(node.getCondition());
+      statements.add(node.getThenStatement());
+      if (node.getElseStatement() != null && node.getElseStatement().getKind() == IF) {
+        node = (IfTree) node.getElseStatement();
+      } else {
+        break;
+      }
+    }
+    builder.open(ZERO);
+    boolean first = true;
+    boolean followingBlock = false;
+    int expressionsN = expressions.size();
+    for (int i = 0; i < expressionsN; i++) {
+      if (!first) {
+        if (followingBlock) {
+          builder.space();
+        } else {
+          builder.forcedBreak();
+        }
+        token("else");
+        builder.space();
+      }
+      token("if");
+      builder.space();
+      token("(");
+      scan(skipParen(expressions.get(i)), null);
+      token(")");
+      // An empty block can collapse to "{}" if there are no if/else or else clauses
+      boolean onlyClause = expressionsN == 1 && node.getElseStatement() == null;
+      // Trailing blank lines are permitted if this isn't the last clause
+      boolean trailingClauses = i < expressionsN - 1 || node.getElseStatement() != null;
+      visitStatement(
+          statements.get(i),
+          CollapseEmptyOrNot.valueOf(onlyClause),
+          AllowLeadingBlankLine.YES,
+          AllowTrailingBlankLine.valueOf(trailingClauses));
+      followingBlock = statements.get(i).getKind() == BLOCK;
+      first = false;
+    }
+    if (node.getElseStatement() != null) {
+      if (followingBlock) {
+        builder.space();
+      } else {
+        builder.forcedBreak();
+      }
+      token("else");
+      visitStatement(
+          node.getElseStatement(),
+          CollapseEmptyOrNot.NO,
+          AllowLeadingBlankLine.YES,
+          AllowTrailingBlankLine.NO);
+    }
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitImport(ImportTree node, Void unused) {
+    sync(node);
+    token("import");
+    builder.space();
+    if (node.isStatic()) {
+      token("static");
+      builder.space();
+    }
+    visitName(node.getQualifiedIdentifier());
+    token(";");
+    // TODO(cushon): remove this if https://bugs.openjdk.java.net/browse/JDK-8027682 is fixed
+    dropEmptyDeclarations();
+    return null;
+  }
+
+  @Override
+  public Void visitBinary(BinaryTree node, Void unused) {
+    sync(node);
+    /*
+     * Collect together all operators with same precedence to clean up indentation.
+     */
+    List<ExpressionTree> operands = new ArrayList<>();
+    List<String> operators = new ArrayList<>();
+    walkInfix(precedence(node), node, operands, operators);
+    FillMode fillMode = hasOnlyShortItems(operands) ? INDEPENDENT : UNIFIED;
+    builder.open(plusFour);
+    scan(operands.get(0), null);
+    int operatorsN = operators.size();
+    for (int i = 0; i < operatorsN; i++) {
+      builder.breakOp(fillMode, " ", ZERO);
+      builder.op(operators.get(i));
+      builder.space();
+      scan(operands.get(i + 1), null);
+    }
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitInstanceOf(InstanceOfTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    scan(node.getExpression(), null);
+    builder.breakOp(" ");
+    builder.open(ZERO);
+    token("instanceof");
+    builder.breakOp(" ");
+    scan(node.getType(), null);
+    builder.close();
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitIntersectionType(IntersectionTypeTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    boolean first = true;
+    for (Tree type : node.getBounds()) {
+      if (!first) {
+        builder.breakToFill(" ");
+        token("&");
+        builder.space();
+      }
+      scan(type, null);
+      first = false;
+    }
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitLabeledStatement(LabeledStatementTree node, Void unused) {
+    sync(node);
+    builder.open(ZERO);
+    visit(node.getLabel());
+    token(":");
+    builder.forcedBreak();
+    builder.close();
+    scan(node.getStatement(), null);
+    return null;
+  }
+
+  @Override
+  public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) {
+    sync(node);
+    boolean statementBody = node.getBodyKind() == LambdaExpressionTree.BodyKind.STATEMENT;
+    boolean parens = builder.peekToken().equals(Optional.of("("));
+    builder.open(parens ? plusFour : ZERO);
+    if (parens) {
+      token("(");
+    }
+    boolean first = true;
+    for (VariableTree parameter : node.getParameters()) {
+      if (!first) {
+        token(",");
+        builder.breakOp(" ");
+      }
+      scan(parameter, null);
+      first = false;
+    }
+    if (parens) {
+      token(")");
+    }
+    builder.close();
+    builder.space();
+    builder.op("->");
+    builder.open(statementBody ? ZERO : plusFour);
+    if (statementBody) {
+      builder.space();
+    } else {
+      builder.breakOp(" ");
+    }
+    if (node.getBody().getKind() == Tree.Kind.BLOCK) {
+      visitBlock(
+          (BlockTree) node.getBody(),
+          CollapseEmptyOrNot.YES,
+          AllowLeadingBlankLine.NO,
+          AllowTrailingBlankLine.NO);
+    } else {
+      scan(node.getBody(), null);
+    }
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitAnnotation(AnnotationTree node, Void unused) {
+    sync(node);
+
+    if (visitSingleMemberAnnotation(node)) {
+      return null;
+    }
+
+    builder.open(ZERO);
+    token("@");
+    scan(node.getAnnotationType(), null);
+    if (!node.getArguments().isEmpty()) {
+      builder.open(plusFour);
+      token("(");
+      builder.breakOp();
+      boolean first = true;
+
+      // Format the member value pairs one-per-line if any of them are
+      // initialized with arrays.
+      boolean hasArrayInitializer =
+          Iterables.any(node.getArguments(), JavaInputAstVisitor::isArrayValue);
+      for (ExpressionTree argument : node.getArguments()) {
+        if (!first) {
+          token(",");
+          if (hasArrayInitializer) {
+            builder.forcedBreak();
+          } else {
+            builder.breakOp(" ");
+          }
+        }
+        if (argument instanceof AssignmentTree) {
+          visitAnnotationArgument((AssignmentTree) argument);
+        } else {
+          scan(argument, null);
+        }
+        first = false;
+      }
+      token(")");
+      builder.close();
+      builder.close();
+      return null;
+
+    } else if (builder.peekToken().equals(Optional.of("("))) {
+      token("(");
+      token(")");
+    }
+    builder.close();
+    return null;
+  }
+
+  private static boolean isArrayValue(ExpressionTree argument) {
+    if (!(argument instanceof AssignmentTree)) {
+      return false;
+    }
+    ExpressionTree expression = ((AssignmentTree) argument).getExpression();
+    return expression instanceof NewArrayTree && ((NewArrayTree) expression).getType() == null;
+  }
+
+  public void visitAnnotationArgument(AssignmentTree node) {
+    boolean isArrayInitializer = node.getExpression().getKind() == NEW_ARRAY;
+    sync(node);
+    builder.open(isArrayInitializer ? ZERO : plusFour);
+    scan(node.getVariable(), null);
+    builder.space();
+    token("=");
+    if (isArrayInitializer) {
+      builder.space();
+    } else {
+      builder.breakOp(" ");
+    }
+    scan(node.getExpression(), null);
+    builder.close();
+  }
+
+  @Override
+  public Void visitAnnotatedType(AnnotatedTypeTree node, Void unused) {
+    sync(node);
+    ExpressionTree base = node.getUnderlyingType();
+    if (base instanceof MemberSelectTree) {
+      MemberSelectTree selectTree = (MemberSelectTree) base;
+      scan(selectTree.getExpression(), null);
+      token(".");
+      visitAnnotations(node.getAnnotations(), BreakOrNot.NO, BreakOrNot.NO);
+      builder.breakToFill(" ");
+      visit(selectTree.getIdentifier());
+    } else if (base instanceof ArrayTypeTree) {
+      visitAnnotatedArrayType(node);
+    } else {
+      visitAnnotations(node.getAnnotations(), BreakOrNot.NO, BreakOrNot.NO);
+      builder.breakToFill(" ");
+      scan(base, null);
+    }
+    return null;
+  }
+
+  // TODO(cushon): Use Flags if/when we drop support for Java 11
+
+  protected static final long COMPACT_RECORD_CONSTRUCTOR = 1L << 51;
+
+  protected static final long RECORD = 1L << 61;
+
+  @Override
+  public Void visitMethod(MethodTree node, Void unused) {
+    sync(node);
+    List<? extends AnnotationTree> annotations = node.getModifiers().getAnnotations();
+    List<? extends AnnotationTree> returnTypeAnnotations = ImmutableList.of();
+
+    boolean isRecordConstructor =
+        (((JCMethodDecl) node).mods.flags & COMPACT_RECORD_CONSTRUCTOR)
+            == COMPACT_RECORD_CONSTRUCTOR;
+
+    if (!node.getTypeParameters().isEmpty() && !annotations.isEmpty()) {
+      int typeParameterStart = getStartPosition(node.getTypeParameters().get(0));
+      for (int i = 0; i < annotations.size(); i++) {
+        if (getStartPosition(annotations.get(i)) > typeParameterStart) {
+          returnTypeAnnotations = annotations.subList(i, annotations.size());
+          annotations = annotations.subList(0, i);
+          break;
+        }
+      }
+    }
+    builder.addAll(
+        visitModifiers(
+            annotations, Direction.VERTICAL, /* declarationAnnotationBreak= */ Optional.empty()));
+
+    Tree baseReturnType = null;
+    Deque<List<? extends AnnotationTree>> dims = null;
+    if (node.getReturnType() != null) {
+      TypeWithDims extractedDims =
+          DimensionHelpers.extractDims(node.getReturnType(), SortedDims.YES);
+      baseReturnType = extractedDims.node;
+      dims = new ArrayDeque<>(extractedDims.dims);
+    }
+
+    builder.open(plusFour);
+    BreakTag breakBeforeName = genSym();
+    BreakTag breakBeforeType = genSym();
+    builder.open(ZERO);
+    {
+      boolean first = true;
+      if (!node.getTypeParameters().isEmpty()) {
+        token("<");
+        typeParametersRest(node.getTypeParameters(), plusFour);
+        if (!returnTypeAnnotations.isEmpty()) {
+          builder.breakToFill(" ");
+          visitAnnotations(returnTypeAnnotations, BreakOrNot.NO, BreakOrNot.NO);
+        }
+        first = false;
+      }
+
+      boolean openedNameAndTypeScope = false;
+      // constructor-like declarations that don't match the name of the enclosing class are
+      // parsed as method declarations with a null return type
+      if (baseReturnType != null) {
+        if (!first) {
+          builder.breakOp(INDEPENDENT, " ", ZERO, Optional.of(breakBeforeType));
+        } else {
+          first = false;
+        }
+        if (!openedNameAndTypeScope) {
+          builder.open(make(breakBeforeType, plusFour, ZERO));
+          openedNameAndTypeScope = true;
+        }
+        scan(baseReturnType, null);
+        maybeAddDims(dims);
+      }
+      if (!first) {
+        builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO, Optional.of(breakBeforeName));
+      } else {
+        first = false;
+      }
+      if (!openedNameAndTypeScope) {
+        builder.open(ZERO);
+        openedNameAndTypeScope = true;
+      }
+      String name = node.getName().toString();
+      if (name.equals("<init>")) {
+        name = builder.peekToken().get();
+      }
+      token(name);
+      if (!isRecordConstructor) {
+        token("(");
+      }
+      // end of name and type scope
+      builder.close();
+    }
+    builder.close();
+
+    builder.open(Indent.If.make(breakBeforeName, plusFour, ZERO));
+    builder.open(Indent.If.make(breakBeforeType, plusFour, ZERO));
+    builder.open(ZERO);
+    {
+      if (!isRecordConstructor) {
+        if (!node.getParameters().isEmpty() || node.getReceiverParameter() != null) {
+          // Break before args.
+          builder.breakToFill("");
+          visitFormals(Optional.ofNullable(node.getReceiverParameter()), node.getParameters());
+        }
+        token(")");
+      }
+      if (dims != null) {
+        maybeAddDims(dims);
+      }
+      if (!node.getThrows().isEmpty()) {
+        builder.breakToFill(" ");
+        builder.open(plusFour);
+        {
+          visitThrowsClause(node.getThrows());
+        }
+        builder.close();
+      }
+      if (node.getDefaultValue() != null) {
+        builder.space();
+        token("default");
+        if (node.getDefaultValue().getKind() == Tree.Kind.NEW_ARRAY) {
+          builder.open(minusFour);
+          {
+            builder.space();
+            scan(node.getDefaultValue(), null);
+          }
+          builder.close();
+        } else {
+          builder.open(ZERO);
+          {
+            builder.breakToFill(" ");
+            scan(node.getDefaultValue(), null);
+          }
+          builder.close();
+        }
+      }
+    }
+    builder.close();
+    builder.close();
+    builder.close();
+    if (node.getBody() == null) {
+      token(";");
+    } else {
+      builder.space();
+      builder.token("{", Doc.Token.RealOrImaginary.REAL, plusTwo, Optional.of(plusTwo));
+    }
+    builder.close();
+
+    if (node.getBody() != null) {
+      methodBody(node);
+    }
+
+    return null;
+  }
+
+  private void methodBody(MethodTree node) {
+    if (node.getBody().getStatements().isEmpty()) {
+      builder.blankLineWanted(BlankLineWanted.NO);
+    } else {
+      builder.open(plusTwo);
+      builder.forcedBreak();
+      builder.blankLineWanted(BlankLineWanted.PRESERVE);
+      visitStatements(node.getBody().getStatements());
+      builder.close();
+      builder.forcedBreak();
+      builder.blankLineWanted(BlankLineWanted.NO);
+      markForPartialFormat();
+    }
+    token("}", plusTwo);
+  }
+
+  @Override
+  public Void visitMethodInvocation(MethodInvocationTree node, Void unused) {
+    sync(node);
+    if (handleLogStatement(node)) {
+      return null;
+    }
+    visitDot(node);
+    return null;
+  }
+
+  /**
+   * Special-cases log statements, to output:
+   *
+   * <pre>{@code
+   * logger.atInfo().log(
+   *     "Number of foos: %d, foos.size());
+   * }</pre>
+   *
+   * <p>Instead of:
+   *
+   * <pre>{@code
+   * logger
+   *     .atInfo()
+   *     .log(
+   *         "Number of foos: %d, foos.size());
+   * }</pre>
+   */
+  private boolean handleLogStatement(MethodInvocationTree node) {
+    if (!getMethodName(node).contentEquals("log")) {
+      return false;
+    }
+    Deque<ExpressionTree> parts = new ArrayDeque<>();
+    ExpressionTree curr = node;
+    while (curr instanceof MethodInvocationTree) {
+      MethodInvocationTree method = (MethodInvocationTree) curr;
+      parts.addFirst(method);
+      if (!LOG_METHODS.contains(getMethodName(method).toString())) {
+        return false;
+      }
+      curr = Trees.getMethodReceiver(method);
+    }
+    if (!(curr instanceof IdentifierTree)) {
+      return false;
+    }
+    parts.addFirst(curr);
+    visitDotWithPrefix(
+        ImmutableList.copyOf(parts), false, ImmutableList.of(parts.size() - 1), INDEPENDENT);
+    return true;
+  }
+
+  static final ImmutableSet<String> LOG_METHODS =
+      ImmutableSet.of(
+          "at",
+          "atConfig",
+          "atDebug",
+          "atFine",
+          "atFiner",
+          "atFinest",
+          "atInfo",
+          "atMostEvery",
+          "atSevere",
+          "atWarning",
+          "every",
+          "log",
+          "logVarargs",
+          "perUnique",
+          "withCause",
+          "withStackTrace");
+
+  private static List<Long> handleStream(List<ExpressionTree> parts) {
+    return indexes(
+            parts.stream(),
+            p -> {
+              if (!(p instanceof MethodInvocationTree)) {
+                return false;
+              }
+              Name name = getMethodName((MethodInvocationTree) p);
+              return Stream.of("stream", "parallelStream", "toBuilder")
+                  .anyMatch(name::contentEquals);
+            })
+        .collect(toList());
+  }
+
+  private static <T> Stream<Long> indexes(Stream<T> stream, Predicate<T> predicate) {
+    return Streams.mapWithIndex(stream, (x, i) -> predicate.apply(x) ? i : -1).filter(x -> x != -1);
+  }
+
+  @Override
+  public Void visitMemberSelect(MemberSelectTree node, Void unused) {
+    sync(node);
+    visitDot(node);
+    return null;
+  }
+
+  @Override
+  public Void visitLiteral(LiteralTree node, Void unused) {
+    sync(node);
+    String sourceForNode = getSourceForNode(node, getCurrentPath());
+    // A negative numeric literal -n is usually represented as unary minus on n,
+    // but that doesn't work for integer or long MIN_VALUE. The parser works
+    // around that by representing it directly as a signed literal (with no
+    // unary minus), but the lexer still expects two tokens.
+    if (sourceForNode.startsWith("-")) {
+      token("-");
+      sourceForNode = sourceForNode.substring(1).trim();
+    }
+    token(sourceForNode);
+    return null;
+  }
+
+  private void visitPackage(
+      ExpressionTree packageName, List<? extends AnnotationTree> packageAnnotations) {
+    if (!packageAnnotations.isEmpty()) {
+      for (AnnotationTree annotation : packageAnnotations) {
+        builder.forcedBreak();
+        scan(annotation, null);
+      }
+      builder.forcedBreak();
+    }
+    builder.open(plusFour);
+    token("package");
+    builder.space();
+    visitName(packageName);
+    builder.close();
+    token(";");
+  }
+
+  @Override
+  public Void visitParameterizedType(ParameterizedTypeTree node, Void unused) {
+    sync(node);
+    if (node.getTypeArguments().isEmpty()) {
+      scan(node.getType(), null);
+      token("<");
+      token(">");
+    } else {
+      builder.open(plusFour);
+      scan(node.getType(), null);
+      token("<");
+      builder.breakOp();
+      builder.open(ZERO);
+      boolean first = true;
+      for (Tree typeArgument : node.getTypeArguments()) {
+        if (!first) {
+          token(",");
+          builder.breakOp(" ");
+        }
+        scan(typeArgument, null);
+        first = false;
+      }
+      builder.close();
+      builder.close();
+      token(">");
+    }
+    return null;
+  }
+
+  @Override
+  public Void visitParenthesized(ParenthesizedTree node, Void unused) {
+    token("(");
+    scan(node.getExpression(), null);
+    token(")");
+    return null;
+  }
+
+  @Override
+  public Void visitUnary(UnaryTree node, Void unused) {
+    sync(node);
+    String operatorName = operatorName(node);
+    if (((JCTree) node).getTag().isPostUnaryOp()) {
+      scan(node.getExpression(), null);
+      splitToken(operatorName);
+    } else {
+      splitToken(operatorName);
+      if (ambiguousUnaryOperator(node, operatorName)) {
+        builder.space();
+      }
+      scan(node.getExpression(), null);
+    }
+    return null;
+  }
+
+  private void splitToken(String operatorName) {
+    for (int i = 0; i < operatorName.length(); i++) {
+      token(String.valueOf(operatorName.charAt(i)));
+    }
+  }
+
+  private boolean ambiguousUnaryOperator(UnaryTree node, String operatorName) {
+    switch (node.getKind()) {
+      case UNARY_MINUS:
+      case UNARY_PLUS:
+        break;
+      default:
+        return false;
+    }
+    if (!(node.getExpression() instanceof UnaryTree)) {
+      return false;
+    }
+    JCTree.Tag tag = ((JCTree) node.getExpression()).getTag();
+    if (tag.isPostUnaryOp()) {
+      return false;
+    }
+    if (!operatorName(node).startsWith(operatorName)) {
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  public Void visitPrimitiveType(PrimitiveTypeTree node, Void unused) {
+    sync(node);
+    switch (node.getPrimitiveTypeKind()) {
+      case BOOLEAN:
+        token("boolean");
+        break;
+      case BYTE:
+        token("byte");
+        break;
+      case SHORT:
+        token("short");
+        break;
+      case INT:
+        token("int");
+        break;
+      case LONG:
+        token("long");
+        break;
+      case CHAR:
+        token("char");
+        break;
+      case FLOAT:
+        token("float");
+        break;
+      case DOUBLE:
+        token("double");
+        break;
+      case VOID:
+        token("void");
+        break;
+      default:
+        throw new AssertionError(node.getPrimitiveTypeKind());
+    }
+    return null;
+  }
+
+  public boolean visit(Name name) {
+    token(name.toString());
+    return false;
+  }
+
+  @Override
+  public Void visitReturn(ReturnTree node, Void unused) {
+    sync(node);
+    token("return");
+    if (node.getExpression() != null) {
+      builder.space();
+      scan(node.getExpression(), null);
+    }
+    token(";");
+    return null;
+  }
+
+  // TODO(cushon): is this worth special-casing?
+  boolean visitSingleMemberAnnotation(AnnotationTree node) {
+    if (node.getArguments().size() != 1) {
+      return false;
+    }
+    ExpressionTree value = getOnlyElement(node.getArguments());
+    if (value.getKind() == ASSIGNMENT) {
+      return false;
+    }
+    boolean isArrayInitializer = value.getKind() == NEW_ARRAY;
+    builder.open(isArrayInitializer ? ZERO : plusFour);
+    token("@");
+    scan(node.getAnnotationType(), null);
+    token("(");
+    if (!isArrayInitializer) {
+      builder.breakOp();
+    }
+    scan(value, null);
+    builder.close();
+    token(")");
+    return true;
+  }
+
+  @Override
+  public Void visitCase(CaseTree node, Void unused) {
+    sync(node);
+    markForPartialFormat();
+    builder.forcedBreak();
+    if (node.getExpression() == null) {
+      token("default", plusTwo);
+      token(":");
+    } else {
+      token("case", plusTwo);
+      builder.space();
+      scan(node.getExpression(), null);
+      token(":");
+    }
+    builder.open(plusTwo);
+    visitStatements(node.getStatements());
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitSwitch(SwitchTree node, Void unused) {
+    sync(node);
+    visitSwitch(node.getExpression(), node.getCases());
+    return null;
+  }
+
+  protected void visitSwitch(ExpressionTree expression, List<? extends CaseTree> cases) {
+    token("switch");
+    builder.space();
+    token("(");
+    scan(skipParen(expression), null);
+    token(")");
+    builder.space();
+    tokenBreakTrailingComment("{", plusTwo);
+    builder.blankLineWanted(BlankLineWanted.NO);
+    builder.open(plusTwo);
+    boolean first = true;
+    for (CaseTree caseTree : cases) {
+      if (!first) {
+        builder.blankLineWanted(BlankLineWanted.PRESERVE);
+      }
+      scan(caseTree, null);
+      first = false;
+    }
+    builder.close();
+    builder.forcedBreak();
+    builder.blankLineWanted(BlankLineWanted.NO);
+    token("}", plusFour);
+  }
+
+  @Override
+  public Void visitSynchronized(SynchronizedTree node, Void unused) {
+    sync(node);
+    token("synchronized");
+    builder.space();
+    token("(");
+    builder.open(plusFour);
+    builder.breakOp();
+    scan(skipParen(node.getExpression()), null);
+    builder.close();
+    token(")");
+    builder.space();
+    scan(node.getBlock(), null);
+    return null;
+  }
+
+  @Override
+  public Void visitThrow(ThrowTree node, Void unused) {
+    sync(node);
+    token("throw");
+    builder.space();
+    scan(node.getExpression(), null);
+    token(";");
+    return null;
+  }
+
+  @Override
+  public Void visitTry(TryTree node, Void unused) {
+    sync(node);
+    builder.open(ZERO);
+    token("try");
+    builder.space();
+    if (!node.getResources().isEmpty()) {
+      token("(");
+      builder.open(node.getResources().size() > 1 ? plusFour : ZERO);
+      boolean first = true;
+      for (Tree resource : node.getResources()) {
+        if (!first) {
+          builder.forcedBreak();
+        }
+        if (resource instanceof VariableTree) {
+          VariableTree variableTree = (VariableTree) resource;
+          declareOne(
+              DeclarationKind.PARAMETER,
+              fieldAnnotationDirection(variableTree.getModifiers()),
+              Optional.of(variableTree.getModifiers()),
+              variableTree.getType(),
+              /* name= */ variableTree.getName(),
+              "",
+              "=",
+              Optional.ofNullable(variableTree.getInitializer()),
+              /* trailing= */ Optional.empty(),
+              /* receiverExpression= */ Optional.empty(),
+              /* typeWithDims= */ Optional.empty());
+        } else {
+          // TODO(cushon): think harder about what to do with `try (resource1; resource2) {}`
+          scan(resource, null);
+        }
+        if (builder.peekToken().equals(Optional.of(";"))) {
+          token(";");
+          builder.space();
+        }
+        first = false;
+      }
+      if (builder.peekToken().equals(Optional.of(";"))) {
+        token(";");
+        builder.space();
+      }
+      token(")");
+      builder.close();
+      builder.space();
+    }
+    // An empty try-with-resources body can collapse to "{}" if there are no trailing catch or
+    // finally blocks.
+    boolean trailingClauses = !node.getCatches().isEmpty() || node.getFinallyBlock() != null;
+    visitBlock(
+        node.getBlock(),
+        CollapseEmptyOrNot.valueOf(!trailingClauses),
+        AllowLeadingBlankLine.YES,
+        AllowTrailingBlankLine.valueOf(trailingClauses));
+    for (int i = 0; i < node.getCatches().size(); i++) {
+      CatchTree catchClause = node.getCatches().get(i);
+      trailingClauses = i < node.getCatches().size() - 1 || node.getFinallyBlock() != null;
+      visitCatchClause(catchClause, AllowTrailingBlankLine.valueOf(trailingClauses));
+    }
+    if (node.getFinallyBlock() != null) {
+      builder.space();
+      token("finally");
+      builder.space();
+      visitBlock(
+          node.getFinallyBlock(),
+          CollapseEmptyOrNot.NO,
+          AllowLeadingBlankLine.YES,
+          AllowTrailingBlankLine.NO);
+    }
+    builder.close();
+    return null;
+  }
+
+  public void visitClassDeclaration(ClassTree node) {
+    sync(node);
+    List<Op> breaks =
+        visitModifiers(
+            node.getModifiers(),
+            Direction.VERTICAL,
+            /* declarationAnnotationBreak= */ Optional.empty());
+    boolean hasSuperclassType = node.getExtendsClause() != null;
+    boolean hasSuperInterfaceTypes = !node.getImplementsClause().isEmpty();
+    builder.addAll(breaks);
+    token(node.getKind() == Tree.Kind.INTERFACE ? "interface" : "class");
+    builder.space();
+    visit(node.getSimpleName());
+    if (!node.getTypeParameters().isEmpty()) {
+      token("<");
+    }
+    builder.open(plusFour);
+    {
+      if (!node.getTypeParameters().isEmpty()) {
+        typeParametersRest(
+            node.getTypeParameters(),
+            hasSuperclassType || hasSuperInterfaceTypes ? plusFour : ZERO);
+      }
+      if (hasSuperclassType) {
+        builder.breakToFill(" ");
+        token("extends");
+        builder.space();
+        scan(node.getExtendsClause(), null);
+      }
+      if (hasSuperInterfaceTypes) {
+        builder.breakToFill(" ");
+        builder.open(node.getImplementsClause().size() > 1 ? plusFour : ZERO);
+        token(node.getKind() == Tree.Kind.INTERFACE ? "extends" : "implements");
+        builder.space();
+        boolean first = true;
+        for (Tree superInterfaceType : node.getImplementsClause()) {
+          if (!first) {
+            token(",");
+            builder.breakOp(" ");
+          }
+          scan(superInterfaceType, null);
+          first = false;
+        }
+        builder.close();
+      }
+    }
+    builder.close();
+    if (node.getMembers() == null) {
+      token(";");
+    } else {
+      addBodyDeclarations(node.getMembers(), BracesOrNot.YES, FirstDeclarationsOrNot.YES);
+    }
+    dropEmptyDeclarations();
+  }
+
+  @Override
+  public Void visitTypeParameter(TypeParameterTree node, Void unused) {
+    sync(node);
+    builder.open(ZERO);
+    visitAnnotations(node.getAnnotations(), BreakOrNot.NO, BreakOrNot.YES);
+    visit(node.getName());
+    if (!node.getBounds().isEmpty()) {
+      builder.space();
+      token("extends");
+      builder.open(plusFour);
+      builder.breakOp(" ");
+      builder.open(plusFour);
+      boolean first = true;
+      for (Tree typeBound : node.getBounds()) {
+        if (!first) {
+          builder.breakToFill(" ");
+          token("&");
+          builder.space();
+        }
+        scan(typeBound, null);
+        first = false;
+      }
+      builder.close();
+      builder.close();
+    }
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitUnionType(UnionTypeTree node, Void unused) {
+    throw new IllegalStateException("expected manual descent into union types");
+  }
+
+  @Override
+  public Void visitWhileLoop(WhileLoopTree node, Void unused) {
+    sync(node);
+    token("while");
+    builder.space();
+    token("(");
+    scan(skipParen(node.getCondition()), null);
+    token(")");
+    visitStatement(
+        node.getStatement(),
+        CollapseEmptyOrNot.YES,
+        AllowLeadingBlankLine.YES,
+        AllowTrailingBlankLine.NO);
+    return null;
+  }
+
+  @Override
+  public Void visitWildcard(WildcardTree node, Void unused) {
+    sync(node);
+    builder.open(ZERO);
+    token("?");
+    if (node.getBound() != null) {
+      builder.open(plusFour);
+      builder.space();
+      token(node.getKind() == EXTENDS_WILDCARD ? "extends" : "super");
+      builder.breakOp(" ");
+      scan(node.getBound(), null);
+      builder.close();
+    }
+    builder.close();
+    return null;
+  }
+
+  // Helper methods.
+
+  /** Helper method for annotations. */
+  void visitAnnotations(
+      List<? extends AnnotationTree> annotations, BreakOrNot breakBefore, BreakOrNot breakAfter) {
+    if (!annotations.isEmpty()) {
+      if (breakBefore.isYes()) {
+        builder.breakToFill(" ");
+      }
+      boolean first = true;
+      for (AnnotationTree annotation : annotations) {
+        if (!first) {
+          builder.breakToFill(" ");
+        }
+        scan(annotation, null);
+        first = false;
+      }
+      if (breakAfter.isYes()) {
+        builder.breakToFill(" ");
+      }
+    }
+  }
+
+  /** Helper method for blocks. */
+  private void visitBlock(
+      BlockTree node,
+      CollapseEmptyOrNot collapseEmptyOrNot,
+      AllowLeadingBlankLine allowLeadingBlankLine,
+      AllowTrailingBlankLine allowTrailingBlankLine) {
+    sync(node);
+    if (node.isStatic()) {
+      token("static");
+      builder.space();
+    }
+    if (collapseEmptyOrNot.isYes() && node.getStatements().isEmpty()) {
+      if (builder.peekToken().equals(Optional.of(";"))) {
+        // TODO(cushon): is this needed?
+        token(";");
+      } else {
+        tokenBreakTrailingComment("{", plusTwo);
+        builder.blankLineWanted(BlankLineWanted.NO);
+        token("}", plusTwo);
+      }
+    } else {
+      builder.open(ZERO);
+      builder.open(plusTwo);
+      tokenBreakTrailingComment("{", plusTwo);
+      if (allowLeadingBlankLine == AllowLeadingBlankLine.NO) {
+        builder.blankLineWanted(BlankLineWanted.NO);
+      } else {
+        builder.blankLineWanted(BlankLineWanted.PRESERVE);
+      }
+      visitStatements(node.getStatements());
+      builder.close();
+      builder.forcedBreak();
+      builder.close();
+      if (allowTrailingBlankLine == AllowTrailingBlankLine.NO) {
+        builder.blankLineWanted(BlankLineWanted.NO);
+      } else {
+        builder.blankLineWanted(BlankLineWanted.PRESERVE);
+      }
+      markForPartialFormat();
+      token("}", plusTwo);
+    }
+  }
+
+  /** Helper method for statements. */
+  private void visitStatement(
+      StatementTree node,
+      CollapseEmptyOrNot collapseEmptyOrNot,
+      AllowLeadingBlankLine allowLeadingBlank,
+      AllowTrailingBlankLine allowTrailingBlank) {
+    sync(node);
+    switch (node.getKind()) {
+      case BLOCK:
+        builder.space();
+        visitBlock((BlockTree) node, collapseEmptyOrNot, allowLeadingBlank, allowTrailingBlank);
+        break;
+      default:
+        builder.open(plusTwo);
+        builder.breakOp(" ");
+        scan(node, null);
+        builder.close();
+    }
+  }
+
+  protected void visitStatements(List<? extends StatementTree> statements) {
+    boolean first = true;
+    PeekingIterator<StatementTree> it = Iterators.peekingIterator(statements.iterator());
+    dropEmptyDeclarations();
+    while (it.hasNext()) {
+      StatementTree tree = it.next();
+      builder.forcedBreak();
+      if (!first) {
+        builder.blankLineWanted(BlankLineWanted.PRESERVE);
+      }
+      markForPartialFormat();
+      first = false;
+      List<VariableTree> fragments = variableFragments(it, tree);
+      if (!fragments.isEmpty()) {
+        visitVariables(
+            fragments,
+            DeclarationKind.NONE,
+            canLocalHaveHorizontalAnnotations(fragments.get(0).getModifiers()));
+      } else {
+        scan(tree, null);
+      }
+    }
+  }
+
+  /** Output combined modifiers and annotations and the trailing break. */
+  void visitAndBreakModifiers(
+      ModifiersTree modifiers,
+      Direction annotationDirection,
+      Optional<BreakTag> declarationAnnotationBreak) {
+    builder.addAll(visitModifiers(modifiers, annotationDirection, declarationAnnotationBreak));
+  }
+
+  @Override
+  public Void visitModifiers(ModifiersTree node, Void unused) {
+    throw new IllegalStateException("expected manual descent into modifiers");
+  }
+
+  /** Output combined modifiers and annotations and returns the trailing break. */
+  protected List<Op> visitModifiers(
+      ModifiersTree modifiersTree,
+      Direction annotationsDirection,
+      Optional<BreakTag> declarationAnnotationBreak) {
+    return visitModifiers(
+        modifiersTree.getAnnotations(), annotationsDirection, declarationAnnotationBreak);
+  }
+
+  protected List<Op> visitModifiers(
+      List<? extends AnnotationTree> annotationTrees,
+      Direction annotationsDirection,
+      Optional<BreakTag> declarationAnnotationBreak) {
+    if (annotationTrees.isEmpty() && !nextIsModifier()) {
+      return EMPTY_LIST;
+    }
+    Deque<AnnotationTree> annotations = new ArrayDeque<>(annotationTrees);
+    builder.open(ZERO);
+    boolean first = true;
+    boolean lastWasAnnotation = false;
+    while (!annotations.isEmpty()) {
+      if (nextIsModifier()) {
+        break;
+      }
+      if (!first) {
+        builder.addAll(
+            annotationsDirection.isVertical()
+                ? forceBreakList(declarationAnnotationBreak)
+                : breakList(declarationAnnotationBreak));
+      }
+      scan(annotations.removeFirst(), null);
+      first = false;
+      lastWasAnnotation = true;
+    }
+    builder.close();
+    ImmutableList<Op> trailingBreak =
+        annotationsDirection.isVertical()
+            ? forceBreakList(declarationAnnotationBreak)
+            : breakList(declarationAnnotationBreak);
+    if (annotations.isEmpty() && !nextIsModifier()) {
+      return trailingBreak;
+    }
+    if (lastWasAnnotation) {
+      builder.addAll(trailingBreak);
+    }
+
+    builder.open(ZERO);
+    first = true;
+    while (nextIsModifier() || !annotations.isEmpty()) {
+      if (!first) {
+        builder.addAll(breakFillList(Optional.empty()));
+      }
+      if (nextIsModifier()) {
+        token(builder.peekToken().get());
+      } else {
+        scan(annotations.removeFirst(), null);
+        lastWasAnnotation = true;
+      }
+      first = false;
+    }
+    builder.close();
+    return breakFillList(Optional.empty());
+  }
+
+  boolean nextIsModifier() {
+    switch (builder.peekToken().get()) {
+      case "public":
+      case "protected":
+      case "private":
+      case "abstract":
+      case "static":
+      case "final":
+      case "transient":
+      case "volatile":
+      case "synchronized":
+      case "native":
+      case "strictfp":
+      case "default":
+        return true;
+      default:
+        return false;
+    }
+  }
+
+  @Override
+  public Void visitCatch(CatchTree node, Void unused) {
+    throw new IllegalStateException("expected manual descent into catch trees");
+  }
+
+  /** Helper method for {@link CatchTree}s. */
+  private void visitCatchClause(CatchTree node, AllowTrailingBlankLine allowTrailingBlankLine) {
+    sync(node);
+    builder.space();
+    token("catch");
+    builder.space();
+    token("(");
+    builder.open(plusFour);
+    VariableTree ex = node.getParameter();
+    if (ex.getType().getKind() == UNION_TYPE) {
+      builder.open(ZERO);
+      visitUnionType(ex);
+      builder.close();
+    } else {
+      // TODO(cushon): don't break after here for consistency with for, while, etc.
+      builder.breakToFill();
+      builder.open(ZERO);
+      scan(ex, null);
+      builder.close();
+    }
+    builder.close();
+    token(")");
+    builder.space();
+    visitBlock(
+        node.getBlock(), CollapseEmptyOrNot.NO, AllowLeadingBlankLine.YES, allowTrailingBlankLine);
+  }
+
+  /** Formats a union type declaration in a catch clause. */
+  private void visitUnionType(VariableTree declaration) {
+    UnionTypeTree type = (UnionTypeTree) declaration.getType();
+    builder.open(ZERO);
+    sync(declaration);
+    visitAndBreakModifiers(
+        declaration.getModifiers(),
+        Direction.HORIZONTAL,
+        /* declarationAnnotationBreak= */ Optional.empty());
+    List<? extends Tree> union = type.getTypeAlternatives();
+    boolean first = true;
+    for (int i = 0; i < union.size() - 1; i++) {
+      if (!first) {
+        builder.breakOp(" ");
+        token("|");
+        builder.space();
+      } else {
+        first = false;
+      }
+      scan(union.get(i), null);
+    }
+    builder.breakOp(" ");
+    token("|");
+    builder.space();
+    Tree last = union.get(union.size() - 1);
+    declareOne(
+        DeclarationKind.NONE,
+        Direction.HORIZONTAL,
+        /* modifiers= */ Optional.empty(),
+        last,
+        /* name= */ declaration.getName(),
+        /* op= */ "",
+        "=",
+        Optional.ofNullable(declaration.getInitializer()),
+        /* trailing= */ Optional.empty(),
+        /* receiverExpression= */ Optional.empty(),
+        /* typeWithDims= */ Optional.empty());
+    builder.close();
+  }
+
+  /** Accumulate the operands and operators. */
+  private static void walkInfix(
+      int precedence,
+      ExpressionTree expression,
+      List<ExpressionTree> operands,
+      List<String> operators) {
+    if (expression instanceof BinaryTree) {
+      BinaryTree binaryTree = (BinaryTree) expression;
+      if (precedence(binaryTree) == precedence) {
+        walkInfix(precedence, binaryTree.getLeftOperand(), operands, operators);
+        operators.add(operatorName(expression));
+        walkInfix(precedence, binaryTree.getRightOperand(), operands, operators);
+      } else {
+        operands.add(expression);
+      }
+    } else {
+      operands.add(expression);
+    }
+  }
+
+  protected void visitFormals(
+      Optional<VariableTree> receiver, List<? extends VariableTree> parameters) {
+    if (!receiver.isPresent() && parameters.isEmpty()) {
+      return;
+    }
+    builder.open(ZERO);
+    boolean first = true;
+    if (receiver.isPresent()) {
+      // TODO(jdd): Use builders.
+      declareOne(
+          DeclarationKind.PARAMETER,
+          Direction.HORIZONTAL,
+          Optional.of(receiver.get().getModifiers()),
+          receiver.get().getType(),
+          /* name= */ receiver.get().getName(),
+          "",
+          "",
+          /* initializer= */ Optional.empty(),
+          !parameters.isEmpty() ? Optional.of(",") : Optional.empty(),
+          Optional.of(receiver.get().getNameExpression()),
+          /* typeWithDims= */ Optional.empty());
+      first = false;
+    }
+    for (int i = 0; i < parameters.size(); i++) {
+      VariableTree parameter = parameters.get(i);
+      if (!first) {
+        builder.breakOp(" ");
+      }
+      visitToDeclare(
+          DeclarationKind.PARAMETER,
+          Direction.HORIZONTAL,
+          parameter,
+          /* initializer= */ Optional.empty(),
+          "=",
+          i < parameters.size() - 1 ? Optional.of(",") : /* a= */ Optional.empty());
+      first = false;
+    }
+    builder.close();
+  }
+
+  //  /** Helper method for {@link MethodDeclaration}s. */
+  private void visitThrowsClause(List<? extends ExpressionTree> thrownExceptionTypes) {
+    token("throws");
+    builder.breakToFill(" ");
+    boolean first = true;
+    for (ExpressionTree thrownExceptionType : thrownExceptionTypes) {
+      if (!first) {
+        token(",");
+        builder.breakToFill(" ");
+      }
+      scan(thrownExceptionType, null);
+      first = false;
+    }
+  }
+
+  @Override
+  public Void visitIdentifier(IdentifierTree node, Void unused) {
+    sync(node);
+    token(node.getName().toString());
+    return null;
+  }
+
+  @Override
+  public Void visitModule(ModuleTree node, Void unused) {
+    for (AnnotationTree annotation : node.getAnnotations()) {
+      scan(annotation, null);
+      builder.forcedBreak();
+    }
+    if (node.getModuleType() == ModuleTree.ModuleKind.OPEN) {
+      token("open");
+      builder.space();
+    }
+    token("module");
+    builder.space();
+    scan(node.getName(), null);
+    builder.space();
+    if (node.getDirectives().isEmpty()) {
+      tokenBreakTrailingComment("{", plusTwo);
+      builder.blankLineWanted(BlankLineWanted.NO);
+      token("}", plusTwo);
+    } else {
+      builder.open(plusTwo);
+      token("{");
+      builder.forcedBreak();
+      Optional<Tree.Kind> previousDirective = Optional.empty();
+      for (DirectiveTree directiveTree : node.getDirectives()) {
+        markForPartialFormat();
+        builder.blankLineWanted(
+            previousDirective.map(k -> !k.equals(directiveTree.getKind())).orElse(false)
+                ? BlankLineWanted.YES
+                : BlankLineWanted.NO);
+        builder.forcedBreak();
+        scan(directiveTree, null);
+        previousDirective = Optional.of(directiveTree.getKind());
+      }
+      builder.close();
+      builder.forcedBreak();
+      token("}");
+    }
+    return null;
+  }
+
+  private void visitDirective(
+      String name,
+      String separator,
+      ExpressionTree nameExpression,
+      @Nullable List<? extends ExpressionTree> items) {
+    token(name);
+    builder.space();
+    scan(nameExpression, null);
+    if (items != null) {
+      builder.open(plusFour);
+      builder.space();
+      token(separator);
+      builder.forcedBreak();
+      boolean first = true;
+      for (ExpressionTree item : items) {
+        if (!first) {
+          token(",");
+          builder.forcedBreak();
+        }
+        scan(item, null);
+        first = false;
+      }
+      token(";");
+      builder.close();
+    } else {
+      token(";");
+    }
+  }
+
+  @Override
+  public Void visitExports(ExportsTree node, Void unused) {
+    visitDirective("exports", "to", node.getPackageName(), node.getModuleNames());
+    return null;
+  }
+
+  @Override
+  public Void visitOpens(OpensTree node, Void unused) {
+    visitDirective("opens", "to", node.getPackageName(), node.getModuleNames());
+    return null;
+  }
+
+  @Override
+  public Void visitProvides(ProvidesTree node, Void unused) {
+    visitDirective("provides", "with", node.getServiceName(), node.getImplementationNames());
+    return null;
+  }
+
+  @Override
+  public Void visitRequires(RequiresTree node, Void unused) {
+    token("requires");
+    builder.space();
+    while (true) {
+      if (builder.peekToken().equals(Optional.of("static"))) {
+        token("static");
+        builder.space();
+      } else if (builder.peekToken().equals(Optional.of("transitive"))) {
+        token("transitive");
+        builder.space();
+      } else {
+        break;
+      }
+    }
+    scan(node.getModuleName(), null);
+    token(";");
+    return null;
+  }
+
+  @Override
+  public Void visitUses(UsesTree node, Void unused) {
+    token("uses");
+    builder.space();
+    scan(node.getServiceName(), null);
+    token(";");
+    return null;
+  }
+
+  /** Helper method for import declarations, names, and qualified names. */
+  private void visitName(Tree node) {
+    Deque<Name> stack = new ArrayDeque<>();
+    for (; node instanceof MemberSelectTree; node = ((MemberSelectTree) node).getExpression()) {
+      stack.addFirst(((MemberSelectTree) node).getIdentifier());
+    }
+    stack.addFirst(((IdentifierTree) node).getName());
+    boolean first = true;
+    for (Name name : stack) {
+      if (!first) {
+        token(".");
+      }
+      token(name.toString());
+      first = false;
+    }
+  }
+
+  private void visitToDeclare(
+      DeclarationKind kind,
+      Direction annotationsDirection,
+      VariableTree node,
+      Optional<ExpressionTree> initializer,
+      String equals,
+      Optional<String> trailing) {
+    sync(node);
+    Optional<TypeWithDims> typeWithDims;
+    Tree type;
+    if (node.getType() != null) {
+      TypeWithDims extractedDims = DimensionHelpers.extractDims(node.getType(), SortedDims.YES);
+      typeWithDims = Optional.of(extractedDims);
+      type = extractedDims.node;
+    } else {
+      typeWithDims = Optional.empty();
+      type = null;
+    }
+    declareOne(
+        kind,
+        annotationsDirection,
+        Optional.of(node.getModifiers()),
+        type,
+        node.getName(),
+        "",
+        equals,
+        initializer,
+        trailing,
+        /* receiverExpression= */ Optional.empty(),
+        typeWithDims);
+  }
+
+  /** Does not omit the leading {@code "<"}, which should be associated with the type name. */
+  protected void typeParametersRest(
+      List<? extends TypeParameterTree> typeParameters, Indent plusIndent) {
+    builder.open(plusIndent);
+    builder.breakOp();
+    builder.open(ZERO);
+    boolean first = true;
+    for (TypeParameterTree typeParameter : typeParameters) {
+      if (!first) {
+        token(",");
+        builder.breakOp(" ");
+      }
+      scan(typeParameter, null);
+      first = false;
+    }
+    token(">");
+    builder.close();
+    builder.close();
+  }
+
+  /** Collapse chains of {@code .} operators, across multiple {@link ASTNode} types. */
+
+  /**
+   * Output a "." node.
+   *
+   * @param node0 the "." node
+   */
+  void visitDot(ExpressionTree node0) {
+    ExpressionTree node = node0;
+
+    // collect a flattened list of "."-separated items
+    // e.g. ImmutableList.builder().add(1).build() -> [ImmutableList, builder(), add(1), build()]
+    Deque<ExpressionTree> stack = new ArrayDeque<>();
+    LOOP:
+    do {
+      stack.addFirst(node);
+      if (node.getKind() == ARRAY_ACCESS) {
+        node = getArrayBase(node);
+      }
+      switch (node.getKind()) {
+        case MEMBER_SELECT:
+          node = ((MemberSelectTree) node).getExpression();
+          break;
+        case METHOD_INVOCATION:
+          node = getMethodReceiver((MethodInvocationTree) node);
+          break;
+        case IDENTIFIER:
+          node = null;
+          break LOOP;
+        default:
+          // If the dot chain starts with a primary expression
+          // (e.g. a class instance creation, or a conditional expression)
+          // then remove it from the list and deal with it first.
+          node = stack.removeFirst();
+          break LOOP;
+      }
+    } while (node != null);
+    List<ExpressionTree> items = new ArrayList<>(stack);
+
+    boolean needDot = false;
+
+    // The dot chain started with a primary expression: output it normally, and indent
+    // the rest of the chain +4.
+    if (node != null) {
+      // Exception: if it's an anonymous class declaration, we don't need to
+      // break and indent after the trailing '}'.
+      if (node.getKind() == NEW_CLASS && ((NewClassTree) node).getClassBody() != null) {
+        builder.open(ZERO);
+        scan(getArrayBase(node), null);
+        token(".");
+      } else {
+        builder.open(plusFour);
+        scan(getArrayBase(node), null);
+        builder.breakOp();
+        needDot = true;
+      }
+      formatArrayIndices(getArrayIndices(node));
+      if (stack.isEmpty()) {
+        builder.close();
+        return;
+      }
+    }
+
+    Set<Integer> prefixes = new LinkedHashSet<>();
+
+    // Check if the dot chain has a prefix that looks like a type name, so we can
+    // treat the type name-shaped part as a single syntactic unit.
+    TypeNameClassifier.typePrefixLength(simpleNames(stack)).ifPresent(prefixes::add);
+
+    int invocationCount = 0;
+    int firstInvocationIndex = -1;
+    {
+      for (int i = 0; i < items.size(); i++) {
+        ExpressionTree expression = items.get(i);
+        if (expression.getKind() == METHOD_INVOCATION) {
+          if (i > 0 || node != null) {
+            // we only want dereference invocations
+            invocationCount++;
+          }
+          if (firstInvocationIndex < 0) {
+            firstInvocationIndex = i;
+          }
+        }
+      }
+    }
+
+    // If there's only one invocation, treat leading field accesses as a single
+    // unit. In the normal case we want to preserve the alignment of subsequent
+    // method calls, and would emit e.g.:
+    //
+    // myField
+    //     .foo()
+    //     .bar();
+    //
+    // But if there's no 'bar()' to worry about the alignment of we prefer:
+    //
+    // myField.foo();
+    //
+    // to:
+    //
+    // myField
+    //     .foo();
+    //
+    if (invocationCount == 1 && firstInvocationIndex > 0) {
+      prefixes.add(firstInvocationIndex);
+    }
+
+    if (prefixes.isEmpty() && items.get(0) instanceof IdentifierTree) {
+      switch (((IdentifierTree) items.get(0)).getName().toString()) {
+        case "this":
+        case "super":
+          prefixes.add(1);
+          break;
+        default:
+          break;
+      }
+    }
+
+    List<Long> streamPrefixes = handleStream(items);
+    streamPrefixes.forEach(x -> prefixes.add(x.intValue()));
+    if (!prefixes.isEmpty()) {
+      visitDotWithPrefix(
+          items, needDot, prefixes, streamPrefixes.isEmpty() ? INDEPENDENT : UNIFIED);
+    } else {
+      visitRegularDot(items, needDot);
+    }
+
+    if (node != null) {
+      builder.close();
+    }
+  }
+
+  /**
+   * Output a "regular" chain of dereferences, possibly in builder-style. Break before every dot.
+   *
+   * @param items in the chain
+   * @param needDot whether a leading dot is needed
+   */
+  private void visitRegularDot(List<ExpressionTree> items, boolean needDot) {
+    boolean trailingDereferences = items.size() > 1;
+    boolean needDot0 = needDot;
+    if (!needDot0) {
+      builder.open(plusFour);
+    }
+    // don't break after the first element if it is every small, unless the
+    // chain starts with another expression
+    int minLength = indentMultiplier * 4;
+    int length = needDot0 ? minLength : 0;
+    for (ExpressionTree e : items) {
+      if (needDot) {
+        if (length > minLength) {
+          builder.breakOp(FillMode.UNIFIED, "", ZERO);
+        }
+        token(".");
+        length++;
+      }
+      if (!fillFirstArgument(e, items, trailingDereferences ? ZERO : minusFour)) {
+        BreakTag tyargTag = genSym();
+        dotExpressionUpToArgs(e, Optional.of(tyargTag));
+        Indent tyargIndent = Indent.If.make(tyargTag, plusFour, ZERO);
+        dotExpressionArgsAndParen(
+            e, tyargIndent, (trailingDereferences || needDot) ? plusFour : ZERO);
+      }
+      length += getLength(e, getCurrentPath());
+      needDot = true;
+    }
+    if (!needDot0) {
+      builder.close();
+    }
+  }
+
+  // avoid formattings like:
+  //
+  // when(
+  //         something
+  //             .happens())
+  //     .thenReturn(result);
+  //
+  private boolean fillFirstArgument(ExpressionTree e, List<ExpressionTree> items, Indent indent) {
+    // is there a trailing dereference?
+    if (items.size() < 2) {
+      return false;
+    }
+    // don't special-case calls nested inside expressions
+    if (e.getKind() != METHOD_INVOCATION) {
+      return false;
+    }
+    MethodInvocationTree methodInvocation = (MethodInvocationTree) e;
+    Name name = getMethodName(methodInvocation);
+    if (!(methodInvocation.getMethodSelect() instanceof IdentifierTree)
+        || name.length() > 4
+        || !methodInvocation.getTypeArguments().isEmpty()
+        || methodInvocation.getArguments().size() != 1) {
+      return false;
+    }
+    builder.open(ZERO);
+    builder.open(indent);
+    visit(name);
+    token("(");
+    ExpressionTree arg = getOnlyElement(methodInvocation.getArguments());
+    scan(arg, null);
+    builder.close();
+    token(")");
+    builder.close();
+    return true;
+  }
+
+  /**
+   * Output a chain of dereferences where some prefix should be treated as a single syntactic unit,
+   * either because it looks like a type name or because there is only a single method invocation in
+   * the chain.
+   *
+   * @param items in the chain
+   * @param needDot whether a leading dot is needed
+   * @param prefixes the terminal indices of 'prefixes' of the expression that should be treated as
+   *     a syntactic unit
+   */
+  private void visitDotWithPrefix(
+      List<ExpressionTree> items,
+      boolean needDot,
+      Collection<Integer> prefixes,
+      FillMode prefixFillMode) {
+    // Are there method invocations or field accesses after the prefix?
+    boolean trailingDereferences = !prefixes.isEmpty() && getLast(prefixes) < items.size() - 1;
+
+    builder.open(plusFour);
+    for (int times = 0; times < prefixes.size(); times++) {
+      builder.open(ZERO);
+    }
+
+    Deque<Integer> unconsumedPrefixes = new ArrayDeque<>(ImmutableSortedSet.copyOf(prefixes));
+    BreakTag nameTag = genSym();
+    for (int i = 0; i < items.size(); i++) {
+      ExpressionTree e = items.get(i);
+      if (needDot) {
+        FillMode fillMode;
+        if (!unconsumedPrefixes.isEmpty() && i <= unconsumedPrefixes.peekFirst()) {
+          fillMode = prefixFillMode;
+        } else {
+          fillMode = FillMode.UNIFIED;
+        }
+
+        builder.breakOp(fillMode, "", ZERO, Optional.of(nameTag));
+        token(".");
+      }
+      BreakTag tyargTag = genSym();
+      dotExpressionUpToArgs(e, Optional.of(tyargTag));
+      if (!unconsumedPrefixes.isEmpty() && i == unconsumedPrefixes.peekFirst()) {
+        builder.close();
+        unconsumedPrefixes.removeFirst();
+      }
+
+      Indent tyargIndent = Indent.If.make(tyargTag, plusFour, ZERO);
+      Indent argsIndent = Indent.If.make(nameTag, plusFour, trailingDereferences ? plusFour : ZERO);
+      dotExpressionArgsAndParen(e, tyargIndent, argsIndent);
+
+      needDot = true;
+    }
+
+    builder.close();
+  }
+
+  /** Returns the simple names of expressions in a "." chain. */
+  private List<String> simpleNames(Deque<ExpressionTree> stack) {
+    ImmutableList.Builder<String> simpleNames = ImmutableList.builder();
+    OUTER:
+    for (ExpressionTree expression : stack) {
+      boolean isArray = expression.getKind() == ARRAY_ACCESS;
+      expression = getArrayBase(expression);
+      switch (expression.getKind()) {
+        case MEMBER_SELECT:
+          simpleNames.add(((MemberSelectTree) expression).getIdentifier().toString());
+          break;
+        case IDENTIFIER:
+          simpleNames.add(((IdentifierTree) expression).getName().toString());
+          break;
+        case METHOD_INVOCATION:
+          simpleNames.add(getMethodName((MethodInvocationTree) expression).toString());
+          break OUTER;
+        default:
+          break OUTER;
+      }
+      if (isArray) {
+        break OUTER;
+      }
+    }
+    return simpleNames.build();
+  }
+
+  private void dotExpressionUpToArgs(ExpressionTree expression, Optional<BreakTag> tyargTag) {
+    expression = getArrayBase(expression);
+    switch (expression.getKind()) {
+      case MEMBER_SELECT:
+        MemberSelectTree fieldAccess = (MemberSelectTree) expression;
+        visit(fieldAccess.getIdentifier());
+        break;
+      case METHOD_INVOCATION:
+        MethodInvocationTree methodInvocation = (MethodInvocationTree) expression;
+        if (!methodInvocation.getTypeArguments().isEmpty()) {
+          builder.open(plusFour);
+          addTypeArguments(methodInvocation.getTypeArguments(), ZERO);
+          // TODO(jdd): Should indent the name -4.
+          builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO, tyargTag);
+          builder.close();
+        }
+        visit(getMethodName(methodInvocation));
+        break;
+      case IDENTIFIER:
+        visit(((IdentifierTree) expression).getName());
+        break;
+      default:
+        scan(expression, null);
+        break;
+    }
+  }
+
+  /**
+   * Returns the base expression of an erray access, e.g. given {@code foo[0][0]} returns {@code
+   * foo}.
+   */
+  private ExpressionTree getArrayBase(ExpressionTree node) {
+    while (node instanceof ArrayAccessTree) {
+      node = ((ArrayAccessTree) node).getExpression();
+    }
+    return node;
+  }
+
+  private ExpressionTree getMethodReceiver(MethodInvocationTree methodInvocation) {
+    ExpressionTree select = methodInvocation.getMethodSelect();
+    return select instanceof MemberSelectTree ? ((MemberSelectTree) select).getExpression() : null;
+  }
+
+  private void dotExpressionArgsAndParen(
+      ExpressionTree expression, Indent tyargIndent, Indent indent) {
+    Deque<ExpressionTree> indices = getArrayIndices(expression);
+    expression = getArrayBase(expression);
+    switch (expression.getKind()) {
+      case METHOD_INVOCATION:
+        builder.open(tyargIndent);
+        MethodInvocationTree methodInvocation = (MethodInvocationTree) expression;
+        addArguments(methodInvocation.getArguments(), indent);
+        builder.close();
+        break;
+      default:
+        break;
+    }
+    formatArrayIndices(indices);
+  }
+
+  /** Lays out one or more array indices. Does not output the expression for the array itself. */
+  private void formatArrayIndices(Deque<ExpressionTree> indices) {
+    if (indices.isEmpty()) {
+      return;
+    }
+    builder.open(ZERO);
+    do {
+      token("[");
+      builder.breakToFill();
+      scan(indices.removeLast(), null);
+      token("]");
+    } while (!indices.isEmpty());
+    builder.close();
+  }
+
+  /**
+   * Returns all array indices for the given expression, e.g. given {@code foo[0][0]} returns the
+   * expressions for {@code [0][0]}.
+   */
+  private Deque<ExpressionTree> getArrayIndices(ExpressionTree expression) {
+    Deque<ExpressionTree> indices = new ArrayDeque<>();
+    while (expression instanceof ArrayAccessTree) {
+      ArrayAccessTree array = (ArrayAccessTree) expression;
+      indices.addLast(array.getIndex());
+      expression = array.getExpression();
+    }
+    return indices;
+  }
+
+  /** Helper methods for method invocations. */
+  void addTypeArguments(List<? extends Tree> typeArguments, Indent plusIndent) {
+    if (typeArguments == null || typeArguments.isEmpty()) {
+      return;
+    }
+    token("<");
+    builder.open(plusIndent);
+    boolean first = true;
+    for (Tree typeArgument : typeArguments) {
+      if (!first) {
+        token(",");
+        builder.breakToFill(" ");
+      }
+      scan(typeArgument, null);
+      first = false;
+    }
+    builder.close();
+    token(">");
+  }
+
+  /**
+   * Add arguments to a method invocation, etc. The arguments indented {@code plusFour}, filled,
+   * from the current indent. The arguments may be output two at a time if they seem to be arguments
+   * to a map constructor, etc.
+   *
+   * @param arguments the arguments
+   * @param plusIndent the extra indent for the arguments
+   */
+  void addArguments(List<? extends ExpressionTree> arguments, Indent plusIndent) {
+    builder.open(plusIndent);
+    token("(");
+    if (!arguments.isEmpty()) {
+      if (arguments.size() % 2 == 0 && argumentsAreTabular(arguments) == 2) {
+        builder.forcedBreak();
+        builder.open(ZERO);
+        boolean first = true;
+        for (int i = 0; i < arguments.size() - 1; i += 2) {
+          ExpressionTree argument0 = arguments.get(i);
+          ExpressionTree argument1 = arguments.get(i + 1);
+          if (!first) {
+            token(",");
+            builder.forcedBreak();
+          }
+          builder.open(plusFour);
+          scan(argument0, null);
+          token(",");
+          builder.breakOp(" ");
+          scan(argument1, null);
+          builder.close();
+          first = false;
+        }
+        builder.close();
+      } else if (isFormatMethod(arguments)) {
+        builder.breakOp();
+        builder.open(ZERO);
+        scan(arguments.get(0), null);
+        token(",");
+        builder.breakOp(" ");
+        builder.open(ZERO);
+        argList(arguments.subList(1, arguments.size()));
+        builder.close();
+        builder.close();
+      } else {
+        builder.breakOp();
+        argList(arguments);
+      }
+    }
+    token(")");
+    builder.close();
+  }
+
+  private void argList(List<? extends ExpressionTree> arguments) {
+    builder.open(ZERO);
+    boolean first = true;
+    FillMode fillMode = hasOnlyShortItems(arguments) ? FillMode.INDEPENDENT : FillMode.UNIFIED;
+    for (ExpressionTree argument : arguments) {
+      if (!first) {
+        token(",");
+        builder.breakOp(fillMode, " ", ZERO);
+      }
+      scan(argument, null);
+      first = false;
+    }
+    builder.close();
+  }
+
+  /**
+   * Identifies String formatting methods like {@link String#format} which we prefer to format as:
+   *
+   * <pre>{@code
+   * String.format(
+   *     "the format string: %s %s %s",
+   *     arg, arg, arg);
+   * }</pre>
+   *
+   * <p>And not:
+   *
+   * <pre>{@code
+   * String.format(
+   *     "the format string: %s %s %s",
+   *     arg,
+   *     arg,
+   *     arg);
+   * }</pre>
+   */
+  private boolean isFormatMethod(List<? extends ExpressionTree> arguments) {
+    if (arguments.size() < 2) {
+      return false;
+    }
+    return isStringConcat(arguments.get(0));
+  }
+
+  private static final Pattern FORMAT_SPECIFIER = Pattern.compile("%|\\{[0-9]\\}");
+
+  private boolean isStringConcat(ExpressionTree first) {
+    final boolean[] stringLiteral = {true};
+    final boolean[] formatString = {false};
+    new TreeScanner() {
+      @Override
+      public void scan(JCTree tree) {
+        if (tree == null) {
+          return;
+        }
+        switch (tree.getKind()) {
+          case STRING_LITERAL:
+            break;
+          case PLUS:
+            super.scan(tree);
+            break;
+          default:
+            stringLiteral[0] = false;
+            break;
+        }
+        if (tree.getKind() == STRING_LITERAL) {
+          Object value = ((LiteralTree) tree).getValue();
+          if (value instanceof String && FORMAT_SPECIFIER.matcher(value.toString()).find()) {
+            formatString[0] = true;
+          }
+        }
+      }
+    }.scan((JCTree) first);
+    return stringLiteral[0] && formatString[0];
+  }
+
+  /** Returns the number of columns if the arguments arg laid out in a grid, or else {@code -1}. */
+  private int argumentsAreTabular(List<? extends ExpressionTree> arguments) {
+    if (arguments.isEmpty()) {
+      return -1;
+    }
+    List<List<ExpressionTree>> rows = new ArrayList<>();
+    PeekingIterator<ExpressionTree> it = Iterators.peekingIterator(arguments.iterator());
+    int start0 = actualColumn(it.peek());
+    {
+      List<ExpressionTree> row = new ArrayList<>();
+      row.add(it.next());
+      while (it.hasNext() && actualColumn(it.peek()) > start0) {
+        row.add(it.next());
+      }
+      if (!it.hasNext()) {
+        return -1;
+      }
+      if (rowLength(row) <= 1) {
+        return -1;
+      }
+      rows.add(row);
+    }
+    while (it.hasNext()) {
+      List<ExpressionTree> row = new ArrayList<>();
+      int start = actualColumn(it.peek());
+      if (start != start0) {
+        return -1;
+      }
+      row.add(it.next());
+      while (it.hasNext() && actualColumn(it.peek()) > start0) {
+        row.add(it.next());
+      }
+      rows.add(row);
+    }
+    int size0 = rows.get(0).size();
+    if (!expressionsAreParallel(rows, 0, rows.size())) {
+      return -1;
+    }
+    for (int i = 1; i < size0; i++) {
+      if (!expressionsAreParallel(rows, i, rows.size() / 2 + 1)) {
+        return -1;
+      }
+    }
+    // if there are only two rows, they must be the same length
+    if (rows.size() == 2) {
+      if (size0 == rows.get(1).size()) {
+        return size0;
+      }
+      return -1;
+    }
+    // allow a ragged trailing row for >= 3 columns
+    for (int i = 1; i < rows.size() - 1; i++) {
+      if (size0 != rows.get(i).size()) {
+        return -1;
+      }
+    }
+    if (size0 < getLast(rows).size()) {
+      return -1;
+    }
+    return size0;
+  }
+
+  static int rowLength(List<? extends ExpressionTree> row) {
+    int size = 0;
+    for (ExpressionTree tree : row) {
+      if (tree.getKind() != NEW_ARRAY) {
+        size++;
+        continue;
+      }
+      NewArrayTree array = (NewArrayTree) tree;
+      if (array.getInitializers() == null) {
+        size++;
+        continue;
+      }
+      size += rowLength(array.getInitializers());
+    }
+    return size;
+  }
+
+  private Integer actualColumn(ExpressionTree expression) {
+    Map<Integer, Integer> positionToColumnMap = builder.getInput().getPositionToColumnMap();
+    return positionToColumnMap.get(builder.actualStartColumn(getStartPosition(expression)));
+  }
+
+  /** Returns true if {@code atLeastM} of the expressions in the given column are the same kind. */
+  private static boolean expressionsAreParallel(
+      List<List<ExpressionTree>> rows, int column, int atLeastM) {
+    Multiset<Tree.Kind> nodeTypes = HashMultiset.create();
+    for (List<? extends ExpressionTree> row : rows) {
+      if (column >= row.size()) {
+        continue;
+      }
+      // Treat UnaryTree expressions as their underlying type for the comparison (so, for example
+      // -ve and +ve numeric literals are considered the same).
+      if (row.get(column) instanceof UnaryTree) {
+        nodeTypes.add(((UnaryTree) row.get(column)).getExpression().getKind());
+      } else {
+        nodeTypes.add(row.get(column).getKind());
+      }
+    }
+    for (Multiset.Entry<Tree.Kind> nodeType : nodeTypes.entrySet()) {
+      if (nodeType.getCount() >= atLeastM) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // General helper functions.
+
+  enum DeclarationKind {
+    NONE,
+    FIELD,
+    PARAMETER
+  }
+
+  /** Declare one variable or variable-like thing. */
+  int declareOne(
+      DeclarationKind kind,
+      Direction annotationsDirection,
+      Optional<ModifiersTree> modifiers,
+      Tree type,
+      Name name,
+      String op,
+      String equals,
+      Optional<ExpressionTree> initializer,
+      Optional<String> trailing,
+      Optional<ExpressionTree> receiverExpression,
+      Optional<TypeWithDims> typeWithDims) {
+
+    BreakTag typeBreak = genSym();
+    BreakTag verticalAnnotationBreak = genSym();
+
+    // If the node is a field declaration, try to output any declaration
+    // annotations in-line. If the entire declaration doesn't fit on a single
+    // line, fall back to one-per-line.
+    boolean isField = kind == DeclarationKind.FIELD;
+
+    if (isField) {
+      builder.blankLineWanted(BlankLineWanted.conditional(verticalAnnotationBreak));
+    }
+
+    Deque<List<? extends AnnotationTree>> dims =
+        new ArrayDeque<>(
+            typeWithDims.isPresent() ? typeWithDims.get().dims : Collections.emptyList());
+    int baseDims = 0;
+
+    builder.open(
+        kind == DeclarationKind.PARAMETER
+                && (modifiers.isPresent() && !modifiers.get().getAnnotations().isEmpty())
+            ? plusFour
+            : ZERO);
+    {
+      if (modifiers.isPresent()) {
+        visitAndBreakModifiers(
+            modifiers.get(), annotationsDirection, Optional.of(verticalAnnotationBreak));
+      }
+      boolean isVar =
+          builder.peekToken().get().equals("var")
+              && (!name.contentEquals("var") || builder.peekToken(1).get().equals("var"));
+      boolean hasType = type != null || isVar;
+      builder.open(hasType ? plusFour : ZERO);
+      {
+        builder.open(ZERO);
+        {
+          builder.open(ZERO);
+          {
+            if (typeWithDims.isPresent() && typeWithDims.get().node != null) {
+              scan(typeWithDims.get().node, null);
+              int totalDims = dims.size();
+              builder.open(plusFour);
+              maybeAddDims(dims);
+              builder.close();
+              baseDims = totalDims - dims.size();
+            } else if (isVar) {
+              token("var");
+            } else {
+              scan(type, null);
+            }
+          }
+          builder.close();
+
+          if (hasType) {
+            builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO, Optional.of(typeBreak));
+          }
+
+          // conditionally ident the name and initializer +4 if the type spans
+          // multiple lines
+          builder.open(Indent.If.make(typeBreak, plusFour, ZERO));
+          if (receiverExpression.isPresent()) {
+            scan(receiverExpression.get(), null);
+          } else {
+            visit(name);
+          }
+          builder.op(op);
+        }
+        maybeAddDims(dims);
+        builder.close();
+      }
+      builder.close();
+
+      if (initializer.isPresent()) {
+        builder.space();
+        token(equals);
+        if (initializer.get().getKind() == Tree.Kind.NEW_ARRAY
+            && ((NewArrayTree) initializer.get()).getType() == null) {
+          builder.open(minusFour);
+          builder.space();
+          initializer.get().accept(this, null);
+          builder.close();
+        } else {
+          builder.open(Indent.If.make(typeBreak, plusFour, ZERO));
+          {
+            builder.breakToFill(" ");
+            scan(initializer.get(), null);
+          }
+          builder.close();
+        }
+      }
+      if (trailing.isPresent() && builder.peekToken().equals(trailing)) {
+        builder.guessToken(trailing.get());
+      }
+
+      // end of conditional name and initializer indent
+      builder.close();
+    }
+    builder.close();
+
+    if (isField) {
+      builder.blankLineWanted(BlankLineWanted.conditional(verticalAnnotationBreak));
+    }
+
+    return baseDims;
+  }
+
+  private void maybeAddDims(Deque<List<? extends AnnotationTree>> annotations) {
+    maybeAddDims(new ArrayDeque<>(), annotations);
+  }
+
+  /**
+   * The compiler does not always preserve the concrete syntax of annotated array dimensions, and
+   * mixed-notation array dimensions. Use look-ahead to preserve the original syntax.
+   *
+   * <p>It is assumed that any number of regular dimension specifiers ({@code []} with no
+   * annotations) may be present in the input.
+   *
+   * @param dimExpressions an ordered list of dimension expressions (e.g. the {@code 0} in {@code
+   *     new int[0]}
+   * @param annotations an ordered list of type annotations grouped by dimension (e.g. {@code
+   *     [[@A, @B], [@C]]} for {@code int @A [] @B @C []}
+   */
+  private void maybeAddDims(
+      Deque<ExpressionTree> dimExpressions, Deque<List<? extends AnnotationTree>> annotations) {
+    boolean lastWasAnnotation = false;
+    while (builder.peekToken().isPresent()) {
+      switch (builder.peekToken().get()) {
+        case "@":
+          if (annotations.isEmpty()) {
+            return;
+          }
+          List<? extends AnnotationTree> dimAnnotations = annotations.removeFirst();
+          if (dimAnnotations.isEmpty()) {
+            continue;
+          }
+          builder.breakToFill(" ");
+          visitAnnotations(dimAnnotations, BreakOrNot.NO, BreakOrNot.NO);
+          lastWasAnnotation = true;
+          break;
+        case "[":
+          if (lastWasAnnotation) {
+            builder.breakToFill(" ");
+          } else {
+            builder.breakToFill();
+          }
+          token("[");
+          if (!builder.peekToken().get().equals("]")) {
+            scan(dimExpressions.removeFirst(), null);
+          }
+          token("]");
+          lastWasAnnotation = false;
+          break;
+        case ".":
+          if (!builder.peekToken().get().equals(".") || !builder.peekToken(1).get().equals(".")) {
+            return;
+          }
+          if (lastWasAnnotation) {
+            builder.breakToFill(" ");
+          } else {
+            builder.breakToFill();
+          }
+          builder.op("...");
+          lastWasAnnotation = false;
+          break;
+        default:
+          return;
+      }
+    }
+  }
+
+  private void declareMany(List<VariableTree> fragments, Direction annotationDirection) {
+    builder.open(ZERO);
+
+    ModifiersTree modifiers = fragments.get(0).getModifiers();
+    Tree type = fragments.get(0).getType();
+
+    visitAndBreakModifiers(
+        modifiers, annotationDirection, /* declarationAnnotationBreak= */ Optional.empty());
+    builder.open(plusFour);
+    builder.open(ZERO);
+    TypeWithDims extractedDims = DimensionHelpers.extractDims(type, SortedDims.YES);
+    Deque<List<? extends AnnotationTree>> dims = new ArrayDeque<>(extractedDims.dims);
+    scan(extractedDims.node, null);
+    int baseDims = dims.size();
+    maybeAddDims(dims);
+    baseDims = baseDims - dims.size();
+    boolean first = true;
+    for (VariableTree fragment : fragments) {
+      if (!first) {
+        token(",");
+      }
+      TypeWithDims fragmentDims = variableFragmentDims(first, baseDims, fragment.getType());
+      dims = new ArrayDeque<>(fragmentDims.dims);
+      builder.breakOp(" ");
+      builder.open(ZERO);
+      maybeAddDims(dims);
+      visit(fragment.getName());
+      maybeAddDims(dims);
+      ExpressionTree initializer = fragment.getInitializer();
+      if (initializer != null) {
+        builder.space();
+        token("=");
+        builder.open(plusFour);
+        builder.breakOp(" ");
+        scan(initializer, null);
+        builder.close();
+      }
+      builder.close();
+      if (first) {
+        builder.close();
+      }
+      first = false;
+    }
+    builder.close();
+    token(";");
+    builder.close();
+  }
+
+  /** Add a list of declarations. */
+  protected void addBodyDeclarations(
+      List<? extends Tree> bodyDeclarations, BracesOrNot braces, FirstDeclarationsOrNot first0) {
+    if (bodyDeclarations.isEmpty()) {
+      if (braces.isYes()) {
+        builder.space();
+        tokenBreakTrailingComment("{", plusTwo);
+        builder.blankLineWanted(BlankLineWanted.NO);
+        builder.open(ZERO);
+        token("}", plusTwo);
+        builder.close();
+      }
+    } else {
+      if (braces.isYes()) {
+        builder.space();
+        tokenBreakTrailingComment("{", plusTwo);
+        builder.open(ZERO);
+      }
+      builder.open(plusTwo);
+      boolean first = first0.isYes();
+      boolean lastOneGotBlankLineBefore = false;
+      PeekingIterator<Tree> it = Iterators.peekingIterator(bodyDeclarations.iterator());
+      while (it.hasNext()) {
+        Tree bodyDeclaration = it.next();
+        dropEmptyDeclarations();
+        builder.forcedBreak();
+        boolean thisOneGetsBlankLineBefore =
+            bodyDeclaration.getKind() != VARIABLE || hasJavaDoc(bodyDeclaration);
+        if (first) {
+          builder.blankLineWanted(PRESERVE);
+        } else if (!first && (thisOneGetsBlankLineBefore || lastOneGotBlankLineBefore)) {
+          builder.blankLineWanted(YES);
+        }
+        markForPartialFormat();
+
+        if (bodyDeclaration.getKind() == VARIABLE) {
+          visitVariables(
+              variableFragments(it, bodyDeclaration),
+              DeclarationKind.FIELD,
+              fieldAnnotationDirection(((VariableTree) bodyDeclaration).getModifiers()));
+        } else {
+          scan(bodyDeclaration, null);
+        }
+        first = false;
+        lastOneGotBlankLineBefore = thisOneGetsBlankLineBefore;
+      }
+      dropEmptyDeclarations();
+      builder.forcedBreak();
+      builder.close();
+      builder.forcedBreak();
+      markForPartialFormat();
+      if (braces.isYes()) {
+        builder.blankLineWanted(BlankLineWanted.NO);
+        token("}", plusTwo);
+        builder.close();
+      }
+    }
+  }
+
+  /**
+   * The parser expands multi-variable declarations into separate single-variable declarations. All
+   * of the fragments in the original declaration have the same start position, so we use that as a
+   * signal to collect them and preserve the multi-variable declaration in the output.
+   *
+   * <p>e.g. {@code int x, y;} is parsed as {@code int x; int y;}.
+   */
+  private List<VariableTree> variableFragments(PeekingIterator<? extends Tree> it, Tree first) {
+    List<VariableTree> fragments = new ArrayList<>();
+    if (first.getKind() == VARIABLE) {
+      int start = getStartPosition(first);
+      fragments.add((VariableTree) first);
+      while (it.hasNext()
+          && it.peek().getKind() == VARIABLE
+          && getStartPosition(it.peek()) == start) {
+        fragments.add((VariableTree) it.next());
+      }
+    }
+    return fragments;
+  }
+
+  /** Does this declaration have javadoc preceding it? */
+  private boolean hasJavaDoc(Tree bodyDeclaration) {
+    int position = ((JCTree) bodyDeclaration).getStartPosition();
+    Input.Token token = builder.getInput().getPositionTokenMap().get(position);
+    if (token != null) {
+      for (Input.Tok tok : token.getToksBefore()) {
+        if (tok.getText().startsWith("/**")) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  private static Optional<? extends Input.Token> getNextToken(Input input, int position) {
+    return Optional.ofNullable(input.getPositionTokenMap().get(position));
+  }
+
+  /** Does this list of trees end with the specified token? */
+  private boolean hasTrailingToken(Input input, List<? extends Tree> nodes, String token) {
+    if (nodes.isEmpty()) {
+      return false;
+    }
+    Tree lastNode = getLast(nodes);
+    Optional<? extends Input.Token> nextToken =
+        getNextToken(input, getEndPosition(lastNode, getCurrentPath()));
+    return nextToken.isPresent() && nextToken.get().getTok().getText().equals(token);
+  }
+
+  /**
+   * Can a local with a set of modifiers be declared with horizontal annotations? This is currently
+   * true if there is at most one parameterless annotation, and no others.
+   *
+   * @param modifiers the list of {@link ModifiersTree}s
+   * @return whether the local can be declared with horizontal annotations
+   */
+  private Direction canLocalHaveHorizontalAnnotations(ModifiersTree modifiers) {
+    int parameterlessAnnotations = 0;
+    for (AnnotationTree annotation : modifiers.getAnnotations()) {
+      if (annotation.getArguments().isEmpty()) {
+        parameterlessAnnotations++;
+      }
+    }
+    return parameterlessAnnotations <= 1
+            && parameterlessAnnotations == modifiers.getAnnotations().size()
+        ? Direction.HORIZONTAL
+        : Direction.VERTICAL;
+  }
+
+  /**
+   * Should a field with a set of modifiers be declared with horizontal annotations? This is
+   * currently true if all annotations are parameterless annotations.
+   */
+  private Direction fieldAnnotationDirection(ModifiersTree modifiers) {
+    for (AnnotationTree annotation : modifiers.getAnnotations()) {
+      if (!annotation.getArguments().isEmpty()) {
+        return Direction.VERTICAL;
+      }
+    }
+    return Direction.HORIZONTAL;
+  }
+
+  /**
+   * Emit a {@link Doc.Token}.
+   *
+   * @param token the {@link String} to wrap in a {@link Doc.Token}
+   */
+  protected final void token(String token) {
+    builder.token(
+        token,
+        Doc.Token.RealOrImaginary.REAL,
+        ZERO,
+        /* breakAndIndentTrailingComment= */ Optional.empty());
+  }
+
+  /**
+   * Emit a {@link Doc.Token}.
+   *
+   * @param token the {@link String} to wrap in a {@link Doc.Token}
+   * @param plusIndentCommentsBefore extra indent for comments before this token
+   */
+  protected final void token(String token, Indent plusIndentCommentsBefore) {
+    builder.token(
+        token,
+        Doc.Token.RealOrImaginary.REAL,
+        plusIndentCommentsBefore,
+        /* breakAndIndentTrailingComment= */ Optional.empty());
+  }
+
+  /** Emit a {@link Doc.Token}, and breaks and indents trailing javadoc or block comments. */
+  final void tokenBreakTrailingComment(String token, Indent breakAndIndentTrailingComment) {
+    builder.token(
+        token, Doc.Token.RealOrImaginary.REAL, ZERO, Optional.of(breakAndIndentTrailingComment));
+  }
+
+  protected void markForPartialFormat() {
+    if (!inExpression()) {
+      builder.markForPartialFormat();
+    }
+  }
+
+  /**
+   * Sync to position in the input. If we've skipped outputting any tokens that were present in the
+   * input tokens, output them here and complain.
+   *
+   * @param node the ASTNode holding the input position
+   */
+  protected final void sync(Tree node) {
+    builder.sync(((JCTree) node).getStartPosition());
+  }
+
+  final BreakTag genSym() {
+    return new BreakTag();
+  }
+
+  @Override
+  public final String toString() {
+    return MoreObjects.toStringHelper(this).add("builder", builder).toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java b/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java
new file mode 100644
index 0000000..c059318
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright 2015 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 java.util.Comparator.comparing;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+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 com.google.googlejavaformat.CommentsHelper;
+import com.google.googlejavaformat.Input;
+import com.google.googlejavaformat.Input.Token;
+import com.google.googlejavaformat.Newlines;
+import com.google.googlejavaformat.OpsBuilder.BlankLineWanted;
+import com.google.googlejavaformat.Output;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/*
+ * Throughout this file, {@code i} is an index for input lines, {@code j} is an index for output
+ * lines, {@code ij} is an index into either input or output lines, and {@code k} is an index for
+ * toks.
+ */
+
+/**
+ * {@code JavaOutput} extends {@link Output Output} to represent a Java output document. It includes
+ * methods to emit the output document.
+ */
+public final class JavaOutput extends Output {
+  private final String lineSeparator;
+  private final Input javaInput; // Used to follow along while emitting the output.
+  private final CommentsHelper commentsHelper; // Used to re-flow comments.
+  private final Map<Integer, BlankLineWanted> blankLines = new HashMap<>(); // Info on blank lines.
+  private final RangeSet<Integer> partialFormatRanges = TreeRangeSet.create();
+
+  private final List<String> mutableLines = new ArrayList<>();
+  private final int kN; // The number of tokens or comments in the input, excluding the EOF.
+  private int iLine = 0; // Closest corresponding line number on input.
+  private int lastK = -1; // Last {@link Tok} index output.
+  private int newlinesPending = 0;
+  private StringBuilder lineBuilder = new StringBuilder();
+  private StringBuilder spacesPending = new StringBuilder();
+
+  /**
+   * {@code JavaOutput} constructor.
+   *
+   * @param javaInput the {@link Input}, used to match up blank lines in the output
+   * @param commentsHelper the {@link CommentsHelper}, used to rewrite comments
+   */
+  public JavaOutput(String lineSeparator, Input javaInput, CommentsHelper commentsHelper) {
+    this.lineSeparator = lineSeparator;
+    this.javaInput = javaInput;
+    this.commentsHelper = commentsHelper;
+    kN = javaInput.getkN();
+  }
+
+  @Override
+  public void blankLine(int k, BlankLineWanted wanted) {
+    if (blankLines.containsKey(k)) {
+      blankLines.put(k, blankLines.get(k).merge(wanted));
+    } else {
+      blankLines.put(k, wanted);
+    }
+  }
+
+  @Override
+  public void markForPartialFormat(Token start, Token end) {
+    int lo = JavaOutput.startTok(start).getIndex();
+    int hi = JavaOutput.endTok(end).getIndex();
+    partialFormatRanges.add(Range.closed(lo, hi));
+  }
+
+  // TODO(jdd): Add invariant.
+  @Override
+  public void append(String text, Range<Integer> range) {
+    if (!range.isEmpty()) {
+      boolean sawNewlines = false;
+      // Skip over input line we've passed.
+      int iN = javaInput.getLineCount();
+      while (iLine < iN
+          && (javaInput.getRanges(iLine).isEmpty()
+              || javaInput.getRanges(iLine).upperEndpoint() <= range.lowerEndpoint())) {
+        if (javaInput.getRanges(iLine).isEmpty()) {
+          // Skipped over a blank line.
+          sawNewlines = true;
+        }
+        ++iLine;
+      }
+      /*
+       * Output blank line if we've called {@link OpsBuilder#blankLine}{@code (true)} here, or if
+       * there's a blank line here and it's a comment.
+       */
+      BlankLineWanted wanted = blankLines.getOrDefault(lastK, BlankLineWanted.NO);
+      if (isComment(text) ? sawNewlines : wanted.wanted().orElse(sawNewlines)) {
+        ++newlinesPending;
+      }
+    }
+    if (Newlines.isNewline(text)) {
+      /*
+       * Don't update range information, and swallow extra newlines. The case below for '\n' is for
+       * block comments.
+       */
+      if (newlinesPending == 0) {
+        ++newlinesPending;
+      }
+      spacesPending = new StringBuilder();
+    } else {
+      boolean rangesSet = false;
+      int textN = text.length();
+      for (int i = 0; i < textN; i++) {
+        char c = text.charAt(i);
+        switch (c) {
+          case ' ':
+            spacesPending.append(' ');
+            break;
+          case '\t':
+            spacesPending.append('\t');
+            break;
+          case '\r':
+            if (i + 1 < text.length() && text.charAt(i + 1) == '\n') {
+              i++;
+            }
+            // falls through
+          case '\n':
+            spacesPending = new StringBuilder();
+            ++newlinesPending;
+            break;
+          default:
+            while (newlinesPending > 0) {
+              // drop leading blank lines
+              if (!mutableLines.isEmpty() || lineBuilder.length() > 0) {
+                mutableLines.add(lineBuilder.toString());
+              }
+              lineBuilder = new StringBuilder();
+              rangesSet = false;
+              --newlinesPending;
+            }
+            if (spacesPending.length() > 0) {
+              lineBuilder.append(spacesPending);
+              spacesPending = new StringBuilder();
+            }
+            lineBuilder.append(c);
+            if (!range.isEmpty()) {
+              if (!rangesSet) {
+                while (ranges.size() <= mutableLines.size()) {
+                  ranges.add(Formatter.EMPTY_RANGE);
+                }
+                ranges.set(mutableLines.size(), union(ranges.get(mutableLines.size()), range));
+                rangesSet = true;
+              }
+            }
+        }
+      }
+    }
+    if (!range.isEmpty()) {
+      lastK = range.upperEndpoint();
+    }
+  }
+
+  @Override
+  public void indent(int indent) {
+    spacesPending.append(Strings.repeat(" ", indent));
+  }
+
+  /** Flush any incomplete last line, then add the EOF token into our data structures. */
+  public void flush() {
+    String lastLine = lineBuilder.toString();
+    if (!CharMatcher.whitespace().matchesAllOf(lastLine)) {
+      mutableLines.add(lastLine);
+    }
+    int jN = mutableLines.size();
+    Range<Integer> eofRange = Range.closedOpen(kN, kN + 1);
+    while (ranges.size() < jN) {
+      ranges.add(Formatter.EMPTY_RANGE);
+    }
+    ranges.add(eofRange);
+    setLines(ImmutableList.copyOf(mutableLines));
+  }
+
+  // The following methods can be used after the Output has been built.
+
+  @Override
+  public CommentsHelper getCommentsHelper() {
+    return commentsHelper;
+  }
+
+  /**
+   * Emit a list of {@link Replacement}s to convert from input to output.
+   *
+   * @return a list of {@link Replacement}s, sorted by start index, without overlaps
+   */
+  public ImmutableList<Replacement> getFormatReplacements(RangeSet<Integer> iRangeSet0) {
+    ImmutableList.Builder<Replacement> result = ImmutableList.builder();
+    Map<Integer, Range<Integer>> kToJ = JavaOutput.makeKToIJ(this);
+
+    // Expand the token ranges to align with re-formattable boundaries.
+    RangeSet<Integer> breakableRanges = TreeRangeSet.create();
+    RangeSet<Integer> iRangeSet = iRangeSet0.subRangeSet(Range.closed(0, javaInput.getkN()));
+    for (Range<Integer> iRange : iRangeSet.asRanges()) {
+      Range<Integer> range = expandToBreakableRegions(iRange.canonical(DiscreteDomain.integers()));
+      if (range.equals(EMPTY_RANGE)) {
+        // the range contains only whitespace
+        continue;
+      }
+      breakableRanges.add(range);
+    }
+
+    // Construct replacements for each reformatted region.
+    for (Range<Integer> range : breakableRanges.asRanges()) {
+
+      Input.Tok startTok = startTok(javaInput.getToken(range.lowerEndpoint()));
+      Input.Tok endTok = endTok(javaInput.getToken(range.upperEndpoint() - 1));
+
+      // Add all output lines in the given token range to the replacement.
+      StringBuilder replacement = new StringBuilder();
+
+      int replaceFrom = startTok.getPosition();
+      // Replace leading whitespace in the input with the whitespace from the formatted file
+      while (replaceFrom > 0) {
+        char previous = javaInput.getText().charAt(replaceFrom - 1);
+        if (!CharMatcher.whitespace().matches(previous)) {
+          break;
+        }
+        replaceFrom--;
+      }
+
+      int i = kToJ.get(startTok.getIndex()).lowerEndpoint();
+      // Include leading blank lines from the formatted output, unless the formatted range
+      // starts at the beginning of the file.
+      while (i > 0 && getLine(i - 1).isEmpty()) {
+        i--;
+      }
+      // Write out the formatted range.
+      for (; i < kToJ.get(endTok.getIndex()).upperEndpoint(); i++) {
+        // It's possible to run out of output lines (e.g. if the input ended with
+        // multiple trailing newlines).
+        if (i < getLineCount()) {
+          if (i > 0) {
+            replacement.append(lineSeparator);
+          }
+          replacement.append(getLine(i));
+        }
+      }
+
+      int replaceTo =
+          Math.min(endTok.getPosition() + endTok.length(), javaInput.getText().length());
+      // If the formatted ranged ended in the trailing trivia of the last token before EOF,
+      // format all the way up to EOF to deal with trailing whitespace correctly.
+      if (endTok.getIndex() == javaInput.getkN() - 1) {
+        replaceTo = javaInput.getText().length();
+      }
+      // Replace trailing whitespace in the input with the whitespace from the formatted file.
+      // If the trailing whitespace in the input includes one or more line breaks, preserve the
+      // whitespace after the last newline to avoid re-indenting the line following the formatted
+      // line.
+      int newline = -1;
+      while (replaceTo < javaInput.getText().length()) {
+        char next = javaInput.getText().charAt(replaceTo);
+        if (!CharMatcher.whitespace().matches(next)) {
+          break;
+        }
+        int newlineLength = Newlines.hasNewlineAt(javaInput.getText(), replaceTo);
+        if (newlineLength != -1) {
+          newline = replaceTo;
+          // Skip over the entire newline; don't count the second character of \r\n as a newline.
+          replaceTo += newlineLength;
+        } else {
+          replaceTo++;
+        }
+      }
+      if (newline != -1) {
+        replaceTo = newline;
+      }
+
+      if (newline == -1) {
+        // There wasn't an existing trailing newline; add one.
+        replacement.append(lineSeparator);
+      }
+      for (; i < getLineCount(); i++) {
+        String after = getLine(i);
+        int idx = CharMatcher.whitespace().negate().indexIn(after);
+        if (idx == -1) {
+          // Write out trailing empty lines from the formatted output.
+          replacement.append(lineSeparator);
+        } else {
+          if (newline == -1) {
+            // If there wasn't a trailing newline in the input, indent the next line.
+            replacement.append(after.substring(0, idx));
+          }
+          break;
+        }
+      }
+
+      result.add(Replacement.create(replaceFrom, replaceTo, replacement.toString()));
+    }
+    return result.build();
+  }
+
+  /**
+   * Expand a token range to start and end on acceptable boundaries for re-formatting.
+   *
+   * @param iRange the {@link Range} of tokens
+   * @return the expanded token range
+   */
+  private Range<Integer> expandToBreakableRegions(Range<Integer> iRange) {
+    // The original line range.
+    int loTok = iRange.lowerEndpoint();
+    int hiTok = iRange.upperEndpoint() - 1;
+
+    // Expand the token indices to formattable boundaries (e.g. edges of statements).
+    if (!partialFormatRanges.contains(loTok) || !partialFormatRanges.contains(hiTok)) {
+      return EMPTY_RANGE;
+    }
+    loTok = partialFormatRanges.rangeContaining(loTok).lowerEndpoint();
+    hiTok = partialFormatRanges.rangeContaining(hiTok).upperEndpoint();
+    return Range.closedOpen(loTok, hiTok + 1);
+  }
+
+  public static String applyReplacements(String input, List<Replacement> replacements) {
+    replacements = new ArrayList<>(replacements);
+    replacements.sort(comparing((Replacement r) -> r.getReplaceRange().lowerEndpoint()).reversed());
+    StringBuilder writer = new StringBuilder(input);
+    for (Replacement replacement : replacements) {
+      writer.replace(
+          replacement.getReplaceRange().lowerEndpoint(),
+          replacement.getReplaceRange().upperEndpoint(),
+          replacement.getReplacementString());
+    }
+    return writer.toString();
+  }
+
+  /** The earliest position of any Tok in the Token, including leading whitespace. */
+  public static int startPosition(Token token) {
+    int min = token.getTok().getPosition();
+    for (Input.Tok tok : token.getToksBefore()) {
+      min = Math.min(min, tok.getPosition());
+    }
+    return min;
+  }
+
+  /** The earliest non-whitespace Tok in the Token. */
+  public static Input.Tok startTok(Token token) {
+    for (Input.Tok tok : token.getToksBefore()) {
+      if (tok.getIndex() >= 0) {
+        return tok;
+      }
+    }
+    return token.getTok();
+  }
+
+  /** The last non-whitespace Tok in the Token. */
+  public static Input.Tok endTok(Token token) {
+    for (int i = token.getToksAfter().size() - 1; i >= 0; i--) {
+      Input.Tok tok = token.getToksAfter().get(i);
+      if (tok.getIndex() >= 0) {
+        return tok;
+      }
+    }
+    return token.getTok();
+  }
+
+  private boolean isComment(String text) {
+    return text.startsWith("//") || text.startsWith("/*");
+  }
+
+  private static Range<Integer> union(Range<Integer> x, Range<Integer> y) {
+    return x.isEmpty() ? y : y.isEmpty() ? x : x.span(y).canonical(DiscreteDomain.integers());
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this)
+        .add("iLine", iLine)
+        .add("lastK", lastK)
+        .add("spacesPending", spacesPending.toString().replace("\t", "\\t"))
+        .add("newlinesPending", newlinesPending)
+        .add("blankLines", blankLines)
+        .add("super", super.toString())
+        .toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java b/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java
new file mode 100644
index 0000000..a8c9efd
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2016 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.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.sun.tools.javac.parser.JavaTokenizer;
+import com.sun.tools.javac.parser.Scanner;
+import com.sun.tools.javac.parser.ScannerFactory;
+import com.sun.tools.javac.parser.Tokens.Comment;
+import com.sun.tools.javac.parser.Tokens.Comment.CommentStyle;
+import com.sun.tools.javac.parser.Tokens.Token;
+import com.sun.tools.javac.parser.Tokens.TokenKind;
+import com.sun.tools.javac.parser.UnicodeReader;
+import com.sun.tools.javac.util.Context;
+import java.util.Set;
+
+/** A wrapper around javac's lexer. */
+class JavacTokens {
+
+  /** The lexer eats terminal comments, so feed it one we don't care about. */
+  // TODO(b/33103797): fix javac and remove the work-around
+  private static final CharSequence EOF_COMMENT = "\n//EOF";
+
+  /** An unprocessed input token, including whitespace and comments. */
+  static class RawTok {
+    private final String stringVal;
+    private final TokenKind kind;
+    private final int pos;
+    private final int endPos;
+
+    RawTok(String stringVal, TokenKind kind, int pos, int endPos) {
+      this.stringVal = stringVal;
+      this.kind = kind;
+      this.pos = pos;
+      this.endPos = endPos;
+    }
+
+    /** The token kind, or {@code null} for whitespace and comments. */
+    public TokenKind kind() {
+      return kind;
+    }
+
+    /** The start position. */
+    public int pos() {
+      return pos;
+    }
+
+    /** The end position. */
+    public int endPos() {
+      return endPos;
+    }
+
+    /** The escaped string value of a literal, or {@code null} for other tokens. */
+    public String stringVal() {
+      return stringVal;
+    }
+  }
+
+  /** Lex the input and return a list of {@link RawTok}s. */
+  public static ImmutableList<RawTok> getTokens(
+      String source, Context context, Set<TokenKind> stopTokens) {
+    if (source == null) {
+      return ImmutableList.of();
+    }
+    ScannerFactory fac = ScannerFactory.instance(context);
+    char[] buffer = (source + EOF_COMMENT).toCharArray();
+    Scanner scanner =
+        new AccessibleScanner(fac, new CommentSavingTokenizer(fac, buffer, buffer.length));
+    ImmutableList.Builder<RawTok> tokens = ImmutableList.builder();
+    int end = source.length();
+    int last = 0;
+    do {
+      scanner.nextToken();
+      Token t = scanner.token();
+      if (t.comments != null) {
+        for (Comment c : Lists.reverse(t.comments)) {
+          if (last < c.getSourcePos(0)) {
+            tokens.add(new RawTok(null, null, last, c.getSourcePos(0)));
+          }
+          tokens.add(
+              new RawTok(null, null, c.getSourcePos(0), c.getSourcePos(0) + c.getText().length()));
+          last = c.getSourcePos(0) + c.getText().length();
+        }
+      }
+      if (stopTokens.contains(t.kind)) {
+        if (t.kind != TokenKind.EOF) {
+          end = t.pos;
+        }
+        break;
+      }
+      if (last < t.pos) {
+        tokens.add(new RawTok(null, null, last, t.pos));
+      }
+      tokens.add(
+          new RawTok(
+              t.kind == TokenKind.STRINGLITERAL ? "\"" + t.stringVal() + "\"" : null,
+              t.kind,
+              t.pos,
+              t.endPos));
+      last = t.endPos;
+    } while (scanner.token().kind != TokenKind.EOF);
+    if (last < end) {
+      tokens.add(new RawTok(null, null, last, end));
+    }
+    return tokens.build();
+  }
+
+  /** A {@link JavaTokenizer} that saves comments. */
+  static class CommentSavingTokenizer extends JavaTokenizer {
+    CommentSavingTokenizer(ScannerFactory fac, char[] buffer, int length) {
+      super(fac, buffer, length);
+    }
+
+    @Override
+    protected Comment processComment(int pos, int endPos, CommentStyle style) {
+      char[] buf = reader.getRawCharacters(pos, endPos);
+      return new CommentWithTextAndPosition(
+          pos, endPos, new AccessibleReader(fac, buf, buf.length), style);
+    }
+  }
+
+  /** A {@link Comment} that saves its text and start position. */
+  static class CommentWithTextAndPosition implements Comment {
+
+    private final int pos;
+    private final int endPos;
+    private final AccessibleReader reader;
+    private final CommentStyle style;
+
+    private String text = null;
+
+    public CommentWithTextAndPosition(
+        int pos, int endPos, AccessibleReader reader, CommentStyle style) {
+      this.pos = pos;
+      this.endPos = endPos;
+      this.reader = reader;
+      this.style = style;
+    }
+
+    /**
+     * Returns the source position of the character at index {@code index} in the comment text.
+     *
+     * <p>The handling of javadoc comments in javac has more logic to skip over leading whitespace
+     * and '*' characters when indexing into doc comments, but we don't need any of that.
+     */
+    @Override
+    public int getSourcePos(int index) {
+      checkArgument(
+          0 <= index && index < (endPos - pos),
+          "Expected %s in the range [0, %s)",
+          index,
+          endPos - pos);
+      return pos + index;
+    }
+
+    @Override
+    public CommentStyle getStyle() {
+      return style;
+    }
+
+    @Override
+    public String getText() {
+      String text = this.text;
+      if (text == null) {
+        this.text = text = new String(reader.getRawCharacters());
+      }
+      return text;
+    }
+
+    /**
+     * We don't care about {@code @deprecated} javadoc tags (see the DepAnn check).
+     *
+     * @return false
+     */
+    @Override
+    public boolean isDeprecated() {
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return String.format("Comment: '%s'", getText());
+    }
+  }
+
+  // Scanner(ScannerFactory, JavaTokenizer) is package-private
+  static class AccessibleScanner extends Scanner {
+    protected AccessibleScanner(ScannerFactory fac, JavaTokenizer tokenizer) {
+      super(fac, tokenizer);
+    }
+  }
+
+  // UnicodeReader(ScannerFactory, char[], int) is package-private
+  static class AccessibleReader extends UnicodeReader {
+    protected AccessibleReader(ScannerFactory fac, char[] buffer, int length) {
+      super(fac, buffer, length);
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/Main.java b/core/src/main/java/com/google/googlejavaformat/java/Main.java
new file mode 100644
index 0000000..9231bda
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/Main.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2015 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 java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.io.ByteStreams;
+import com.google.googlejavaformat.FormatterDiagnostic;
+import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+/** The main class for the Java formatter CLI. */
+public final class Main {
+  private static final int MAX_THREADS = 20;
+  private static final String STDIN_FILENAME = "<stdin>";
+
+  static final String versionString() {
+    return "google-java-format: Version " + GoogleJavaFormatVersion.version();
+  }
+
+  private final PrintWriter outWriter;
+  private final PrintWriter errWriter;
+  private final InputStream inStream;
+
+  public Main(PrintWriter outWriter, PrintWriter errWriter, InputStream inStream) {
+    this.outWriter = outWriter;
+    this.errWriter = errWriter;
+    this.inStream = inStream;
+  }
+
+  /**
+   * The main method for the formatter, with some number of file names to format. We process them in
+   * parallel, but we must be careful; if multiple file names refer to the same file (which is hard
+   * to determine), we must serialize their updates.
+   *
+   * @param args the command-line arguments
+   */
+  public static void main(String[] args) {
+    int result;
+    PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out, UTF_8));
+    PrintWriter err = new PrintWriter(new OutputStreamWriter(System.err, UTF_8));
+    try {
+      Main formatter = new Main(out, err, System.in);
+      result = formatter.format(args);
+    } catch (UsageException e) {
+      err.print(e.getMessage());
+      result = 0;
+    } finally {
+      err.flush();
+      out.flush();
+    }
+    System.exit(result);
+  }
+
+  /**
+   * The main entry point for the formatter, with some number of file names to format. We process
+   * them in parallel, but we must be careful; if multiple file names refer to the same file (which
+   * is hard to determine), we must serialize their update.
+   *
+   * @param args the command-line arguments
+   */
+  public int format(String... args) throws UsageException {
+    CommandLineOptions parameters = processArgs(args);
+    if (parameters.version()) {
+      errWriter.println(versionString());
+      return 0;
+    }
+    if (parameters.help()) {
+      throw new UsageException();
+    }
+
+    JavaFormatterOptions options =
+        JavaFormatterOptions.builder()
+            .style(parameters.aosp() ? Style.AOSP : Style.GOOGLE)
+            .formatJavadoc(parameters.formatJavadoc())
+            .build();
+
+    if (parameters.stdin()) {
+      return formatStdin(parameters, options);
+    } else {
+      return formatFiles(parameters, options);
+    }
+  }
+
+  private int formatFiles(CommandLineOptions parameters, JavaFormatterOptions options) {
+    int numThreads = Math.min(MAX_THREADS, parameters.files().size());
+    ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
+
+    Map<Path, String> inputs = new LinkedHashMap<>();
+    Map<Path, Future<String>> results = new LinkedHashMap<>();
+    boolean allOk = true;
+
+    for (String fileName : parameters.files()) {
+      if (!fileName.endsWith(".java")) {
+        errWriter.println("Skipping non-Java file: " + fileName);
+        continue;
+      }
+      Path path = Paths.get(fileName);
+      String input;
+      try {
+        input = new String(Files.readAllBytes(path), UTF_8);
+        inputs.put(path, input);
+        results.put(
+            path, executorService.submit(new FormatFileCallable(parameters, input, options)));
+      } catch (IOException e) {
+        errWriter.println(fileName + ": could not read file: " + e.getMessage());
+        allOk = false;
+      }
+    }
+
+    for (Map.Entry<Path, Future<String>> result : results.entrySet()) {
+      Path path = result.getKey();
+      String formatted;
+      try {
+        formatted = result.getValue().get();
+      } catch (InterruptedException e) {
+        errWriter.println(e.getMessage());
+        allOk = false;
+        continue;
+      } catch (ExecutionException e) {
+        if (e.getCause() instanceof FormatterException) {
+          for (FormatterDiagnostic diagnostic : ((FormatterException) e.getCause()).diagnostics()) {
+            errWriter.println(path + ":" + diagnostic.toString());
+          }
+        } else {
+          errWriter.println(path + ": error: " + e.getCause().getMessage());
+          e.getCause().printStackTrace(errWriter);
+        }
+        allOk = false;
+        continue;
+      }
+      boolean changed = !formatted.equals(inputs.get(path));
+      if (changed && parameters.setExitIfChanged()) {
+        allOk = false;
+      }
+      if (parameters.inPlace()) {
+        if (!changed) {
+          continue; // preserve original file
+        }
+        try {
+          Files.write(path, formatted.getBytes(UTF_8));
+        } catch (IOException e) {
+          errWriter.println(path + ": could not write file: " + e.getMessage());
+          allOk = false;
+          continue;
+        }
+      } else if (parameters.dryRun()) {
+        if (changed) {
+          outWriter.println(path);
+        }
+      } else {
+        outWriter.write(formatted);
+      }
+    }
+    return allOk ? 0 : 1;
+  }
+
+  private int formatStdin(CommandLineOptions parameters, JavaFormatterOptions options) {
+    String input;
+    try {
+      input = new String(ByteStreams.toByteArray(inStream), UTF_8);
+    } catch (IOException e) {
+      throw new IOError(e);
+    }
+    String stdinFilename = parameters.assumeFilename().orElse(STDIN_FILENAME);
+    boolean ok = true;
+    try {
+      String output = new FormatFileCallable(parameters, input, options).call();
+      boolean changed = !input.equals(output);
+      if (changed && parameters.setExitIfChanged()) {
+        ok = false;
+      }
+      if (parameters.dryRun()) {
+        if (changed) {
+          outWriter.println(stdinFilename);
+        }
+      } else {
+        outWriter.write(output);
+      }
+    } catch (FormatterException e) {
+      for (FormatterDiagnostic diagnostic : e.diagnostics()) {
+        errWriter.println(stdinFilename + ":" + diagnostic.toString());
+      }
+      ok = false;
+      // TODO(cpovirk): Catch other types of exception (as we do in the formatFiles case).
+    }
+    return ok ? 0 : 1;
+  }
+
+  /** Parses and validates command-line flags. */
+  public static CommandLineOptions processArgs(String... args) throws UsageException {
+    CommandLineOptions parameters;
+    try {
+      parameters = CommandLineOptionsParser.parse(Arrays.asList(args));
+    } catch (IllegalArgumentException e) {
+      throw new UsageException(e.getMessage());
+    } catch (Throwable t) {
+      t.printStackTrace();
+      throw new UsageException(t.getMessage());
+    }
+    int filesToFormat = parameters.files().size();
+    if (parameters.stdin()) {
+      filesToFormat++;
+    }
+
+    if (parameters.inPlace() && parameters.files().isEmpty()) {
+      throw new UsageException("in-place formatting was requested but no files were provided");
+    }
+    if (parameters.isSelection() && filesToFormat != 1) {
+      throw new UsageException("partial formatting is only support for a single file");
+    }
+    if (parameters.offsets().size() != parameters.lengths().size()) {
+      throw new UsageException("-offsets and -lengths flags must be provided in matching pairs");
+    }
+    if (filesToFormat <= 0 && !parameters.version() && !parameters.help()) {
+      throw new UsageException("no files were provided");
+    }
+    if (parameters.stdin() && !parameters.files().isEmpty()) {
+      throw new UsageException("cannot format from standard input and files simultaneously");
+    }
+    if (parameters.assumeFilename().isPresent() && !parameters.stdin()) {
+      throw new UsageException(
+          "--assume-filename is only supported when formatting standard input");
+    }
+    if (parameters.dryRun() && parameters.inPlace()) {
+      throw new UsageException("cannot use --dry-run and --in-place at the same time");
+    }
+    return parameters;
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java b/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java
new file mode 100644
index 0000000..f7f610b
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2016 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 com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeMap;
+import com.google.googlejavaformat.Input.Tok;
+import com.google.googlejavaformat.Input.Token;
+import com.sun.tools.javac.parser.Tokens.TokenKind;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.lang.model.element.Modifier;
+
+/** Fixes sequences of modifiers to be in JLS order. */
+final class ModifierOrderer {
+
+  /**
+   * Returns the {@link javax.lang.model.element.Modifier} for the given token kind, or {@code
+   * null}.
+   */
+  private static Modifier getModifier(TokenKind kind) {
+    if (kind == null) {
+      return null;
+    }
+    switch (kind) {
+      case PUBLIC:
+        return Modifier.PUBLIC;
+      case PROTECTED:
+        return Modifier.PROTECTED;
+      case PRIVATE:
+        return Modifier.PRIVATE;
+      case ABSTRACT:
+        return Modifier.ABSTRACT;
+      case STATIC:
+        return Modifier.STATIC;
+      case DEFAULT:
+        return Modifier.DEFAULT;
+      case FINAL:
+        return Modifier.FINAL;
+      case TRANSIENT:
+        return Modifier.TRANSIENT;
+      case VOLATILE:
+        return Modifier.VOLATILE;
+      case SYNCHRONIZED:
+        return Modifier.SYNCHRONIZED;
+      case NATIVE:
+        return Modifier.NATIVE;
+      case STRICTFP:
+        return Modifier.STRICTFP;
+      default:
+        return null;
+    }
+  }
+
+  /** Reorders all modifiers in the given text to be in JLS order. */
+  static JavaInput reorderModifiers(String text) throws FormatterException {
+    return reorderModifiers(
+        new JavaInput(text), ImmutableList.of(Range.closedOpen(0, text.length())));
+  }
+
+  /**
+   * Reorders all modifiers in the given text and within the given character ranges to be in JLS
+   * order.
+   */
+  static JavaInput reorderModifiers(JavaInput javaInput, Collection<Range<Integer>> characterRanges)
+      throws FormatterException {
+    if (javaInput.getTokens().isEmpty()) {
+      // There weren't any tokens, possible because of a lexing error.
+      // Errors about invalid input will be reported later after parsing.
+      return javaInput;
+    }
+    RangeSet<Integer> tokenRanges = javaInput.characterRangesToTokenRanges(characterRanges);
+    Iterator<? extends Token> it = javaInput.getTokens().iterator();
+    TreeRangeMap<Integer, String> replacements = TreeRangeMap.create();
+    while (it.hasNext()) {
+      Token token = it.next();
+      if (!tokenRanges.contains(token.getTok().getIndex())) {
+        continue;
+      }
+      Modifier mod = asModifier(token);
+      if (mod == null) {
+        continue;
+      }
+
+      List<Token> modifierTokens = new ArrayList<>();
+      List<Modifier> mods = new ArrayList<>();
+
+      int begin = token.getTok().getPosition();
+      mods.add(mod);
+      modifierTokens.add(token);
+
+      int end = -1;
+      while (it.hasNext()) {
+        token = it.next();
+        mod = asModifier(token);
+        if (mod == null) {
+          break;
+        }
+        mods.add(mod);
+        modifierTokens.add(token);
+        end = token.getTok().getPosition() + token.getTok().length();
+      }
+
+      if (!Ordering.natural().isOrdered(mods)) {
+        Collections.sort(mods);
+        StringBuilder replacement = new StringBuilder();
+        for (int i = 0; i < mods.size(); i++) {
+          if (i > 0) {
+            addTrivia(replacement, modifierTokens.get(i).getToksBefore());
+          }
+          replacement.append(mods.get(i).toString());
+          if (i < (modifierTokens.size() - 1)) {
+            addTrivia(replacement, modifierTokens.get(i).getToksAfter());
+          }
+        }
+        replacements.put(Range.closedOpen(begin, end), replacement.toString());
+      }
+    }
+    return applyReplacements(javaInput, replacements);
+  }
+
+  private static void addTrivia(StringBuilder replacement, ImmutableList<? extends Tok> toks) {
+    for (Tok tok : toks) {
+      replacement.append(tok.getText());
+    }
+  }
+
+  /**
+   * Returns the given token as a {@link javax.lang.model.element.Modifier}, or {@code null} if it
+   * is not a modifier.
+   */
+  private static Modifier asModifier(Token token) {
+    return getModifier(((JavaInput.Tok) token.getTok()).kind());
+  }
+
+  /** Applies replacements to the given string. */
+  private static JavaInput applyReplacements(
+      JavaInput javaInput, TreeRangeMap<Integer, String> replacementMap) throws FormatterException {
+    // process in descending order so the replacement ranges aren't perturbed if any replacements
+    // differ in size from the input
+    Map<Range<Integer>, String> ranges = replacementMap.asDescendingMapOfRanges();
+    if (ranges.isEmpty()) {
+      return javaInput;
+    }
+    StringBuilder sb = new StringBuilder(javaInput.getText());
+    for (Entry<Range<Integer>, String> entry : ranges.entrySet()) {
+      Range<Integer> range = entry.getKey();
+      sb.replace(range.lowerEndpoint(), range.upperEndpoint(), entry.getValue());
+    }
+    return new JavaInput(sb.toString());
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java b/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java
new file mode 100644
index 0000000..d939480
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java
@@ -0,0 +1,321 @@
+/*
+ * 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.java;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeMap;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeMap;
+import com.google.common.collect.TreeRangeSet;
+import com.google.googlejavaformat.Newlines;
+import com.sun.source.doctree.DocCommentTree;
+import com.sun.source.doctree.ReferenceTree;
+import com.sun.source.tree.IdentifierTree;
+import com.sun.source.tree.ImportTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.util.DocTreePath;
+import com.sun.source.util.DocTreePathScanner;
+import com.sun.source.util.TreePathScanner;
+import com.sun.source.util.TreeScanner;
+import com.sun.tools.javac.api.JavacTrees;
+import com.sun.tools.javac.file.JavacFileManager;
+import com.sun.tools.javac.parser.JavacParser;
+import com.sun.tools.javac.parser.ParserFactory;
+import com.sun.tools.javac.tree.DCTree;
+import com.sun.tools.javac.tree.DCTree.DCReference;
+import com.sun.tools.javac.tree.JCTree;
+import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
+import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
+import com.sun.tools.javac.tree.JCTree.JCIdent;
+import com.sun.tools.javac.tree.JCTree.JCImport;
+import com.sun.tools.javac.util.Context;
+import com.sun.tools.javac.util.Log;
+import com.sun.tools.javac.util.Options;
+import java.io.IOError;
+import java.io.IOException;
+import java.net.URI;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.DiagnosticListener;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Removes unused imports from a source file. Imports that are only used in javadoc are also
+ * removed, and the references in javadoc are replaced with fully qualified names.
+ */
+public class RemoveUnusedImports {
+
+  // Visits an AST, recording all simple names that could refer to imported
+  // types and also any javadoc references that could refer to imported
+  // types (`@link`, `@see`, `@throws`, etc.)
+  //
+  // No attempt is made to determine whether simple names occur in contexts
+  // where they are type names, so there will be false positives. For example,
+  // `List` is not identified as unused import below:
+  //
+  // ```
+  // import java.util.List;
+  // class List {}
+  // ```
+  //
+  // This is still reasonably effective in practice because type names differ
+  // from other kinds of names in casing convention, and simple name
+  // clashes between imported and declared types are rare.
+  private static class UnusedImportScanner extends TreePathScanner<Void, Void> {
+
+    private final Set<String> usedNames = new LinkedHashSet<>();
+    private final Multimap<String, Range<Integer>> usedInJavadoc = HashMultimap.create();
+    final JavacTrees trees;
+    final DocTreeScanner docTreeSymbolScanner;
+
+    private UnusedImportScanner(JavacTrees trees) {
+      this.trees = trees;
+      docTreeSymbolScanner = new DocTreeScanner();
+    }
+
+    /** Skip the imports themselves when checking for usage. */
+    @Override
+    public Void visitImport(ImportTree importTree, Void usedSymbols) {
+      return null;
+    }
+
+    @Override
+    public Void visitIdentifier(IdentifierTree tree, Void unused) {
+      if (tree == null) {
+        return null;
+      }
+      usedNames.add(tree.getName().toString());
+      return null;
+    }
+
+    @Override
+    public Void scan(Tree tree, Void unused) {
+      if (tree == null) {
+        return null;
+      }
+      scanJavadoc();
+      return super.scan(tree, unused);
+    }
+
+    private void scanJavadoc() {
+      if (getCurrentPath() == null) {
+        return;
+      }
+      DocCommentTree commentTree = trees.getDocCommentTree(getCurrentPath());
+      if (commentTree == null) {
+        return;
+      }
+      docTreeSymbolScanner.scan(new DocTreePath(getCurrentPath(), commentTree), null);
+    }
+
+    // scan javadoc comments, checking for references to imported types
+    class DocTreeScanner extends DocTreePathScanner<Void, Void> {
+      @Override
+      public Void visitIdentifier(com.sun.source.doctree.IdentifierTree node, Void aVoid) {
+        return null;
+      }
+
+      @Override
+      public Void visitReference(ReferenceTree referenceTree, Void unused) {
+        DCReference reference = (DCReference) referenceTree;
+        long basePos =
+            reference.getSourcePosition((DCTree.DCDocComment) getCurrentPath().getDocComment());
+        // the position of trees inside the reference node aren't stored, but the qualifier's
+        // start position is the beginning of the reference node
+        if (reference.qualifierExpression != null) {
+          new ReferenceScanner(basePos).scan(reference.qualifierExpression, null);
+        }
+        // Record uses inside method parameters. The javadoc tool doesn't use these, but
+        // IntelliJ does.
+        if (reference.paramTypes != null) {
+          for (JCTree param : reference.paramTypes) {
+            // TODO(cushon): get start positions for the parameters
+            new ReferenceScanner(-1).scan(param, null);
+          }
+        }
+        return null;
+      }
+
+      // scans the qualifier and parameters of a javadoc reference for possible type names
+      private class ReferenceScanner extends TreeScanner<Void, Void> {
+        private final long basePos;
+
+        public ReferenceScanner(long basePos) {
+          this.basePos = basePos;
+        }
+
+        @Override
+        public Void visitIdentifier(IdentifierTree node, Void aVoid) {
+          usedInJavadoc.put(
+              node.getName().toString(),
+              basePos != -1
+                  ? Range.closedOpen((int) basePos, (int) basePos + node.getName().length())
+                  : null);
+          return super.visitIdentifier(node, aVoid);
+        }
+      }
+    }
+  }
+
+  public static String removeUnusedImports(final String contents) throws FormatterException {
+    Context context = new Context();
+    JCCompilationUnit unit = parse(context, contents);
+    if (unit == null) {
+      // error handling is done during formatting
+      return contents;
+    }
+    UnusedImportScanner scanner = new UnusedImportScanner(JavacTrees.instance(context));
+    scanner.scan(unit, null);
+    return applyReplacements(
+        contents, buildReplacements(contents, unit, scanner.usedNames, scanner.usedInJavadoc));
+  }
+
+  private static JCCompilationUnit parse(Context context, String javaInput)
+      throws FormatterException {
+    DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
+    context.put(DiagnosticListener.class, diagnostics);
+    Options.instance(context).put("--enable-preview", "true");
+    Options.instance(context).put("allowStringFolding", "false");
+    JCCompilationUnit unit;
+    JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8);
+    try {
+      fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
+    } catch (IOException e) {
+      // impossible
+      throw new IOError(e);
+    }
+    SimpleJavaFileObject source =
+        new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
+          @Override
+          public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+            return javaInput;
+          }
+        };
+    Log.instance(context).useSource(source);
+    ParserFactory parserFactory = ParserFactory.instance(context);
+    JavacParser parser =
+        parserFactory.newParser(
+            javaInput, /*keepDocComments=*/ true, /*keepEndPos=*/ true, /*keepLineMap=*/ true);
+    unit = parser.parseCompilationUnit();
+    unit.sourcefile = source;
+    Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics =
+        Iterables.filter(diagnostics.getDiagnostics(), Formatter::errorDiagnostic);
+    if (!Iterables.isEmpty(errorDiagnostics)) {
+      // error handling is done during formatting
+      throw FormatterException.fromJavacDiagnostics(errorDiagnostics);
+    }
+    return unit;
+  }
+
+  /** Construct replacements to fix unused imports. */
+  private static RangeMap<Integer, String> buildReplacements(
+      String contents,
+      JCCompilationUnit unit,
+      Set<String> usedNames,
+      Multimap<String, Range<Integer>> usedInJavadoc) {
+    RangeMap<Integer, String> replacements = TreeRangeMap.create();
+    for (JCImport importTree : unit.getImports()) {
+      String simpleName = getSimpleName(importTree);
+      if (!isUnused(unit, usedNames, usedInJavadoc, importTree, simpleName)) {
+        continue;
+      }
+      // delete the import
+      int endPosition = importTree.getEndPosition(unit.endPositions);
+      endPosition = Math.max(CharMatcher.isNot(' ').indexIn(contents, endPosition), endPosition);
+      String sep = Newlines.guessLineSeparator(contents);
+      if (endPosition + sep.length() < contents.length()
+          && contents.subSequence(endPosition, endPosition + sep.length()).toString().equals(sep)) {
+        endPosition += sep.length();
+      }
+      replacements.put(Range.closedOpen(importTree.getStartPosition(), endPosition), "");
+    }
+    return replacements;
+  }
+
+  private static String getSimpleName(JCImport importTree) {
+    return importTree.getQualifiedIdentifier() instanceof JCIdent
+        ? ((JCIdent) importTree.getQualifiedIdentifier()).getName().toString()
+        : ((JCFieldAccess) importTree.getQualifiedIdentifier()).getIdentifier().toString();
+  }
+
+  private static boolean isUnused(
+      JCCompilationUnit unit,
+      Set<String> usedNames,
+      Multimap<String, Range<Integer>> usedInJavadoc,
+      JCImport importTree,
+      String simpleName) {
+    String qualifier =
+        ((JCFieldAccess) importTree.getQualifiedIdentifier()).getExpression().toString();
+    if (qualifier.equals("java.lang")) {
+      return true;
+    }
+    if (unit.getPackageName() != null && unit.getPackageName().toString().equals(qualifier)) {
+      return true;
+    }
+    if (importTree.getQualifiedIdentifier() instanceof JCFieldAccess
+        && ((JCFieldAccess) importTree.getQualifiedIdentifier())
+            .getIdentifier()
+            .contentEquals("*")) {
+      return false;
+    }
+
+    if (usedNames.contains(simpleName)) {
+      return false;
+    }
+    if (usedInJavadoc.containsKey(simpleName)) {
+      return false;
+    }
+    return true;
+  }
+
+  /** Applies the replacements to the given source, and re-format any edited javadoc. */
+  private static String applyReplacements(String source, RangeMap<Integer, String> replacements) {
+    // save non-empty fixed ranges for reformatting after fixes are applied
+    RangeSet<Integer> fixedRanges = TreeRangeSet.create();
+
+    // Apply the fixes in increasing order, adjusting ranges to account for
+    // earlier fixes that change the length of the source. The output ranges are
+    // needed so we can reformat fixed regions, otherwise the fixes could just
+    // be applied in descending order without adjusting offsets.
+    StringBuilder sb = new StringBuilder(source);
+    int offset = 0;
+    for (Map.Entry<Range<Integer>, String> replacement : replacements.asMapOfRanges().entrySet()) {
+      Range<Integer> range = replacement.getKey();
+      String replaceWith = replacement.getValue();
+      int start = offset + range.lowerEndpoint();
+      int end = offset + range.upperEndpoint();
+      sb.replace(start, end, replaceWith);
+      if (!replaceWith.isEmpty()) {
+        fixedRanges.add(Range.closedOpen(start, end));
+      }
+      offset += replaceWith.length() - (range.upperEndpoint() - range.lowerEndpoint());
+    }
+    return sb.toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/Replacement.java b/core/src/main/java/com/google/googlejavaformat/java/Replacement.java
new file mode 100644
index 0000000..5df0991
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/Replacement.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2014 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.java;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.Range;
+import java.util.Objects;
+
+/**
+ * Represents a range in the original source and replacement text for that range.
+ *
+ * <p>google-java-format doesn't depend on AutoValue, to allow AutoValue to depend on
+ * google-java-format.
+ */
+public final class Replacement {
+
+  public static Replacement create(int startPosition, int endPosition, String replaceWith) {
+    checkArgument(startPosition >= 0, "startPosition must be non-negative");
+    checkArgument(startPosition <= endPosition, "startPosition cannot be after endPosition");
+    return new Replacement(Range.closedOpen(startPosition, endPosition), replaceWith);
+  }
+
+  private final Range<Integer> replaceRange;
+  private final String replacementString;
+
+  private Replacement(Range<Integer> replaceRange, String replacementString) {
+    this.replaceRange = checkNotNull(replaceRange, "Null replaceRange");
+    this.replacementString = checkNotNull(replacementString, "Null replacementString");
+  }
+
+  /** The range of characters in the original source to replace. */
+  public Range<Integer> getReplaceRange() {
+    return replaceRange;
+  }
+
+  /** The string to replace the range of characters with. */
+  public String getReplacementString() {
+    return replacementString;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o == this) {
+      return true;
+    }
+    if (o instanceof Replacement) {
+      Replacement that = (Replacement) o;
+      return replaceRange.equals(that.getReplaceRange())
+          && replacementString.equals(that.getReplacementString());
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(replaceRange, replacementString);
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/SnippetFormatter.java b/core/src/main/java/com/google/googlejavaformat/java/SnippetFormatter.java
new file mode 100644
index 0000000..8d426b6
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/SnippetFormatter.java
@@ -0,0 +1,215 @@
+/*
+ * 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);
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java
new file mode 100644
index 0000000..e41bb66
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright 2019 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.Iterables.getLast;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
+import com.google.common.base.Verify;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Range;
+import com.google.common.collect.TreeRangeMap;
+import com.google.googlejavaformat.Newlines;
+import com.sun.source.tree.BinaryTree;
+import com.sun.source.tree.LiteralTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.Tree.Kind;
+import com.sun.source.util.TreePath;
+import com.sun.source.util.TreePathScanner;
+import com.sun.tools.javac.file.JavacFileManager;
+import com.sun.tools.javac.parser.JavacParser;
+import com.sun.tools.javac.parser.ParserFactory;
+import com.sun.tools.javac.tree.JCTree;
+import com.sun.tools.javac.util.Context;
+import com.sun.tools.javac.util.Log;
+import com.sun.tools.javac.util.Options;
+import com.sun.tools.javac.util.Position;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Stream;
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.DiagnosticListener;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardLocation;
+
+/** Wraps string literals that exceed the column limit. */
+public final class StringWrapper {
+  /** Reflows long string literals in the given Java source code. */
+  public static String wrap(String input, Formatter formatter) throws FormatterException {
+    return StringWrapper.wrap(Formatter.MAX_LINE_LENGTH, input, formatter);
+  }
+
+  /**
+   * Reflows string literals in the given Java source code that extend past the given column limit.
+   */
+  static String wrap(final int columnLimit, String input, Formatter formatter)
+      throws FormatterException {
+    if (!longLines(columnLimit, input)) {
+      // fast path
+      return input;
+    }
+
+    TreeRangeMap<Integer, String> replacements = getReflowReplacements(columnLimit, input);
+    String firstPass = formatter.formatSource(input, replacements.asMapOfRanges().keySet());
+
+    if (!firstPass.equals(input)) {
+      // If formatting the replacement ranges resulted in a change, recalculate the replacements on
+      // the updated input.
+      input = firstPass;
+      replacements = getReflowReplacements(columnLimit, input);
+    }
+
+    String result = applyReplacements(input, replacements);
+
+    {
+      // We really don't want bugs in this pass to change the behaviour of programs we're
+      // formatting, so check that the pretty-printed AST is the same before and after reformatting.
+      String expected = parse(input, /* allowStringFolding= */ true).toString();
+      String actual = parse(result, /* allowStringFolding= */ true).toString();
+      if (!expected.equals(actual)) {
+        throw new FormatterException(
+            String.format(
+                "Something has gone terribly wrong. Please file a bug: "
+                    + "https://github.com/google/google-java-format/issues/new"
+                    + "\n\n=== Actual: ===\n%s\n=== Expected: ===\n%s\n",
+                actual, expected));
+      }
+    }
+
+    return result;
+  }
+
+  private static TreeRangeMap<Integer, String> getReflowReplacements(
+      int columnLimit, final String input) throws FormatterException {
+    JCTree.JCCompilationUnit unit = parse(input, /* allowStringFolding= */ false);
+    String separator = Newlines.guessLineSeparator(input);
+
+    // Paths to string literals that extend past the column limit.
+    List<TreePath> toFix = new ArrayList<>();
+    final Position.LineMap lineMap = unit.getLineMap();
+    new TreePathScanner<Void, Void>() {
+      @Override
+      public Void visitLiteral(LiteralTree literalTree, Void aVoid) {
+        if (literalTree.getKind() != Kind.STRING_LITERAL) {
+          return null;
+        }
+        Tree parent = getCurrentPath().getParentPath().getLeaf();
+        if (parent instanceof MemberSelectTree
+            && ((MemberSelectTree) parent).getExpression().equals(literalTree)) {
+          return null;
+        }
+        int endPosition = getEndPosition(unit, literalTree);
+        int lineEnd = endPosition;
+        while (Newlines.hasNewlineAt(input, lineEnd) == -1) {
+          lineEnd++;
+        }
+        if (lineMap.getColumnNumber(lineEnd) - 1 <= columnLimit) {
+          return null;
+        }
+        toFix.add(getCurrentPath());
+        return null;
+      }
+    }.scan(new TreePath(unit), null);
+
+    TreeRangeMap<Integer, String> replacements = TreeRangeMap.create();
+    for (TreePath path : toFix) {
+      // Find the outermost contiguous enclosing concatenation expression
+      TreePath enclosing = path;
+      while (enclosing.getParentPath().getLeaf().getKind() == Tree.Kind.PLUS) {
+        enclosing = enclosing.getParentPath();
+      }
+      // Is the literal being wrapped the first in a chain of concatenation expressions?
+      // i.e. `ONE + TWO + THREE`
+      // We need this information to handle continuation indents.
+      AtomicBoolean first = new AtomicBoolean(false);
+      // Finds the set of string literals in the concat expression that includes the one that needs
+      // to be wrapped.
+      List<Tree> flat = flatten(input, unit, path, enclosing, first);
+      // Zero-indexed start column
+      int startColumn = lineMap.getColumnNumber(getStartPosition(flat.get(0))) - 1;
+
+      // Handling leaving trailing non-string tokens at the end of the literal,
+      // e.g. the trailing `);` in `foo("...");`.
+      int end = getEndPosition(unit, getLast(flat));
+      int lineEnd = end;
+      while (Newlines.hasNewlineAt(input, lineEnd) == -1) {
+        lineEnd++;
+      }
+      int trailing = lineEnd - end;
+
+      // Get the original source text of the string literals, excluding `"` and `+`.
+      ImmutableList<String> components = stringComponents(input, unit, flat);
+      replacements.put(
+          Range.closedOpen(getStartPosition(flat.get(0)), getEndPosition(unit, getLast(flat))),
+          reflow(separator, columnLimit, startColumn, trailing, components, first.get()));
+    }
+    return replacements;
+  }
+
+  /**
+   * Returns the source text of the given string literal trees, excluding the leading and trailing
+   * double-quotes and the `+` operator.
+   */
+  private static ImmutableList<String> stringComponents(
+      String input, JCTree.JCCompilationUnit unit, List<Tree> flat) {
+    ImmutableList.Builder<String> result = ImmutableList.builder();
+    StringBuilder piece = new StringBuilder();
+    for (Tree tree : flat) {
+      // adjust for leading and trailing double quotes
+      String text = input.substring(getStartPosition(tree) + 1, getEndPosition(unit, tree) - 1);
+      int start = 0;
+      for (int idx = 0; idx < text.length(); idx++) {
+        if (CharMatcher.whitespace().matches(text.charAt(idx))) {
+          // continue below
+        } else if (hasEscapedWhitespaceAt(text, idx) != -1) {
+          // continue below
+        } else if (hasEscapedNewlineAt(text, idx) != -1) {
+          int length;
+          while ((length = hasEscapedNewlineAt(text, idx)) != -1) {
+            idx += length;
+          }
+        } else {
+          continue;
+        }
+        piece.append(text, start, idx);
+        result.add(piece.toString());
+        piece = new StringBuilder();
+        start = idx;
+      }
+      if (piece.length() > 0) {
+        result.add(piece.toString());
+        piece = new StringBuilder();
+      }
+      if (start < text.length()) {
+        piece.append(text, start, text.length());
+      }
+    }
+    if (piece.length() > 0) {
+      result.add(piece.toString());
+    }
+    return result.build();
+  }
+
+  static int hasEscapedWhitespaceAt(String input, int idx) {
+    return Stream.of("\\t")
+        .mapToInt(x -> input.startsWith(x, idx) ? x.length() : -1)
+        .filter(x -> x != -1)
+        .findFirst()
+        .orElse(-1);
+  }
+
+  static int hasEscapedNewlineAt(String input, int idx) {
+    return Stream.of("\\r\\n", "\\r", "\\n")
+        .mapToInt(x -> input.startsWith(x, idx) ? x.length() : -1)
+        .filter(x -> x != -1)
+        .findFirst()
+        .orElse(-1);
+  }
+
+  /**
+   * Reflows the given source text, trying to split on word boundaries.
+   *
+   * @param separator the line separator
+   * @param columnLimit the number of columns to wrap at
+   * @param startColumn the column position of the beginning of the original text
+   * @param trailing extra space to leave after the last line
+   * @param components the text to reflow
+   * @param first0 true if the text includes the beginning of its enclosing concat chain, i.e. a
+   */
+  private static String reflow(
+      String separator,
+      int columnLimit,
+      int startColumn,
+      int trailing,
+      ImmutableList<String> components,
+      boolean first0) {
+    // We have space between the start column and the limit to output the first line.
+    // Reserve two spaces for the quotes.
+    int width = columnLimit - startColumn - 2;
+    Deque<String> input = new ArrayDeque<>(components);
+    List<String> lines = new ArrayList<>();
+    boolean first = first0;
+    while (!input.isEmpty()) {
+      int length = 0;
+      List<String> line = new ArrayList<>();
+      if (input.stream().mapToInt(x -> x.length()).sum() <= width) {
+        width -= trailing;
+      }
+      while (!input.isEmpty() && (length <= 4 || (length + input.peekFirst().length()) < width)) {
+        String text = input.removeFirst();
+        line.add(text);
+        length += text.length();
+        if (text.endsWith("\\n") || text.endsWith("\\r")) {
+          break;
+        }
+      }
+      if (line.isEmpty()) {
+        line.add(input.removeFirst());
+      }
+      // add the split line to the output, and process whatever's left
+      lines.add(String.join("", line));
+      if (first) {
+        width -= 6; // subsequent lines have a four-space continuation indent and a `+ `
+        first = false;
+      }
+    }
+
+    return lines.stream()
+        .collect(
+            joining(
+                "\"" + separator + Strings.repeat(" ", startColumn + (first0 ? 4 : -2)) + "+ \"",
+                "\"",
+                "\""));
+  }
+
+  /**
+   * Flattens the given binary expression tree, and extracts the subset that contains the given path
+   * and any adjacent nodes that are also string literals.
+   */
+  private static List<Tree> flatten(
+      String input,
+      JCTree.JCCompilationUnit unit,
+      TreePath path,
+      TreePath parent,
+      AtomicBoolean firstInChain) {
+    List<Tree> flat = new ArrayList<>();
+
+    // flatten the expression tree with a pre-order traversal
+    ArrayDeque<Tree> todo = new ArrayDeque<>();
+    todo.add(parent.getLeaf());
+    while (!todo.isEmpty()) {
+      Tree first = todo.removeFirst();
+      if (first.getKind() == Tree.Kind.PLUS) {
+        BinaryTree bt = (BinaryTree) first;
+        todo.addFirst(bt.getRightOperand());
+        todo.addFirst(bt.getLeftOperand());
+      } else {
+        flat.add(first);
+      }
+    }
+
+    int idx = flat.indexOf(path.getLeaf());
+    Verify.verify(idx != -1);
+
+    // walk outwards from the leaf for adjacent string literals to also reflow
+    int startIdx = idx;
+    int endIdx = idx + 1;
+    while (startIdx > 0
+        && flat.get(startIdx - 1).getKind() == Tree.Kind.STRING_LITERAL
+        && noComments(input, unit, flat.get(startIdx - 1), flat.get(startIdx))) {
+      startIdx--;
+    }
+    while (endIdx < flat.size()
+        && flat.get(endIdx).getKind() == Tree.Kind.STRING_LITERAL
+        && noComments(input, unit, flat.get(endIdx - 1), flat.get(endIdx))) {
+      endIdx++;
+    }
+
+    firstInChain.set(startIdx == 0);
+    return ImmutableList.copyOf(flat.subList(startIdx, endIdx));
+  }
+
+  private static boolean noComments(
+      String input, JCTree.JCCompilationUnit unit, Tree one, Tree two) {
+    return STRING_CONCAT_DELIMITER.matchesAllOf(
+        input.subSequence(getEndPosition(unit, one), getStartPosition(two)));
+  }
+
+  public static final CharMatcher STRING_CONCAT_DELIMITER =
+      CharMatcher.whitespace().or(CharMatcher.anyOf("\"+"));
+
+  private static int getEndPosition(JCTree.JCCompilationUnit unit, Tree tree) {
+    return ((JCTree) tree).getEndPosition(unit.endPositions);
+  }
+
+  private static int getStartPosition(Tree tree) {
+    return ((JCTree) tree).getStartPosition();
+  }
+
+  /** Returns true if any lines in the given Java source exceed the column limit. */
+  private static boolean longLines(int columnLimit, String input) {
+    // TODO(cushon): consider adding Newlines.lineIterable?
+    Iterator<String> it = Newlines.lineIterator(input);
+    while (it.hasNext()) {
+      String line = it.next();
+      if (line.length() > columnLimit) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /** Parses the given Java source. */
+  private static JCTree.JCCompilationUnit parse(String source, boolean allowStringFolding)
+      throws FormatterException {
+    DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
+    Context context = new Context();
+    context.put(DiagnosticListener.class, diagnostics);
+    Options.instance(context).put("--enable-preview", "true");
+    Options.instance(context).put("allowStringFolding", Boolean.toString(allowStringFolding));
+    JCTree.JCCompilationUnit unit;
+    JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8);
+    try {
+      fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+    SimpleJavaFileObject sjfo =
+        new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
+          @Override
+          public CharSequence getCharContent(boolean ignoreEncodingErrors) {
+            return source;
+          }
+        };
+    Log.instance(context).useSource(sjfo);
+    ParserFactory parserFactory = ParserFactory.instance(context);
+    JavacParser parser =
+        parserFactory.newParser(
+            source, /*keepDocComments=*/ true, /*keepEndPos=*/ true, /*keepLineMap=*/ true);
+    unit = parser.parseCompilationUnit();
+    unit.sourcefile = sjfo;
+    Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics =
+        Iterables.filter(diagnostics.getDiagnostics(), Formatter::errorDiagnostic);
+    if (!Iterables.isEmpty(errorDiagnostics)) {
+      // error handling is done during formatting
+      throw FormatterException.fromJavacDiagnostics(errorDiagnostics);
+    }
+    return unit;
+  }
+
+  /** Applies replacements to the given string. */
+  private static String applyReplacements(
+      String javaInput, TreeRangeMap<Integer, String> replacementMap) throws FormatterException {
+    // process in descending order so the replacement ranges aren't perturbed if any replacements
+    // differ in size from the input
+    Map<Range<Integer>, String> ranges = replacementMap.asDescendingMapOfRanges();
+    if (ranges.isEmpty()) {
+      return javaInput;
+    }
+    StringBuilder sb = new StringBuilder(javaInput);
+    for (Map.Entry<Range<Integer>, String> entry : ranges.entrySet()) {
+      Range<Integer> range = entry.getKey();
+      sb.replace(range.lowerEndpoint(), range.upperEndpoint(), entry.getValue());
+    }
+    return sb.toString();
+  }
+
+  private StringWrapper() {}
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/Trees.java b/core/src/main/java/com/google/googlejavaformat/java/Trees.java
new file mode 100644
index 0000000..397daca
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/Trees.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2016 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 com.sun.source.tree.ClassTree;
+import com.sun.source.tree.CompoundAssignmentTree;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.IdentifierTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.source.tree.ParenthesizedTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.util.TreePath;
+import com.sun.tools.javac.tree.JCTree;
+import com.sun.tools.javac.tree.Pretty;
+import com.sun.tools.javac.tree.TreeInfo;
+import java.io.IOError;
+import java.io.IOException;
+import javax.lang.model.element.Name;
+
+/** Utilities for working with {@link Tree}s. */
+class Trees {
+  /** Returns the length of the source for the node. */
+  static int getLength(Tree tree, TreePath path) {
+    return getEndPosition(tree, path) - getStartPosition(tree);
+  }
+
+  /** Returns the source start position of the node. */
+  static int getStartPosition(Tree expression) {
+    return ((JCTree) expression).getStartPosition();
+  }
+
+  /** Returns the source end position of the node. */
+  static int getEndPosition(Tree expression, TreePath path) {
+    return ((JCTree) expression)
+        .getEndPosition(((JCTree.JCCompilationUnit) path.getCompilationUnit()).endPositions);
+  }
+
+  /** Returns the source text for the node. */
+  static String getSourceForNode(Tree node, TreePath path) {
+    CharSequence source;
+    try {
+      source = path.getCompilationUnit().getSourceFile().getCharContent(false);
+    } catch (IOException e) {
+      throw new IOError(e);
+    }
+    return source.subSequence(getStartPosition(node), getEndPosition(node, path)).toString();
+  }
+
+  /** Returns the simple name of a (possibly qualified) method invocation expression. */
+  static Name getMethodName(MethodInvocationTree methodInvocation) {
+    ExpressionTree select = methodInvocation.getMethodSelect();
+    return select instanceof MemberSelectTree
+        ? ((MemberSelectTree) select).getIdentifier()
+        : ((IdentifierTree) select).getName();
+  }
+
+  /** Returns the receiver of a qualified method invocation expression, or {@code null}. */
+  static ExpressionTree getMethodReceiver(MethodInvocationTree methodInvocation) {
+    ExpressionTree select = methodInvocation.getMethodSelect();
+    return select instanceof MemberSelectTree ? ((MemberSelectTree) select).getExpression() : null;
+  }
+
+  /** Returns the string name of an operator, including assignment and compound assignment. */
+  static String operatorName(ExpressionTree expression) {
+    JCTree.Tag tag = ((JCTree) expression).getTag();
+    if (tag == JCTree.Tag.ASSIGN) {
+      return "=";
+    }
+    boolean assignOp = expression instanceof CompoundAssignmentTree;
+    if (assignOp) {
+      tag = tag.noAssignOp();
+    }
+    String name = new Pretty(/*writer*/ null, /*sourceOutput*/ true).operatorName(tag);
+    return assignOp ? name + "=" : name;
+  }
+
+  /** Returns the precedence of an expression's operator. */
+  static int precedence(ExpressionTree expression) {
+    return TreeInfo.opPrec(((JCTree) expression).getTag());
+  }
+
+  /**
+   * Returns the enclosing type declaration (class, enum, interface, or annotation) for the given
+   * path.
+   */
+  static ClassTree getEnclosingTypeDeclaration(TreePath path) {
+    for (; path != null; path = path.getParentPath()) {
+      switch (path.getLeaf().getKind()) {
+        case CLASS:
+        case ENUM:
+        case INTERFACE:
+        case ANNOTATED_TYPE:
+          return (ClassTree) path.getLeaf();
+        default:
+          break;
+      }
+    }
+    throw new AssertionError();
+  }
+
+  /** Skips a single parenthesized tree. */
+  static ExpressionTree skipParen(ExpressionTree node) {
+    return ((ParenthesizedTree) node).getExpression();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java b/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java
new file mode 100644
index 0000000..4e871a6
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2015 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 com.google.common.base.Verify;
+import java.util.List;
+import java.util.Optional;
+
+/** Heuristics for classifying qualified names as types. */
+public final class TypeNameClassifier {
+
+  private TypeNameClassifier() {}
+
+  /** A state machine for classifying qualified names. */
+  private enum TyParseState {
+
+    /** The start state. */
+    START(false) {
+      @Override
+      public TyParseState next(JavaCaseFormat n) {
+        switch (n) {
+          case UPPERCASE:
+            // if we see an UpperCamel later, assume this was a class
+            // e.g. com.google.FOO.Bar
+            return TyParseState.AMBIGUOUS;
+          case LOWER_CAMEL:
+            return TyParseState.REJECT;
+          case LOWERCASE:
+            // could be a package
+            return TyParseState.START;
+          case UPPER_CAMEL:
+            return TyParseState.TYPE;
+        }
+        throw new AssertionError();
+      }
+    },
+
+    /** The current prefix is a type. */
+    TYPE(true) {
+      @Override
+      public TyParseState next(JavaCaseFormat n) {
+        switch (n) {
+          case UPPERCASE:
+          case LOWER_CAMEL:
+          case LOWERCASE:
+            return TyParseState.FIRST_STATIC_MEMBER;
+          case UPPER_CAMEL:
+            return TyParseState.TYPE;
+        }
+        throw new AssertionError();
+      }
+    },
+
+    /** The current prefix is a type, followed by a single static member access. */
+    FIRST_STATIC_MEMBER(true) {
+      @Override
+      public TyParseState next(JavaCaseFormat n) {
+        return TyParseState.REJECT;
+      }
+    },
+
+    /** Anything not represented by one of the other states. */
+    REJECT(false) {
+      @Override
+      public TyParseState next(JavaCaseFormat n) {
+        return TyParseState.REJECT;
+      }
+    },
+
+    /** An ambiguous type prefix. */
+    AMBIGUOUS(false) {
+      @Override
+      public TyParseState next(JavaCaseFormat n) {
+        switch (n) {
+          case UPPERCASE:
+            return AMBIGUOUS;
+          case LOWER_CAMEL:
+          case LOWERCASE:
+            return TyParseState.REJECT;
+          case UPPER_CAMEL:
+            return TyParseState.TYPE;
+        }
+        throw new AssertionError();
+      }
+    };
+
+    private final boolean isSingleUnit;
+
+    TyParseState(boolean isSingleUnit) {
+      this.isSingleUnit = isSingleUnit;
+    }
+
+    public boolean isSingleUnit() {
+      return isSingleUnit;
+    }
+
+    /** Transition function. */
+    public abstract TyParseState next(JavaCaseFormat n);
+  }
+
+  /**
+   * Returns the end index (inclusive) of the longest prefix that matches the naming conventions of
+   * a type or static field access, or -1 if no such prefix was found.
+   *
+   * <p>Examples:
+   *
+   * <ul>
+   *   <li>ClassName
+   *   <li>ClassName.staticMemberName
+   *   <li>com.google.ClassName.InnerClass.staticMemberName
+   * </ul>
+   */
+  static Optional<Integer> typePrefixLength(List<String> nameParts) {
+    TyParseState state = TyParseState.START;
+    Optional<Integer> typeLength = Optional.empty();
+    for (int i = 0; i < nameParts.size(); i++) {
+      state = state.next(JavaCaseFormat.from(nameParts.get(i)));
+      if (state == TyParseState.REJECT) {
+        break;
+      }
+      if (state.isSingleUnit()) {
+        typeLength = Optional.of(i);
+      }
+    }
+    return typeLength;
+  }
+
+  /** Case formats used in Java identifiers. */
+  public enum JavaCaseFormat {
+    UPPERCASE,
+    LOWERCASE,
+    UPPER_CAMEL,
+    LOWER_CAMEL;
+
+    /** Classifies an identifier's case format. */
+    static JavaCaseFormat from(String name) {
+      Verify.verify(!name.isEmpty());
+      boolean firstUppercase = false;
+      boolean hasUppercase = false;
+      boolean hasLowercase = false;
+      boolean first = true;
+      for (int i = 0; i < name.length(); i++) {
+        char c = name.charAt(i);
+        if (!Character.isAlphabetic(c)) {
+          continue;
+        }
+        if (first) {
+          firstUppercase = Character.isUpperCase(c);
+          first = false;
+        }
+        hasUppercase |= Character.isUpperCase(c);
+        hasLowercase |= Character.isLowerCase(c);
+      }
+      if (firstUppercase) {
+        return hasLowercase ? UPPER_CAMEL : UPPERCASE;
+      } else {
+        return hasUppercase ? LOWER_CAMEL : LOWERCASE;
+      }
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/UsageException.java b/core/src/main/java/com/google/googlejavaformat/java/UsageException.java
new file mode 100644
index 0000000..82c0843
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/UsageException.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2015 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.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Joiner;
+
+/** Checked exception class for formatter command-line usage errors. */
+final class UsageException extends Exception {
+
+  private static final Joiner NEWLINE_JOINER = Joiner.on(System.lineSeparator());
+
+  private static final String[] DOCS_LINK = {
+    "https://github.com/google/google-java-format",
+  };
+
+  private static final String[] USAGE = {
+    "",
+    "Usage: google-java-format [options] file(s)",
+    "",
+    "Options:",
+    "  -i, -r, -replace, --replace",
+    "    Send formatted output back to files, not stdout.",
+    "  -",
+    "    Format stdin -> stdout",
+    "  --assume-filename, -assume-filename",
+    "    File name to use for diagnostics when formatting standard input (default is <stdin>).",
+    "  --aosp, -aosp, -a",
+    "    Use AOSP style instead of Google Style (4-space indentation).",
+    "  --fix-imports-only",
+    "    Fix import order and remove any unused imports, but do no other formatting.",
+    "  --skip-sorting-imports",
+    "    Do not fix the import order. Unused imports will still be removed.",
+    "  --skip-removing-unused-imports",
+    "    Do not remove unused imports. Imports will still be sorted.",
+    " . --skip-reflowing-long-strings",
+    "    Do not reflow string literals that exceed the column limit.",
+    " . --skip-javadoc-formatting",
+    "    Do not reformat javadoc.",
+    "  --dry-run, -n",
+    "    Prints the paths of the files whose contents would change if the formatter were run"
+        + " normally.",
+    "  --set-exit-if-changed",
+    "    Return exit code 1 if there are any formatting changes.",
+    "  --lines, -lines, --line, -line",
+    "    Line range(s) to format, like 5:10 (1-based; default is all).",
+    "  --offset, -offset",
+    "    Character offset to format (0-based; default is all).",
+    "  --length, -length",
+    "    Character length to format.",
+    "  --help, -help, -h",
+    "    Print this usage statement.",
+    "  --version, -version, -v",
+    "    Print the version.",
+    "  @<filename>",
+    "    Read options and filenames from file.",
+    "",
+  };
+
+  private static final String[] ADDITIONAL_USAGE = {
+    "If -i is given with -, the result is sent to stdout.",
+    "The --lines, --offset, and --length flags may be given more than once.",
+    "The --offset and --length flags must be given an equal number of times.",
+    "If --lines, --offset, or --length are given, only one file (or -) may be given."
+  };
+
+  UsageException() {
+    super(buildMessage(null));
+  }
+
+  UsageException(String message) {
+    super(buildMessage(checkNotNull(message)));
+  }
+
+  private static String buildMessage(String message) {
+    StringBuilder builder = new StringBuilder();
+    if (message != null) {
+      builder.append(message).append('\n');
+    }
+    appendLines(builder, USAGE);
+    appendLines(builder, ADDITIONAL_USAGE);
+    appendLines(builder, new String[] {""});
+    appendLine(builder, Main.versionString());
+    appendLines(builder, DOCS_LINK);
+    return builder.toString();
+  }
+
+  private static void appendLine(StringBuilder builder, String line) {
+    builder.append(line).append(System.lineSeparator());
+  }
+
+  private static void appendLines(StringBuilder builder, String[] lines) {
+    NEWLINE_JOINER.appendTo(builder, lines).append(System.lineSeparator());
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingFiler.java b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingFiler.java
new file mode 100644
index 0000000..d38d84e
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingFiler.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2016 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.filer;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.googlejavaformat.java.Formatter;
+import java.io.IOException;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.Messager;
+import javax.lang.model.element.Element;
+import javax.tools.FileObject;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileObject;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A decorating {@link Filer} implementation which formats Java source files with a {@link
+ * Formatter}.
+ */
+public final class FormattingFiler implements Filer {
+
+  private final Filer delegate;
+  // TODO(ronshapiro): consider allowing users to create their own Formatter instance
+  private final Formatter formatter = new Formatter();
+  private final Messager messager;
+
+  /** @param delegate filer to decorate */
+  public FormattingFiler(Filer delegate) {
+    this(delegate, null);
+  }
+
+  /**
+   * Create a new {@link FormattingFiler}. An optional {@link Messager} may be specified to make
+   * logs more visible.
+   *
+   * @param delegate filer to decorate
+   * @param messager to log warnings to
+   */
+  public FormattingFiler(Filer delegate, @Nullable Messager messager) {
+    this.delegate = checkNotNull(delegate);
+    this.messager = messager;
+  }
+
+  @Override
+  public JavaFileObject createSourceFile(CharSequence name, Element... originatingElements)
+      throws IOException {
+    return new FormattingJavaFileObject(
+        delegate.createSourceFile(name, originatingElements), formatter, messager);
+  }
+
+  @Override
+  public JavaFileObject createClassFile(CharSequence name, Element... originatingElements)
+      throws IOException {
+    return delegate.createClassFile(name, originatingElements);
+  }
+
+  @Override
+  public FileObject createResource(
+      JavaFileManager.Location location,
+      CharSequence pkg,
+      CharSequence relativeName,
+      Element... originatingElements)
+      throws IOException {
+    return delegate.createResource(location, pkg, relativeName, originatingElements);
+  }
+
+  @Override
+  public FileObject getResource(
+      JavaFileManager.Location location, CharSequence pkg, CharSequence relativeName)
+      throws IOException {
+    return delegate.getResource(location, pkg, relativeName);
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingJavaFileObject.java b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingJavaFileObject.java
new file mode 100644
index 0000000..c8ae807
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingJavaFileObject.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016 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.filer;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.io.CharSink;
+import com.google.common.io.CharSource;
+import com.google.googlejavaformat.java.Formatter;
+import com.google.googlejavaformat.java.FormatterException;
+import java.io.IOException;
+import java.io.Writer;
+import javax.annotation.processing.Messager;
+import javax.tools.Diagnostic;
+import javax.tools.ForwardingJavaFileObject;
+import javax.tools.JavaFileObject;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** A {@link JavaFileObject} decorator which {@linkplain Formatter formats} source code. */
+final class FormattingJavaFileObject extends ForwardingJavaFileObject<JavaFileObject> {
+  /** A rough estimate of the average file size: 80 chars per line, 500 lines. */
+  private static final int DEFAULT_FILE_SIZE = 80 * 500;
+
+  private final Formatter formatter;
+  private final Messager messager;
+
+  /**
+   * Create a new {@link FormattingJavaFileObject}.
+   *
+   * @param delegate {@link JavaFileObject} to decorate
+   * @param messager to log messages with.
+   */
+  FormattingJavaFileObject(
+      JavaFileObject delegate, Formatter formatter, @Nullable Messager messager) {
+    super(checkNotNull(delegate));
+    this.formatter = checkNotNull(formatter);
+    this.messager = messager;
+  }
+
+  @Override
+  public Writer openWriter() throws IOException {
+    final StringBuilder stringBuilder = new StringBuilder(DEFAULT_FILE_SIZE);
+    return new Writer() {
+      @Override
+      public void write(char[] chars, int start, int end) throws IOException {
+        stringBuilder.append(chars, start, end - start);
+      }
+
+      @Override
+      public void write(String string) throws IOException {
+        stringBuilder.append(string);
+      }
+
+      @Override
+      public void flush() throws IOException {}
+
+      @Override
+      public void close() throws IOException {
+        try {
+          formatter.formatSource(
+              CharSource.wrap(stringBuilder),
+              new CharSink() {
+                @Override
+                public Writer openStream() throws IOException {
+                  return fileObject.openWriter();
+                }
+              });
+        } catch (FormatterException e) {
+          // An exception will happen when the code being formatted has an error. It's better to
+          // log the exception and emit unformatted code so the developer can view the code which
+          // caused a problem.
+          try (Writer writer = fileObject.openWriter()) {
+            writer.append(stringBuilder.toString());
+          }
+          if (messager != null) {
+            messager.printMessage(Diagnostic.Kind.NOTE, "Error formatting " + getName());
+          }
+        }
+      }
+    };
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/java14/Java14InputAstVisitor.java b/core/src/main/java/com/google/googlejavaformat/java/java14/Java14InputAstVisitor.java
new file mode 100644
index 0000000..28a1103
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/java14/Java14InputAstVisitor.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2020 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.java14;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.MoreCollectors.toOptional;
+
+import com.google.common.base.Verify;
+import com.google.common.collect.ImmutableList;
+import com.google.googlejavaformat.Op;
+import com.google.googlejavaformat.OpsBuilder;
+import com.google.googlejavaformat.java.JavaInputAstVisitor;
+import com.sun.source.tree.BindingPatternTree;
+import com.sun.source.tree.CaseTree;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.InstanceOfTree;
+import com.sun.source.tree.SwitchExpressionTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.YieldTree;
+import com.sun.tools.javac.code.Flags;
+import com.sun.tools.javac.tree.JCTree;
+import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
+import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
+import com.sun.tools.javac.tree.TreeInfo;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Extends {@link JavaInputAstVisitor} with support for AST nodes that were added or modified for
+ * Java 14.
+ */
+public class Java14InputAstVisitor extends JavaInputAstVisitor {
+
+  public Java14InputAstVisitor(OpsBuilder builder, int indentMultiplier) {
+    super(builder, indentMultiplier);
+  }
+
+  @Override
+  public Void visitBindingPattern(BindingPatternTree node, Void unused) {
+    sync(node);
+    scan(node.getType(), null);
+    builder.breakOp(" ");
+    visit(node.getBinding());
+    return null;
+  }
+
+  @Override
+  public Void visitYield(YieldTree node, Void aVoid) {
+    sync(node);
+    return super.visitYield(node, aVoid);
+  }
+
+  @Override
+  public Void visitSwitchExpression(SwitchExpressionTree node, Void aVoid) {
+    sync(node);
+    visitSwitch(node.getExpression(), node.getCases());
+    return null;
+  }
+
+  @Override
+  public Void visitClass(ClassTree tree, Void unused) {
+    switch (tree.getKind()) {
+      case ANNOTATION_TYPE:
+        visitAnnotationType(tree);
+        break;
+      case CLASS:
+      case INTERFACE:
+        visitClassDeclaration(tree);
+        break;
+      case ENUM:
+        visitEnumDeclaration(tree);
+        break;
+      case RECORD:
+        visitRecordDeclaration(tree);
+        break;
+      default:
+        throw new AssertionError(tree.getKind());
+    }
+    return null;
+  }
+
+  public void visitRecordDeclaration(ClassTree node) {
+    sync(node);
+    List<Op> breaks =
+        visitModifiers(
+            node.getModifiers(),
+            Direction.VERTICAL,
+            /* declarationAnnotationBreak= */ Optional.empty());
+    Verify.verify(node.getExtendsClause() == null);
+    boolean hasSuperInterfaceTypes = !node.getImplementsClause().isEmpty();
+    builder.addAll(breaks);
+    token("record");
+    builder.space();
+    visit(node.getSimpleName());
+    if (!node.getTypeParameters().isEmpty()) {
+      token("<");
+    }
+    builder.open(plusFour);
+    {
+      if (!node.getTypeParameters().isEmpty()) {
+        typeParametersRest(node.getTypeParameters(), hasSuperInterfaceTypes ? plusFour : ZERO);
+      }
+      ImmutableList<JCVariableDecl> parameters =
+          compactRecordConstructor(node)
+              .map(m -> ImmutableList.copyOf(m.getParameters()))
+              .orElseGet(() -> recordVariables(node));
+      token("(");
+      if (!parameters.isEmpty()) {
+        // Break before args.
+        builder.breakToFill("");
+      }
+      // record headers can't declare receiver parameters
+      visitFormals(/* receiver= */ Optional.empty(), parameters);
+      token(")");
+      if (hasSuperInterfaceTypes) {
+        builder.breakToFill(" ");
+        builder.open(node.getImplementsClause().size() > 1 ? plusFour : ZERO);
+        token("implements");
+        builder.space();
+        boolean first = true;
+        for (Tree superInterfaceType : node.getImplementsClause()) {
+          if (!first) {
+            token(",");
+            builder.breakOp(" ");
+          }
+          scan(superInterfaceType, null);
+          first = false;
+        }
+        builder.close();
+      }
+    }
+    builder.close();
+    if (node.getMembers() == null) {
+      token(";");
+    } else {
+      List<Tree> members =
+          node.getMembers().stream()
+              .filter(t -> (TreeInfo.flags((JCTree) t) & Flags.GENERATED_MEMBER) == 0)
+              .collect(toImmutableList());
+      addBodyDeclarations(members, BracesOrNot.YES, FirstDeclarationsOrNot.YES);
+    }
+    dropEmptyDeclarations();
+  }
+
+  private static Optional<JCMethodDecl> compactRecordConstructor(ClassTree node) {
+    return node.getMembers().stream()
+        .filter(JCMethodDecl.class::isInstance)
+        .map(JCMethodDecl.class::cast)
+        .filter(m -> (m.mods.flags & COMPACT_RECORD_CONSTRUCTOR) == COMPACT_RECORD_CONSTRUCTOR)
+        .collect(toOptional());
+  }
+
+  private static ImmutableList<JCVariableDecl> recordVariables(ClassTree node) {
+    return node.getMembers().stream()
+        .filter(JCVariableDecl.class::isInstance)
+        .map(JCVariableDecl.class::cast)
+        .filter(m -> (m.mods.flags & RECORD) == RECORD)
+        .collect(toImmutableList());
+  }
+
+  @Override
+  public Void visitInstanceOf(InstanceOfTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    scan(node.getExpression(), null);
+    builder.breakOp(" ");
+    builder.open(ZERO);
+    token("instanceof");
+    builder.breakOp(" ");
+    if (node.getPattern() != null) {
+      scan(node.getPattern(), null);
+    } else {
+      scan(node.getType(), null);
+    }
+    builder.close();
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitCase(CaseTree node, Void unused) {
+    sync(node);
+    markForPartialFormat();
+    builder.forcedBreak();
+    if (node.getExpressions().isEmpty()) {
+      token("default", plusTwo);
+    } else {
+      token("case", plusTwo);
+      builder.space();
+      boolean first = true;
+      for (ExpressionTree expression : node.getExpressions()) {
+        if (!first) {
+          token(",");
+          builder.space();
+        }
+        scan(expression, null);
+        first = false;
+      }
+    }
+    switch (node.getCaseKind()) {
+      case STATEMENT:
+        token(":");
+        builder.open(plusTwo);
+        visitStatements(node.getStatements());
+        builder.close();
+        break;
+      case RULE:
+        builder.space();
+        token("-");
+        token(">");
+        builder.space();
+        scan(node.getBody(), null);
+        token(";");
+        break;
+      default:
+        throw new AssertionError(node.getCaseKind());
+    }
+    return null;
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/CharStream.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/CharStream.java
new file mode 100644
index 0000000..8fbd49f
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/CharStream.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2016 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.javadoc;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * String reader designed for use from the lexer. Callers invoke the {@link #tryConsume tryConsume*}
+ * methods to specify what characters they expect and then {@link #readAndResetRecorded} to retrieve
+ * and consume the matched characters. This is a slightly odd API -- why not just return the matched
+ * characters from tryConsume? -- but it is convenient for the lexer.
+ */
+final class CharStream {
+  String remaining;
+  int toConsume;
+
+  CharStream(String input) {
+    this.remaining = checkNotNull(input);
+  }
+
+  boolean tryConsume(String expected) {
+    if (!remaining.startsWith(expected)) {
+      return false;
+    }
+    toConsume = expected.length();
+    return true;
+  }
+
+  /*
+   * @param pattern the pattern to search for, which must be anchored to match only at position 0
+   */
+  boolean tryConsumeRegex(Pattern pattern) {
+    Matcher matcher = pattern.matcher(remaining);
+    if (!matcher.find()) {
+      return false;
+    }
+    checkArgument(matcher.start() == 0);
+    toConsume = matcher.end();
+    return true;
+  }
+
+  String readAndResetRecorded() {
+    String result = remaining.substring(0, toConsume);
+    remaining = remaining.substring(toConsume);
+    toConsume = 0; // TODO(cpovirk): Set this to a bogus value here and in the constructor.
+    return result;
+  }
+
+  boolean isExhausted() {
+    return remaining.isEmpty();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java
new file mode 100644
index 0000000..5addc67
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2016 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.javadoc;
+
+import static com.google.googlejavaformat.java.javadoc.JavadocLexer.lex;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.BR_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG;
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+import static java.util.regex.Pattern.compile;
+
+import com.google.common.collect.ImmutableList;
+import com.google.googlejavaformat.java.javadoc.JavadocLexer.LexException;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Entry point for formatting Javadoc.
+ *
+ * <p>This stateless class reads tokens from the stateful lexer and translates them to "requests"
+ * and "writes" to the stateful writer. It also munges tokens into "standardized" forms. Finally, it
+ * performs postprocessing to convert the written Javadoc to a one-liner if possible or to leave a
+ * single blank line if it's empty.
+ */
+public final class JavadocFormatter {
+
+  static final int MAX_LINE_LENGTH = 100;
+
+  /**
+   * Formats the given Javadoc comment, which must start with ∕✱✱ and end with ✱∕. The output will
+   * start and end with the same characters.
+   */
+  public static String formatJavadoc(String input, int blockIndent) {
+    ImmutableList<Token> tokens;
+    try {
+      tokens = lex(input);
+    } catch (LexException e) {
+      return input;
+    }
+    String result = render(tokens, blockIndent);
+    return makeSingleLineIfPossible(blockIndent, result);
+  }
+
+  private static String render(List<Token> input, int blockIndent) {
+    JavadocWriter output = new JavadocWriter(blockIndent);
+    for (Token token : input) {
+      switch (token.getType()) {
+        case BEGIN_JAVADOC:
+          output.writeBeginJavadoc();
+          break;
+        case END_JAVADOC:
+          output.writeEndJavadoc();
+          return output.toString();
+        case FOOTER_JAVADOC_TAG_START:
+          output.writeFooterJavadocTagStart(token);
+          break;
+        case LIST_OPEN_TAG:
+          output.writeListOpen(token);
+          break;
+        case LIST_CLOSE_TAG:
+          output.writeListClose(token);
+          break;
+        case LIST_ITEM_OPEN_TAG:
+          output.writeListItemOpen(token);
+          break;
+        case HEADER_OPEN_TAG:
+          output.writeHeaderOpen(token);
+          break;
+        case HEADER_CLOSE_TAG:
+          output.writeHeaderClose(token);
+          break;
+        case PARAGRAPH_OPEN_TAG:
+          output.writeParagraphOpen(standardizePToken(token));
+          break;
+        case BLOCKQUOTE_OPEN_TAG:
+        case BLOCKQUOTE_CLOSE_TAG:
+          output.writeBlockquoteOpenOrClose(token);
+          break;
+        case PRE_OPEN_TAG:
+          output.writePreOpen(token);
+          break;
+        case PRE_CLOSE_TAG:
+          output.writePreClose(token);
+          break;
+        case CODE_OPEN_TAG:
+          output.writeCodeOpen(token);
+          break;
+        case CODE_CLOSE_TAG:
+          output.writeCodeClose(token);
+          break;
+        case TABLE_OPEN_TAG:
+          output.writeTableOpen(token);
+          break;
+        case TABLE_CLOSE_TAG:
+          output.writeTableClose(token);
+          break;
+        case MOE_BEGIN_STRIP_COMMENT:
+          output.requestMoeBeginStripComment(token);
+          break;
+        case MOE_END_STRIP_COMMENT:
+          output.writeMoeEndStripComment(token);
+          break;
+        case HTML_COMMENT:
+          output.writeHtmlComment(token);
+          break;
+        case BR_TAG:
+          output.writeBr(standardizeBrToken(token));
+          break;
+        case WHITESPACE:
+          output.requestWhitespace();
+          break;
+        case FORCED_NEWLINE:
+          output.writeLineBreakNoAutoIndent();
+          break;
+        case LITERAL:
+          output.writeLiteral(token);
+          break;
+        case PARAGRAPH_CLOSE_TAG:
+        case LIST_ITEM_CLOSE_TAG:
+        case OPTIONAL_LINE_BREAK:
+          break;
+        default:
+          throw new AssertionError(token.getType());
+      }
+    }
+    throw new AssertionError();
+  }
+
+  /*
+   * TODO(cpovirk): Is this really the right location for the standardize* methods? Maybe the lexer
+   * should include them as part of its own postprocessing? Or even the writer could make sense.
+   */
+
+  private static Token standardizeBrToken(Token token) {
+    return standardize(token, STANDARD_BR_TOKEN);
+  }
+
+  private static Token standardizePToken(Token token) {
+    return standardize(token, STANDARD_P_TOKEN);
+  }
+
+  private static Token standardize(Token token, Token standardToken) {
+    return SIMPLE_TAG_PATTERN.matcher(token.getValue()).matches() ? standardToken : token;
+  }
+
+  private static final Token STANDARD_BR_TOKEN = new Token(BR_TAG, "<br>");
+  private static final Token STANDARD_P_TOKEN = new Token(PARAGRAPH_OPEN_TAG, "<p>");
+  private static final Pattern SIMPLE_TAG_PATTERN = compile("^<\\w+\\s*/?\\s*>", CASE_INSENSITIVE);
+
+  private static final Pattern ONE_CONTENT_LINE_PATTERN = compile(" */[*][*]\n *[*] (.*)\n *[*]/");
+
+  /**
+   * Returns the given string or a one-line version of it (e.g., "∕✱✱ Tests for foos. ✱∕") if it
+   * fits on one line.
+   */
+  private static String makeSingleLineIfPossible(int blockIndent, String input) {
+    int oneLinerContentLength = MAX_LINE_LENGTH - "/**  */".length() - blockIndent;
+    Matcher matcher = ONE_CONTENT_LINE_PATTERN.matcher(input);
+    if (matcher.matches() && matcher.group(1).isEmpty()) {
+      return "/** */";
+    } else if (matcher.matches() && matcher.group(1).length() <= oneLinerContentLength) {
+      return "/** " + matcher.group(1) + " */";
+    }
+    return input;
+  }
+
+  private JavadocFormatter() {}
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java
new file mode 100644
index 0000000..108d4a7
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java
@@ -0,0 +1,557 @@
+/*
+ * Copyright 2016 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.javadoc;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Verify.verify;
+import static com.google.common.collect.Iterators.peekingIterator;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.BEGIN_JAVADOC;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.BLOCKQUOTE_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.BLOCKQUOTE_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.BR_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.CODE_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.CODE_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.END_JAVADOC;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.FOOTER_JAVADOC_TAG_START;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.FORCED_NEWLINE;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.HEADER_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.HEADER_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.HTML_COMMENT;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_ITEM_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_ITEM_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.LITERAL;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.MOE_BEGIN_STRIP_COMMENT;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.MOE_END_STRIP_COMMENT;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.OPTIONAL_LINE_BREAK;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.PRE_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.PRE_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.TABLE_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.TABLE_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.WHITESPACE;
+import static java.lang.String.format;
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+import static java.util.regex.Pattern.DOTALL;
+import static java.util.regex.Pattern.compile;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.PeekingIterator;
+import com.google.googlejavaformat.java.javadoc.Token.Type;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/** Lexer for the Javadoc formatter. */
+final class JavadocLexer {
+  /** Takes a Javadoc comment, including ∕✱✱ and ✱∕, and returns tokens, including ∕✱✱ and ✱∕. */
+  static ImmutableList<Token> lex(String input) throws LexException {
+    /*
+     * TODO(cpovirk): In theory, we should interpret Unicode escapes (yet output them in their
+     * original form). This would mean mean everything from an encoded ∕✱✱ to an encoded <pre> tag,
+     * so we'll probably never bother.
+     */
+    input = stripJavadocBeginAndEnd(input);
+    input = normalizeLineEndings(input);
+    return new JavadocLexer(new CharStream(input)).generateTokens();
+  }
+
+  /** The lexer crashes on windows line endings, so for now just normalize to `\n`. */
+  // TODO(cushon): use the platform line separator for output
+  private static String normalizeLineEndings(String input) {
+    return NON_UNIX_LINE_ENDING.matcher(input).replaceAll("\n");
+  }
+
+  private static final Pattern NON_UNIX_LINE_ENDING = Pattern.compile("\r\n?");
+
+  private static String stripJavadocBeginAndEnd(String input) {
+    /*
+     * We do this ahead of time so that the main part of the lexer need not say things like
+     * "(?![*]/)" to avoid accidentally swallowing ✱∕ when consuming a newline.
+     */
+    checkArgument(input.startsWith("/**"), "Missing /**: %s", input);
+    checkArgument(input.endsWith("*/") && input.length() > 4, "Missing */: %s", input);
+    return input.substring("/**".length(), input.length() - "*/".length());
+  }
+
+  private final CharStream input;
+  private final NestingCounter braceDepth = new NestingCounter();
+  private final NestingCounter preDepth = new NestingCounter();
+  private final NestingCounter codeDepth = new NestingCounter();
+  private final NestingCounter tableDepth = new NestingCounter();
+  private boolean somethingSinceNewline;
+
+  private JavadocLexer(CharStream input) {
+    this.input = checkNotNull(input);
+  }
+
+  private ImmutableList<Token> generateTokens() throws LexException {
+    ImmutableList.Builder<Token> tokens = ImmutableList.builder();
+
+    Token token = new Token(BEGIN_JAVADOC, "/**");
+    tokens.add(token);
+
+    while (!input.isExhausted()) {
+      token = readToken();
+      tokens.add(token);
+    }
+
+    checkMatchingTags();
+
+    token = new Token(END_JAVADOC, "*/");
+    tokens.add(token);
+
+    ImmutableList<Token> result = tokens.build();
+    result = joinAdjacentLiteralsAndAdjacentWhitespace(result);
+    result = inferParagraphTags(result);
+    result = optionalizeSpacesAfterLinks(result);
+    result = deindentPreCodeBlocks(result);
+    return result;
+  }
+
+  private Token readToken() throws LexException {
+    Type type = consumeToken();
+    String value = input.readAndResetRecorded();
+    return new Token(type, value);
+  }
+
+  private Type consumeToken() throws LexException {
+    boolean preserveExistingFormatting = preserveExistingFormatting();
+
+    if (input.tryConsumeRegex(NEWLINE_PATTERN)) {
+      somethingSinceNewline = false;
+      return preserveExistingFormatting ? FORCED_NEWLINE : WHITESPACE;
+    } else if (input.tryConsume(" ") || input.tryConsume("\t")) {
+      // TODO(cpovirk): How about weird whitespace chars? Ideally we'd distinguish breaking vs. not.
+      // Returning LITERAL here prevent us from breaking a <pre> line. For more info, see LITERAL.
+      return preserveExistingFormatting ? LITERAL : WHITESPACE;
+    }
+
+    /*
+     * TODO(cpovirk): Maybe try to detect things like "{@code\n@GwtCompatible}" that aren't intended
+     * as tags. But in the most likely case, in which that happens inside <pre>{@code, we have no
+     * great options for fixing it.
+     * https://github.com/google/google-java-format/issues/7#issuecomment-197383926
+     */
+    if (!somethingSinceNewline && input.tryConsumeRegex(FOOTER_TAG_PATTERN)) {
+      checkMatchingTags();
+      somethingSinceNewline = true;
+      return FOOTER_JAVADOC_TAG_START;
+    }
+    somethingSinceNewline = true;
+
+    if (input.tryConsumeRegex(INLINE_TAG_OPEN_PATTERN)) {
+      braceDepth.increment();
+      return LITERAL;
+    } else if (input.tryConsume("{")) {
+      braceDepth.incrementIfPositive();
+      return LITERAL;
+    } else if (input.tryConsume("}")) {
+      braceDepth.decrementIfPositive();
+      return LITERAL;
+    }
+
+    // Inside an inline tag, don't do any HTML interpretation.
+    if (braceDepth.isPositive()) {
+      verify(input.tryConsumeRegex(LITERAL_PATTERN));
+      return LITERAL;
+    }
+
+    if (input.tryConsumeRegex(PRE_OPEN_PATTERN)) {
+      preDepth.increment();
+      return preserveExistingFormatting ? LITERAL : PRE_OPEN_TAG;
+    } else if (input.tryConsumeRegex(PRE_CLOSE_PATTERN)) {
+      preDepth.decrementIfPositive();
+      return preserveExistingFormatting() ? LITERAL : PRE_CLOSE_TAG;
+    }
+
+    if (input.tryConsumeRegex(CODE_OPEN_PATTERN)) {
+      codeDepth.increment();
+      return preserveExistingFormatting ? LITERAL : CODE_OPEN_TAG;
+    } else if (input.tryConsumeRegex(CODE_CLOSE_PATTERN)) {
+      codeDepth.decrementIfPositive();
+      return preserveExistingFormatting() ? LITERAL : CODE_CLOSE_TAG;
+    }
+
+    if (input.tryConsumeRegex(TABLE_OPEN_PATTERN)) {
+      tableDepth.increment();
+      return preserveExistingFormatting ? LITERAL : TABLE_OPEN_TAG;
+    } else if (input.tryConsumeRegex(TABLE_CLOSE_PATTERN)) {
+      tableDepth.decrementIfPositive();
+      return preserveExistingFormatting() ? LITERAL : TABLE_CLOSE_TAG;
+    }
+
+    if (preserveExistingFormatting) {
+      verify(input.tryConsumeRegex(LITERAL_PATTERN));
+      return LITERAL;
+    }
+
+    if (input.tryConsumeRegex(PARAGRAPH_OPEN_PATTERN)) {
+      return PARAGRAPH_OPEN_TAG;
+    } else if (input.tryConsumeRegex(PARAGRAPH_CLOSE_PATTERN)) {
+      return PARAGRAPH_CLOSE_TAG;
+    } else if (input.tryConsumeRegex(LIST_OPEN_PATTERN)) {
+      return LIST_OPEN_TAG;
+    } else if (input.tryConsumeRegex(LIST_CLOSE_PATTERN)) {
+      return LIST_CLOSE_TAG;
+    } else if (input.tryConsumeRegex(LIST_ITEM_OPEN_PATTERN)) {
+      return LIST_ITEM_OPEN_TAG;
+    } else if (input.tryConsumeRegex(LIST_ITEM_CLOSE_PATTERN)) {
+      return LIST_ITEM_CLOSE_TAG;
+    } else if (input.tryConsumeRegex(BLOCKQUOTE_OPEN_PATTERN)) {
+      return BLOCKQUOTE_OPEN_TAG;
+    } else if (input.tryConsumeRegex(BLOCKQUOTE_CLOSE_PATTERN)) {
+      return BLOCKQUOTE_CLOSE_TAG;
+    } else if (input.tryConsumeRegex(HEADER_OPEN_PATTERN)) {
+      return HEADER_OPEN_TAG;
+    } else if (input.tryConsumeRegex(HEADER_CLOSE_PATTERN)) {
+      return HEADER_CLOSE_TAG;
+    } else if (input.tryConsumeRegex(BR_PATTERN)) {
+      return BR_TAG;
+    } else if (input.tryConsumeRegex(MOE_BEGIN_STRIP_COMMENT_PATTERN)) {
+      return MOE_BEGIN_STRIP_COMMENT;
+    } else if (input.tryConsumeRegex(MOE_END_STRIP_COMMENT_PATTERN)) {
+      return MOE_END_STRIP_COMMENT;
+    } else if (input.tryConsumeRegex(HTML_COMMENT_PATTERN)) {
+      return HTML_COMMENT;
+    } else if (input.tryConsumeRegex(LITERAL_PATTERN)) {
+      return LITERAL;
+    }
+    throw new AssertionError();
+  }
+
+  private boolean preserveExistingFormatting() {
+    return preDepth.isPositive() || tableDepth.isPositive() || codeDepth.isPositive();
+  }
+
+  private void checkMatchingTags() throws LexException {
+    if (braceDepth.isPositive()
+        || preDepth.isPositive()
+        || tableDepth.isPositive()
+        || codeDepth.isPositive()) {
+      throw new LexException();
+    }
+  }
+
+  /**
+   * Join together adjacent literal tokens, and join together adjacent whitespace tokens.
+   *
+   * <p>For literal tokens, this means something like {@code ["<b>", "foo", "</b>"] =>
+   * ["<b>foo</b>"]}. See {@link #LITERAL_PATTERN} for discussion of why those tokens are separate
+   * to begin with.
+   *
+   * <p>Whitespace tokens are treated analogously. We don't really "want" to join whitespace tokens,
+   * but in the course of joining literals, we incidentally join whitespace, too. We do take
+   * advantage of the joining later on: It simplifies {@link #inferParagraphTags}.
+   *
+   * <p>Note that we do <i>not</i> merge a literal token and a whitespace token together.
+   */
+  private static ImmutableList<Token> joinAdjacentLiteralsAndAdjacentWhitespace(List<Token> input) {
+    /*
+     * Note: Our final token is always END_JAVADOC. This saves us some trouble:
+     *
+     * - Our inner while() doesn't need a hasNext() check.
+     *
+     * - We don't need to check for leftover accumulated literals after we exit the loop.
+     */
+    ImmutableList.Builder<Token> output = ImmutableList.builder();
+    StringBuilder accumulated = new StringBuilder();
+
+    for (PeekingIterator<Token> tokens = peekingIterator(input.iterator()); tokens.hasNext(); ) {
+      if (tokens.peek().getType() == LITERAL) {
+        accumulated.append(tokens.peek().getValue());
+        tokens.next();
+        continue;
+      }
+
+      /*
+       * IF we have accumulated some literals to join together (say, "foo<b>bar</b>"), and IF we'll
+       * next see whitespace followed by a "@" literal, we need to join that together with the
+       * previous literals. That ensures that we won't insert a line break before the "@," turning
+       * it into a tag.
+       */
+
+      if (accumulated.length() == 0) {
+        output.add(tokens.peek());
+        tokens.next();
+        continue;
+      }
+
+      StringBuilder seenWhitespace = new StringBuilder();
+      while (tokens.peek().getType() == WHITESPACE) {
+        seenWhitespace.append(tokens.next().getValue());
+      }
+
+      if (tokens.peek().getType() == LITERAL && tokens.peek().getValue().startsWith("@")) {
+        // OK, we're in the case described above.
+        accumulated.append(" ");
+        accumulated.append(tokens.peek().getValue());
+        tokens.next();
+        continue;
+      }
+
+      output.add(new Token(LITERAL, accumulated.toString()));
+      accumulated.setLength(0);
+
+      if (seenWhitespace.length() > 0) {
+        output.add(new Token(WHITESPACE, seenWhitespace.toString()));
+      }
+
+      // We have another token coming, possibly of type OTHER. Leave it for the next iteration.
+    }
+
+    /*
+     * TODO(cpovirk): Another case where we could try to join tokens is if a line ends with
+     * /[^ -]-/, as in "non-\nblocking."
+     */
+    return output.build();
+  }
+
+  /**
+   * Where the input has two consecutive line breaks between literals, insert a {@code <p>} tag
+   * between the literals.
+   *
+   * <p>This method must be called after {@link #joinAdjacentLiteralsAndAdjacentWhitespace}, as it
+   * assumes that adjacent whitespace tokens have already been joined.
+   */
+  private static ImmutableList<Token> inferParagraphTags(List<Token> input) {
+    ImmutableList.Builder<Token> output = ImmutableList.builder();
+
+    for (PeekingIterator<Token> tokens = peekingIterator(input.iterator()); tokens.hasNext(); ) {
+      if (tokens.peek().getType() == LITERAL) {
+        output.add(tokens.next());
+
+        if (tokens.peek().getType() == WHITESPACE
+            && hasMultipleNewlines(tokens.peek().getValue())) {
+          output.add(tokens.next());
+
+          if (tokens.peek().getType() == LITERAL) {
+            output.add(new Token(PARAGRAPH_OPEN_TAG, "<p>"));
+          }
+        }
+      } else {
+        // TODO(cpovirk): Or just `continue` from the <p> case and move this out of the `else`?
+        output.add(tokens.next());
+      }
+    }
+
+    return output.build();
+
+    /*
+     * Note: We do not want to insert <p> tags inside <pre>. Fortunately, the formatter gets that
+     * right without special effort on our part. The reason: Line breaks inside a <pre> section are
+     * of type FORCED_NEWLINE rather than WHITESPACE.
+     */
+  }
+
+  /**
+   * Replaces whitespace after a {@code href=...>} token with an "optional link break." This allows
+   * us to output either {@code <a href=foo>foo</a>} or {@code <a href=foo>\nfoo</a>}, depending on
+   * how much space we have left on the line.
+   *
+   * <p>This method must be called after {@link #joinAdjacentLiteralsAndAdjacentWhitespace}, as it
+   * assumes that adjacent whitespace tokens have already been joined.
+   */
+  private static ImmutableList<Token> optionalizeSpacesAfterLinks(List<Token> input) {
+    ImmutableList.Builder<Token> output = ImmutableList.builder();
+
+    for (PeekingIterator<Token> tokens = peekingIterator(input.iterator()); tokens.hasNext(); ) {
+      if (tokens.peek().getType() == LITERAL && tokens.peek().getValue().matches("^href=[^>]*>")) {
+        output.add(tokens.next());
+
+        if (tokens.peek().getType() == WHITESPACE) {
+          output.add(new Token(OPTIONAL_LINE_BREAK, tokens.next().getValue()));
+        }
+      } else {
+        output.add(tokens.next());
+      }
+    }
+
+    return output.build();
+
+    /*
+     * Note: We do not want to insert <p> tags inside <pre>. Fortunately, the formatter gets that
+     * right without special effort on our part. The reason: Line breaks inside a <pre> section are
+     * of type FORCED_NEWLINE rather than WHITESPACE.
+     */
+  }
+
+  /**
+   * Adjust indentation inside `<pre>{@code` blocks.
+   *
+   * <p>Also trim leading and trailing blank lines, and move the trailing `}` to its own line.
+   */
+  private static ImmutableList<Token> deindentPreCodeBlocks(List<Token> input) {
+    ImmutableList.Builder<Token> output = ImmutableList.builder();
+    for (PeekingIterator<Token> tokens = peekingIterator(input.iterator()); tokens.hasNext(); ) {
+      if (tokens.peek().getType() != PRE_OPEN_TAG) {
+        output.add(tokens.next());
+        continue;
+      }
+
+      output.add(tokens.next());
+      List<Token> initialNewlines = new ArrayList<>();
+      while (tokens.hasNext() && tokens.peek().getType() == FORCED_NEWLINE) {
+        initialNewlines.add(tokens.next());
+      }
+      if (tokens.peek().getType() != LITERAL
+          || !tokens.peek().getValue().matches("[ \t]*[{]@code")) {
+        output.addAll(initialNewlines);
+        output.add(tokens.next());
+        continue;
+      }
+
+      deindentPreCodeBlock(output, tokens);
+    }
+    return output.build();
+  }
+
+  private static void deindentPreCodeBlock(
+      ImmutableList.Builder<Token> output, PeekingIterator<Token> tokens) {
+    Deque<Token> saved = new ArrayDeque<>();
+    output.add(new Token(LITERAL, tokens.next().getValue().trim()));
+    while (tokens.hasNext() && tokens.peek().getType() != PRE_CLOSE_TAG) {
+      Token token = tokens.next();
+      saved.addLast(token);
+    }
+    while (!saved.isEmpty() && saved.peekFirst().getType() == FORCED_NEWLINE) {
+      saved.removeFirst();
+    }
+    while (!saved.isEmpty() && saved.peekLast().getType() == FORCED_NEWLINE) {
+      saved.removeLast();
+    }
+    if (saved.isEmpty()) {
+      return;
+    }
+
+    // move the trailing `}` to its own line
+    Token last = saved.peekLast();
+    boolean trailingBrace = false;
+    if (last.getType() == LITERAL && last.getValue().endsWith("}")) {
+      saved.removeLast();
+      if (last.length() > 1) {
+        saved.addLast(
+            new Token(LITERAL, last.getValue().substring(0, last.getValue().length() - 1)));
+        saved.addLast(new Token(FORCED_NEWLINE, null));
+      }
+      trailingBrace = true;
+    }
+
+    int trim = -1;
+    for (Token token : saved) {
+      if (token.getType() == LITERAL) {
+        int idx = CharMatcher.isNot(' ').indexIn(token.getValue());
+        if (idx != -1 && (trim == -1 || idx < trim)) {
+          trim = idx;
+        }
+      }
+    }
+
+    output.add(new Token(FORCED_NEWLINE, "\n"));
+    for (Token token : saved) {
+      if (token.getType() == LITERAL) {
+        output.add(
+            new Token(
+                LITERAL,
+                trim > 0 && token.length() > trim
+                    ? token.getValue().substring(trim)
+                    : token.getValue()));
+      } else {
+        output.add(token);
+      }
+    }
+
+    if (trailingBrace) {
+      output.add(new Token(LITERAL, "}"));
+    } else {
+      output.add(new Token(FORCED_NEWLINE, "\n"));
+    }
+  }
+
+  private static final CharMatcher NEWLINE = CharMatcher.is('\n');
+
+  private static boolean hasMultipleNewlines(String s) {
+    return NEWLINE.countIn(s) > 1;
+  }
+
+  /*
+   * This also eats any trailing whitespace. We would be smart enough to ignore that, anyway --
+   * except in the case of <pre>/<table>, inside which we otherwise leave whitespace intact.
+   *
+   * We'd remove the trailing whitespace later on (in JavaCommentsHelper.rewrite), but I feel safer
+   * stripping it now: It otherwise might confuse our line-length count, which we use for wrapping.
+   */
+  private static final Pattern NEWLINE_PATTERN = compile("^[ \t]*\n[ \t]*[*]?[ \t]?");
+
+  // We ensure elsewhere that we match this only at the beginning of a line.
+  // Only match tags that start with a lowercase letter, to avoid false matches on unescaped
+  // annotations inside code blocks.
+  // Match "@param <T>" specially in case the <T> is a <P> or other HTML tag we treat specially.
+  private static final Pattern FOOTER_TAG_PATTERN = compile("^@(param\\s+<\\w+>|[a-z]\\w*)");
+  private static final Pattern MOE_BEGIN_STRIP_COMMENT_PATTERN =
+      compile("^<!--\\s*MOE:begin_intracomment_strip\\s*-->");
+  private static final Pattern MOE_END_STRIP_COMMENT_PATTERN =
+      compile("^<!--\\s*MOE:end_intracomment_strip\\s*-->");
+  private static final Pattern HTML_COMMENT_PATTERN = fullCommentPattern();
+  private static final Pattern PRE_OPEN_PATTERN = openTagPattern("pre");
+  private static final Pattern PRE_CLOSE_PATTERN = closeTagPattern("pre");
+  private static final Pattern CODE_OPEN_PATTERN = openTagPattern("code");
+  private static final Pattern CODE_CLOSE_PATTERN = closeTagPattern("code");
+  private static final Pattern TABLE_OPEN_PATTERN = openTagPattern("table");
+  private static final Pattern TABLE_CLOSE_PATTERN = closeTagPattern("table");
+  private static final Pattern LIST_OPEN_PATTERN = openTagPattern("ul|ol|dl");
+  private static final Pattern LIST_CLOSE_PATTERN = closeTagPattern("ul|ol|dl");
+  private static final Pattern LIST_ITEM_OPEN_PATTERN = openTagPattern("li|dt|dd");
+  private static final Pattern LIST_ITEM_CLOSE_PATTERN = closeTagPattern("li|dt|dd");
+  private static final Pattern HEADER_OPEN_PATTERN = openTagPattern("h[1-6]");
+  private static final Pattern HEADER_CLOSE_PATTERN = closeTagPattern("h[1-6]");
+  private static final Pattern PARAGRAPH_OPEN_PATTERN = openTagPattern("p");
+  private static final Pattern PARAGRAPH_CLOSE_PATTERN = closeTagPattern("p");
+  private static final Pattern BLOCKQUOTE_OPEN_PATTERN = openTagPattern("blockquote");
+  private static final Pattern BLOCKQUOTE_CLOSE_PATTERN = closeTagPattern("blockquote");
+  private static final Pattern BR_PATTERN = openTagPattern("br");
+  private static final Pattern INLINE_TAG_OPEN_PATTERN = compile("^[{]@\\w*");
+  /*
+   * We exclude < so that we don't swallow following HTML tags. This lets us fix up "foo<p>" (~400
+   * hits in Google-internal code). We will join unnecessarily split "words" (like "foo<b>bar</b>")
+   * in a later step. There's a similar story for braces. I'm not sure I actually need to exclude @
+   * or *. TODO(cpovirk): Try removing them.
+   *
+   * Thanks to the "rejoin" step in joinAdjacentLiteralsAndAdjacentWhitespace(), we could get away
+   * with matching only one character here. That would eliminate the need for the regex entirely.
+   * That might be faster or slower than what we do now.
+   */
+  private static final Pattern LITERAL_PATTERN = compile("^.[^ \t\n@<{}*]*", DOTALL);
+
+  private static Pattern fullCommentPattern() {
+    return compile("^<!--.*?-->", DOTALL);
+  }
+
+  private static Pattern openTagPattern(String namePattern) {
+    return compile(format("^<(?:%s)\\b[^>]*>", namePattern), CASE_INSENSITIVE);
+  }
+
+  private static Pattern closeTagPattern(String namePattern) {
+    return compile(format("^</(?:%s)\\b[^>]*>", namePattern), CASE_INSENSITIVE);
+  }
+
+  static class LexException extends Exception {}
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java
new file mode 100644
index 0000000..c2431c4
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright 2016 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.javadoc;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Sets.immutableEnumSet;
+import static com.google.googlejavaformat.java.javadoc.JavadocWriter.AutoIndent.AUTO_INDENT;
+import static com.google.googlejavaformat.java.javadoc.JavadocWriter.AutoIndent.NO_AUTO_INDENT;
+import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.BLANK_LINE;
+import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NEWLINE;
+import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NONE;
+import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.WHITESPACE;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.HEADER_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_ITEM_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Ordering;
+import com.google.googlejavaformat.java.javadoc.Token.Type;
+
+/**
+ * Stateful object that accepts "requests" and "writes," producing formatted Javadoc.
+ *
+ * <p>Our Javadoc formatter doesn't ever generate a parse tree, only a stream of tokens, so the
+ * writer must compute and store the answer to questions like "How many levels of nested HTML list
+ * are we inside?"
+ */
+final class JavadocWriter {
+  private final int blockIndent;
+  private final StringBuilder output = new StringBuilder();
+  /**
+   * Whether we are inside an {@code <li>} element, excluding the case in which the {@code <li>}
+   * contains a {@code <ul>} or {@code <ol>} that we are also inside -- unless of course we're
+   * inside an {@code <li>} element in that inner list :)
+   */
+  private boolean continuingListItemOfInnermostList;
+
+  private boolean continuingFooterTag;
+  private final NestingCounter continuingListItemCount = new NestingCounter();
+  private final NestingCounter continuingListCount = new NestingCounter();
+  private final NestingCounter postWriteModifiedContinuingListCount = new NestingCounter();
+  private int remainingOnLine;
+  private boolean atStartOfLine;
+  private RequestedWhitespace requestedWhitespace = NONE;
+  private Token requestedMoeBeginStripComment;
+  private int indentForMoeEndStripComment;
+  private boolean wroteAnythingSignificant;
+
+  JavadocWriter(int blockIndent) {
+    this.blockIndent = blockIndent;
+  }
+
+  /**
+   * Requests whitespace between the previously written token and the next written token. The
+   * request may be honored, or it may be overridden by a request for "more significant" whitespace,
+   * like a newline.
+   */
+  void requestWhitespace() {
+    requestWhitespace(WHITESPACE);
+  }
+
+  void requestMoeBeginStripComment(Token token) {
+    // We queue this up so that we can put it after any requested whitespace.
+    requestedMoeBeginStripComment = checkNotNull(token);
+  }
+
+  void writeBeginJavadoc() {
+    /*
+     * JavaCommentsHelper will make sure this is indented right. But it seems sensible enough that,
+     * if our input starts with ∕✱✱, so too does our output.
+     */
+    output.append("/**");
+    writeNewline();
+  }
+
+  void writeEndJavadoc() {
+    output.append("\n");
+    appendSpaces(blockIndent + 1);
+    output.append("*/");
+  }
+
+  void writeFooterJavadocTagStart(Token token) {
+    // Close any unclosed lists (e.g., <li> without <ul>).
+    // TODO(cpovirk): Actually generate </ul>, etc.?
+    /*
+     * TODO(cpovirk): Also generate </pre> and </table> if appropriate. This is necessary for
+     * idempotency in broken Javadoc. (We don't necessarily need that, but full idempotency may be a
+     * nice goal, especially if it helps us use a fuzzer to test.) Unfortunately, the writer doesn't
+     * currently know which of those tags are open.
+     */
+    continuingListItemOfInnermostList = false;
+    continuingListItemCount.reset();
+    continuingListCount.reset();
+    /*
+     * There's probably no need for this, since its only effect is to disable blank lines in some
+     * cases -- and we're doing that already in the footer.
+     */
+    postWriteModifiedContinuingListCount.reset();
+
+    if (!wroteAnythingSignificant) {
+      // Javadoc consists solely of tags. This is frowned upon in general but OK for @Overrides.
+    } else if (!continuingFooterTag) {
+      // First footer tag after a body tag.
+      requestBlankLine();
+    } else {
+      // Subsequent footer tag.
+      continuingFooterTag = false;
+      requestNewline();
+    }
+    writeToken(token);
+    continuingFooterTag = true;
+  }
+
+  void writeListOpen(Token token) {
+    requestBlankLine();
+
+    writeToken(token);
+    continuingListItemOfInnermostList = false;
+    continuingListCount.increment();
+    postWriteModifiedContinuingListCount.increment();
+
+    requestNewline();
+  }
+
+  void writeListClose(Token token) {
+    requestNewline();
+
+    continuingListItemCount.decrementIfPositive();
+    continuingListCount.decrementIfPositive();
+    writeToken(token);
+    postWriteModifiedContinuingListCount.decrementIfPositive();
+
+    requestBlankLine();
+  }
+
+  void writeListItemOpen(Token token) {
+    requestNewline();
+
+    if (continuingListItemOfInnermostList) {
+      continuingListItemOfInnermostList = false;
+      continuingListItemCount.decrementIfPositive();
+    }
+    writeToken(token);
+    continuingListItemOfInnermostList = true;
+    continuingListItemCount.increment();
+  }
+
+  void writeHeaderOpen(Token token) {
+    requestBlankLine();
+
+    writeToken(token);
+  }
+
+  void writeHeaderClose(Token token) {
+    writeToken(token);
+
+    requestBlankLine();
+  }
+
+  void writeParagraphOpen(Token token) {
+    if (!wroteAnythingSignificant) {
+      /*
+       * The user included an initial <p> tag. Ignore it, and don't request a blank line before the
+       * next token.
+       */
+      return;
+    }
+
+    requestBlankLine();
+
+    writeToken(token);
+  }
+
+  void writeBlockquoteOpenOrClose(Token token) {
+    requestBlankLine();
+
+    writeToken(token);
+
+    requestBlankLine();
+  }
+
+  void writePreOpen(Token token) {
+    requestBlankLine();
+
+    writeToken(token);
+  }
+
+  void writePreClose(Token token) {
+    writeToken(token);
+
+    requestBlankLine();
+  }
+
+  void writeCodeOpen(Token token) {
+    writeToken(token);
+  }
+
+  void writeCodeClose(Token token) {
+    writeToken(token);
+  }
+
+  void writeTableOpen(Token token) {
+    requestBlankLine();
+
+    writeToken(token);
+  }
+
+  void writeTableClose(Token token) {
+    writeToken(token);
+
+    requestBlankLine();
+  }
+
+  void writeMoeEndStripComment(Token token) {
+    writeLineBreakNoAutoIndent();
+    appendSpaces(indentForMoeEndStripComment);
+
+    // Or maybe just "output.append(token.getValue())?" I'm kind of surprised this is so easy.
+    writeToken(token);
+
+    requestNewline();
+  }
+
+  void writeHtmlComment(Token token) {
+    requestNewline();
+
+    writeToken(token);
+
+    requestNewline();
+  }
+
+  void writeBr(Token token) {
+    writeToken(token);
+
+    requestNewline();
+  }
+
+  void writeLineBreakNoAutoIndent() {
+    writeNewline(NO_AUTO_INDENT);
+  }
+
+  void writeLiteral(Token token) {
+    writeToken(token);
+  }
+
+  @Override
+  public String toString() {
+    return output.toString();
+  }
+
+  private void requestBlankLine() {
+    requestWhitespace(BLANK_LINE);
+  }
+
+  private void requestNewline() {
+    requestWhitespace(NEWLINE);
+  }
+
+  private void requestWhitespace(RequestedWhitespace requestedWhitespace) {
+    this.requestedWhitespace =
+        Ordering.natural().max(requestedWhitespace, this.requestedWhitespace);
+  }
+
+  /**
+   * The kind of whitespace that has been requested between the previous and next tokens. The order
+   * of the values is significant: It goes from lowest priority to highest. For example, if the
+   * previous token requests {@link #BLANK_LINE} after it but the next token requests only {@link
+   * #NEWLINE} before it, we insert {@link #BLANK_LINE}.
+   */
+  enum RequestedWhitespace {
+    NONE,
+    WHITESPACE,
+    NEWLINE,
+    BLANK_LINE,
+    ;
+  }
+
+  private void writeToken(Token token) {
+    if (requestedMoeBeginStripComment != null) {
+      requestNewline();
+    }
+
+    if (requestedWhitespace == BLANK_LINE
+        && (postWriteModifiedContinuingListCount.isPositive() || continuingFooterTag)) {
+      /*
+       * We don't write blank lines inside lists or footer tags, even in cases where we otherwise
+       * would (e.g., before a <p> tag). Justification: We don't write blank lines _between_ list
+       * items or footer tags, so it would be strange to write blank lines _within_ one. Of course,
+       * an alternative approach would be to go ahead and write blank lines between items/tags,
+       * either always or only in the case that an item contains a blank line.
+       */
+      requestedWhitespace = NEWLINE;
+    }
+
+    if (requestedWhitespace == BLANK_LINE) {
+      writeBlankLine();
+      requestedWhitespace = NONE;
+    } else if (requestedWhitespace == NEWLINE) {
+      writeNewline();
+      requestedWhitespace = NONE;
+    }
+    boolean needWhitespace = (requestedWhitespace == WHITESPACE);
+
+    /*
+     * Write a newline if necessary to respect the line limit. (But if we're at the beginning of the
+     * line, a newline won't help. Or it might help but only by separating "<p>veryverylongword,"
+     * which goes against our style.)
+     */
+    if (!atStartOfLine && token.length() + (needWhitespace ? 1 : 0) > remainingOnLine) {
+      writeNewline();
+    }
+    if (!atStartOfLine && needWhitespace) {
+      output.append(" ");
+      remainingOnLine--;
+    }
+
+    if (requestedMoeBeginStripComment != null) {
+      output.append(requestedMoeBeginStripComment.getValue());
+      requestedMoeBeginStripComment = null;
+      indentForMoeEndStripComment = innerIndent();
+      requestNewline();
+      writeToken(token);
+      return;
+    }
+
+    output.append(token.getValue());
+
+    if (!START_OF_LINE_TOKENS.contains(token.getType())) {
+      atStartOfLine = false;
+    }
+
+    /*
+     * TODO(cpovirk): We really want the number of "characters," not chars. Figure out what the
+     * right way of measuring that is (grapheme count (with BreakIterator?)? sum of widths of all
+     * graphemes? I don't think that our style guide is specific about this.). Moreover, I am
+     * probably brushing other problems with surrogates, etc. under the table. Hopefully I mostly
+     * get away with it by joining all non-space, non-tab characters together.
+     *
+     * Possibly the "width" question has no right answer:
+     * http://denisbider.blogspot.com/2015/09/when-monospace-fonts-arent-unicode.html
+     */
+    remainingOnLine -= token.length();
+    requestedWhitespace = NONE;
+    wroteAnythingSignificant = true;
+  }
+
+  private void writeBlankLine() {
+    output.append("\n");
+    appendSpaces(blockIndent + 1);
+    output.append("*");
+    writeNewline();
+  }
+
+  private void writeNewline() {
+    writeNewline(AUTO_INDENT);
+  }
+
+  private void writeNewline(AutoIndent autoIndent) {
+    output.append("\n");
+    appendSpaces(blockIndent + 1);
+    output.append("*");
+    appendSpaces(1);
+    remainingOnLine = JavadocFormatter.MAX_LINE_LENGTH - blockIndent - 3;
+    if (autoIndent == AUTO_INDENT) {
+      appendSpaces(innerIndent());
+      remainingOnLine -= innerIndent();
+    }
+    atStartOfLine = true;
+  }
+
+  enum AutoIndent {
+    AUTO_INDENT,
+    NO_AUTO_INDENT
+  }
+
+  private int innerIndent() {
+    int innerIndent = continuingListItemCount.value() * 4 + continuingListCount.value() * 2;
+    if (continuingFooterTag) {
+      innerIndent += 4;
+    }
+    return innerIndent;
+  }
+
+  // If this is a hotspot, keep a String of many spaces around, and call append(string, start, end).
+  private void appendSpaces(int count) {
+    output.append(Strings.repeat(" ", count));
+  }
+
+  /**
+   * Tokens that are always pinned to the following token. For example, {@code <p>} in {@code <p>Foo
+   * bar} (never {@code <p> Foo bar} or {@code <p>\nFoo bar}).
+   *
+   * <p>This is not the only kind of "pinning" that we do: See also the joining of LITERAL tokens
+   * done by the lexer. The special pinning here is necessary because these tokens are not of type
+   * LITERAL (because they require other special handling).
+   */
+  private static final ImmutableSet<Type> START_OF_LINE_TOKENS =
+      immutableEnumSet(LIST_ITEM_OPEN_TAG, PARAGRAPH_OPEN_TAG, HEADER_OPEN_TAG);
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingCounter.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingCounter.java
new file mode 100644
index 0000000..43e7125
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingCounter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016 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.javadoc;
+
+/** Mutable integer for tracking the level of nesting. */
+final class NestingCounter {
+  private int value;
+
+  int value() {
+    return value;
+  }
+
+  void increment() {
+    value++;
+  }
+
+  void incrementIfPositive() {
+    if (value > 0) {
+      value++;
+    }
+  }
+
+  void decrementIfPositive() {
+    if (value > 0) {
+      value--;
+    }
+  }
+
+  boolean isPositive() {
+    return value > 0;
+  }
+
+  void reset() {
+    value = 0;
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/Token.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/Token.java
new file mode 100644
index 0000000..d617824
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/Token.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2016 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.javadoc;
+
+/**
+ * Javadoc token. Our idea of what constitutes a token is often larger or smaller than what you'd
+ * naturally expect. The decision is usually pragmatic rather than theoretical. Most of the details
+ * are in {@link JavadocLexer}.
+ */
+final class Token {
+  /**
+   * Javadoc token type.
+   *
+   * <p>The general idea is that every token that requires special handling (extra line breaks,
+   * indentation, forcing or forbidding whitespace) from {@link JavadocWriter} gets its own type.
+   * But I haven't been super careful about it, so I'd imagine that we could merge or remove some of
+   * these if we wanted. (For example, PARAGRAPH_CLOSE_TAG and LIST_ITEM_CLOSE_TAG could share a
+   * common IGNORABLE token type. But their corresponding OPEN tags exist, so I've kept the CLOSE
+   * tags.)
+   *
+   * <p>Note, though, that tokens of the same type may still have been handled differently by {@link
+   * JavadocLexer} when it created them. For example, LITERAL is used for both plain text and inline
+   * tags, even though the two affect the lexer's state differently.
+   */
+  enum Type {
+    /** ∕✱✱ */
+    BEGIN_JAVADOC,
+    /** ✱∕ */
+    END_JAVADOC,
+    /** The {@code @foo} that begins a block Javadoc tag like {@code @throws}. */
+    FOOTER_JAVADOC_TAG_START,
+    LIST_OPEN_TAG,
+    LIST_CLOSE_TAG,
+    LIST_ITEM_OPEN_TAG,
+    LIST_ITEM_CLOSE_TAG,
+    HEADER_OPEN_TAG,
+    HEADER_CLOSE_TAG,
+    PARAGRAPH_OPEN_TAG,
+    PARAGRAPH_CLOSE_TAG,
+    // TODO(cpovirk): Support <div> (probably identically to <blockquote>).
+    BLOCKQUOTE_OPEN_TAG,
+    BLOCKQUOTE_CLOSE_TAG,
+    PRE_OPEN_TAG,
+    PRE_CLOSE_TAG,
+    CODE_OPEN_TAG,
+    CODE_CLOSE_TAG,
+    TABLE_OPEN_TAG,
+    TABLE_CLOSE_TAG,
+    /** {@code <!-- MOE:begin_intracomment_strip -->} */
+    MOE_BEGIN_STRIP_COMMENT,
+    /** {@code <!-- MOE:end_intracomment_strip -->} */
+    MOE_END_STRIP_COMMENT,
+    HTML_COMMENT,
+    // TODO(cpovirk): Support <hr> (probably a blank line before and after).
+    BR_TAG,
+    /**
+     * Whitespace that is not in a {@code <pre>} or {@code <table>} section. Whitespace includes
+     * leading newlines, asterisks, and tabs and spaces. In the output, it is translated to newlines
+     * (with leading spaces and asterisks) or spaces.
+     */
+    WHITESPACE,
+    /**
+     * A newline in a {@code <pre>} or {@code <table>} section. We preserve user formatting in these
+     * sections, including newlines.
+     */
+    FORCED_NEWLINE,
+    /**
+     * Token that permits but does not force a line break. The way that we accomplish this is
+     * somewhat indirect: As far as {@link JavadocWriter} is concerned, this token is meaningless.
+     * But its mere existence prevents {@link JavadocLexer} from joining two {@link #LITERAL} tokens
+     * that would otherwise be adjacent. Since this token is not real whitespace, the writer may end
+     * up writing the literals together with no space between, just as if they'd been joined.
+     * However, if they don't fit together on the line, the writer will write the first one, start a
+     * new line, and write the second. Hence, the token acts as an optional line break.
+     */
+    OPTIONAL_LINE_BREAK,
+    /**
+     * Anything else: {@code foo}, {@code <b>}, {@code {@code foo}} etc. {@link JavadocLexer}
+     * sometimes creates adjacent literal tokens, which it then merges into a single, larger literal
+     * token before returning its output.
+     *
+     * <p>This also includes whitespace in a {@code <pre>} or {@code <table>} section. We preserve
+     * user formatting in these sections, including arbitrary numbers of spaces. By treating such
+     * whitespace as a literal, we can merge it with adjacent literals, preventing us from
+     * autowrapping inside these sections -- and doing so naively, to boot. The wrapped line would
+     * have no indentation after "* " or, possibly worse, it might begin with an arbitrary amount of
+     * whitespace that didn't fit on the previous line. Of course, by doing this, we're potentially
+     * creating lines of more than 100 characters. But it seems fair to call in the humans to
+     * resolve such problems.
+     */
+    LITERAL,
+    ;
+  }
+
+  private final Type type;
+  private final String value;
+
+  Token(Type type, String value) {
+    this.type = type;
+    this.value = value;
+  }
+
+  Type getType() {
+    return type;
+  }
+
+  String getValue() {
+    return value;
+  }
+
+  int length() {
+    return value.length();
+  }
+
+  @Override
+  public String toString() {
+    return "\n" + getType() + ": " + getValue();
+  }
+}
diff --git a/core/src/main/scripts/google-java-format.el b/core/src/main/scripts/google-java-format.el
new file mode 100644
index 0000000..f9e8d2a
--- /dev/null
+++ b/core/src/main/scripts/google-java-format.el
@@ -0,0 +1,105 @@
+;;; google-java-format.el --- Format code with google-java-format -*- lexical-binding: t; -*-
+;;
+;; Copyright 2015 Google, Inc. All Rights Reserved.
+;;
+;; Package-Requires: ((emacs "24"))
+;;
+;; 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.
+
+;; Keywords: tools, Java
+
+;;; Commentary:
+
+;; This package allows a user to filter code through
+;; google-java-format, fixing its formatting.
+
+;; To use it, ensure the directory of this file is in your `load-path'
+;; and add
+;;
+;;   (require 'google-java-format)
+;;
+;; to your .emacs configuration.
+
+;; You may also want to bind `google-java-format-region' to a key:
+;;
+;;   (global-set-key [C-M-tab] #'google-java-format-region)
+
+;;; Code:
+
+(defgroup google-java-format nil
+  "Format code using google-java-format."
+  :group 'tools)
+
+(defcustom google-java-format-executable
+  "/usr/bin/google-java-format"
+  "Location of the google-java-format executable.
+
+A string containing the name or the full path of the executable."
+  :group 'google-java-format
+  :type '(file :must-match t :match #'file-executable-p)
+  :risky t)
+
+;;;###autoload
+(defun google-java-format-region (start end)
+  "Use google-java-format to format the code between START and END.
+If called interactively, uses the region, if there is one.  If
+there is no region, then formats the current line."
+  (interactive
+   (if (use-region-p)
+       (list (region-beginning) (region-end))
+     (list (point) (1+ (point)))))
+  (let ((cursor (point))
+        (temp-buffer (generate-new-buffer " *google-java-format-temp*"))
+        (stderr-file (make-temp-file "google-java-format")))
+    (unwind-protect
+        (let ((status (call-process-region
+                       ;; Note that emacs character positions are 1-indexed,
+                       ;; and google-java-format is 0-indexed, so we have to
+                       ;; subtract 1 from START to line it up correctly.
+                       (point-min) (point-max)
+                       google-java-format-executable
+                       nil (list temp-buffer stderr-file) t
+                       "--offset" (number-to-string (1- start))
+                       "--length" (number-to-string (- end start))
+                       "-"))
+              (stderr
+               (with-temp-buffer
+                 (insert-file-contents stderr-file)
+                 (when (> (point-max) (point-min))
+                   (insert ": "))
+                 (buffer-substring-no-properties
+                  (point-min) (line-end-position)))))
+          (cond
+           ((stringp status)
+            (error "google-java-format killed by signal %s%s" status stderr))
+           ((not (zerop status))
+            (error "google-java-format failed with code %d%s" status stderr))
+           (t (message "google-java-format succeeded%s" stderr)
+              (delete-region (point-min) (point-max))
+              (insert-buffer-substring temp-buffer)
+              (goto-char cursor))))
+      (delete-file stderr-file)
+      (when (buffer-name temp-buffer) (kill-buffer temp-buffer)))))
+
+;;;###autoload
+(defun google-java-format-buffer ()
+  "Use google-java-format to format the current buffer."
+  (interactive)
+  (google-java-format-region (point-min) (point-max)))
+
+;;;###autoload
+(defalias 'google-java-format 'google-java-format-region)
+
+(provide 'google-java-format)
+
+;;; google-java-format.el ends here
diff --git a/core/src/test/java/com/google/googlejavaformat/NewlinesTest.java b/core/src/test/java/com/google/googlejavaformat/NewlinesTest.java
new file mode 100644
index 0000000..4b7dab7
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/NewlinesTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2016 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** {@link Newlines}Test */
+@RunWith(JUnit4.class)
+public class NewlinesTest {
+  @Test
+  public void offsets() {
+    assertThat(ImmutableList.copyOf(Newlines.lineOffsetIterator("foo\nbar\n")))
+        .containsExactly(0, 4, 8);
+    assertThat(ImmutableList.copyOf(Newlines.lineOffsetIterator("foo\nbar"))).containsExactly(0, 4);
+
+    assertThat(ImmutableList.copyOf(Newlines.lineOffsetIterator("foo\rbar\r")))
+        .containsExactly(0, 4, 8);
+    assertThat(ImmutableList.copyOf(Newlines.lineOffsetIterator("foo\rbar"))).containsExactly(0, 4);
+
+    assertThat(ImmutableList.copyOf(Newlines.lineOffsetIterator("foo\r\nbar\r\n")))
+        .containsExactly(0, 5, 10);
+    assertThat(ImmutableList.copyOf(Newlines.lineOffsetIterator("foo\r\nbar")))
+        .containsExactly(0, 5);
+  }
+
+  @Test
+  public void lines() {
+    assertThat(ImmutableList.copyOf(Newlines.lineIterator("foo\nbar\n")))
+        .containsExactly("foo\n", "bar\n");
+    assertThat(ImmutableList.copyOf(Newlines.lineIterator("foo\nbar")))
+        .containsExactly("foo\n", "bar");
+
+    assertThat(ImmutableList.copyOf(Newlines.lineIterator("foo\rbar\r")))
+        .containsExactly("foo\r", "bar\r");
+    assertThat(ImmutableList.copyOf(Newlines.lineIterator("foo\rbar")))
+        .containsExactly("foo\r", "bar");
+
+    assertThat(ImmutableList.copyOf(Newlines.lineIterator("foo\r\nbar\r\n")))
+        .containsExactly("foo\r\n", "bar\r\n");
+    assertThat(ImmutableList.copyOf(Newlines.lineIterator("foo\r\nbar")))
+        .containsExactly("foo\r\n", "bar");
+  }
+
+  @Test
+  public void terminalOffset() {
+    Iterator<Integer> it = Newlines.lineOffsetIterator("foo\nbar\n");
+    it.next();
+    it.next();
+    it.next();
+    try {
+      it.next();
+      fail();
+    } catch (NoSuchElementException e) {
+      // expected
+    }
+
+    it = Newlines.lineOffsetIterator("foo\nbar");
+    it.next();
+    it.next();
+    try {
+      it.next();
+      fail();
+    } catch (NoSuchElementException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void terminalLine() {
+    Iterator<String> it = Newlines.lineIterator("foo\nbar\n");
+    it.next();
+    it.next();
+    try {
+      it.next();
+      fail();
+    } catch (NoSuchElementException e) {
+      // expected
+    }
+
+    it = Newlines.lineIterator("foo\nbar");
+    it.next();
+    it.next();
+    try {
+      it.next();
+      fail();
+    } catch (NoSuchElementException e) {
+      // expected
+    }
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/ArrayDimensionTest.java b/core/src/test/java/com/google/googlejavaformat/java/ArrayDimensionTest.java
new file mode 100644
index 0000000..3da9834
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/ArrayDimensionTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2016 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.truth.Truth.assertThat;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests for array dimension handling, especially mixed array notation and type annotations on
+ * dimensions.
+ */
+@RunWith(Parameterized.class)
+public class ArrayDimensionTest {
+  @Parameters
+  public static Iterable<Object[]> parameters() {
+    String[] inputs = {
+      // mixed array notation multi-variable declarations
+      "int a[], b @B [], c @B [][] @C [];",
+      "int a @A [], b @B [], c @B [] @C [];",
+      "int a[] @A [], b @B [], c @B [] @C [];",
+      "int a, b @B [], c @B [] @C [];",
+      "int @A [] a, b @B [], c @B [] @C [];",
+      "int @A [] a = {}, b @B [] = {{}}, c @B [] @C [] = {{{}}};",
+      // mixed array notation methods
+      "int[][][][][] argh()[] @A @B [] @C @D [][] {}",
+      "int[][] @T [] @U [] @V @W [][][] argh() @A @B [] @C @D [] {}",
+      "int e1() @A [] {}",
+      "int f1()[] @A [] {}",
+      "int g1() @A [] @B [] {}",
+      "int h1() @A @B [] @C @B [] {}",
+      "int[] e2() @A [] {}",
+      "int @X [] f2()[] @A [] {}",
+      "int[] g2() @A [] @B [] {}",
+      "int @X [] h2() @A @B [] @C @B [] {}",
+      "@X int[] e3() @A [] {}",
+      "@X int @Y [] f3()[] @A [] {}",
+      "@X int @Y [] g3() @A [] @B [] {}",
+      "@X int[] h3() @A @B [] @C @B [] {}",
+      // mixed array notation single-variable declarations
+      "int[] e2() @A [] {}",
+      "int @I [] f2()[] @A [] {}",
+      "int[] @J [] g2() @A [] @B [] {}",
+      "int @I [] @J [] h2() @A @B [] @C @B [] {}",
+      "int a1[];",
+      "int b1 @A [];",
+      "int c1[] @A [];",
+      "int d1 @A [] @B [];",
+      "int[] a2[];",
+      "int @A [] b2 @B [];",
+      "int[] c2[] @A [];",
+      "int @A [] d2 @B [] @C [];",
+      // array dimension expressions
+      "int[][] a0 = new @A int[0];",
+      "int[][] a1 = new int @A [0] @B [];",
+      "int[][] a2 = new int[0] @A [] @B [];",
+      "int[][] a4 = new int[0] @A [][] @B [];",
+      // nested array type uses
+      "List<int @A [] @B []> xs;",
+      "List<int[] @A [][] @B []> xs;",
+    };
+    return Iterables.transform(Arrays.asList(inputs), input -> new Object[] {input});
+  }
+
+  private final String input;
+
+  public ArrayDimensionTest(String input) {
+    this.input = input;
+  }
+
+  @Test
+  public void format() throws Exception {
+    String source = "class T {" + input + "}";
+    String formatted = new Formatter().formatSource(source);
+    String statement =
+        formatted.substring("class T {".length(), formatted.length() - "}\n".length());
+    // ignore line breaks after declaration-style annotations
+    statement =
+        Joiner.on(' ').join(Splitter.on('\n').omitEmptyStrings().trimResults().split(statement));
+    assertThat(statement).isEqualTo(input);
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/CommandLineFlagsTest.java b/core/src/test/java/com/google/googlejavaformat/java/CommandLineFlagsTest.java
new file mode 100644
index 0000000..e5fbc9f
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/CommandLineFlagsTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2015 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.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for command-line flags.
+ */
+@RunWith(JUnit4.class)
+public class CommandLineFlagsTest {
+
+  // TODO(eaftan): Disallow passing both -lines and -offset/-length, like clang-format.
+
+  @Test
+  public void formatInPlaceRequiresAtLeastOneFile() throws UsageException {
+    try {
+      Main.processArgs("-i");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+
+    try {
+      Main.processArgs("-i", "-");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+
+    Main.processArgs("-i", "Foo.java");
+    Main.processArgs("-i", "Foo.java", "Bar.java");
+  }
+
+  @Test
+  public void formatASubsetRequiresExactlyOneFile() throws UsageException {
+    Main.processArgs("-lines", "10", "Foo.java");
+
+    try {
+      Main.processArgs("-lines", "10");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+
+    try {
+      Main.processArgs("-lines", "10", "Foo.java", "Bar.java");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+
+    Main.processArgs("-offset", "10", "-length", "10", "Foo.java");
+
+    try {
+      Main.processArgs("-offset", "10", "-length", "10");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+
+    try {
+      Main.processArgs("-offset", "10", "-length", "10", "Foo.java", "Bar.java");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+  }
+
+  // TODO(eaftan): clang-format allows a single offset with no length, which means to format
+  // up to the end of the file.  We should match that behavior.
+  @Test
+  public void numberOfOffsetsMustMatchNumberOfLengths() throws UsageException {
+    Main.processArgs("-offset", "10", "-length", "20", "Foo.java");
+
+    try {
+      Main.processArgs("-offset", "10", "-length", "20", "-offset", "50", "Foo.java");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+
+    try {
+      Main.processArgs("-offset", "10", "-length", "20", "-length", "50", "Foo.java");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void noFilesToFormatRequiresEitherHelpOrVersion() throws UsageException {
+    Main.processArgs("-version");
+
+    Main.processArgs("-help");
+
+    try {
+      Main.processArgs();
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+
+    try {
+      Main.processArgs("-aosp");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void stdinAndFiles() {
+    try {
+      Main.processArgs("-", "A.java");
+      fail();
+    } catch (UsageException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .contains("cannot format from standard input and files simultaneously");
+    }
+  }
+
+  @Test
+  public void inPlaceStdin() {
+    try {
+      Main.processArgs("-i", "-");
+      fail();
+    } catch (UsageException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .contains("in-place formatting was requested but no files were provided");
+    }
+  }
+
+  @Test
+  public void inPlaceDryRun() {
+    try {
+      Main.processArgs("-i", "-n", "A.java");
+      fail();
+    } catch (UsageException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .contains("cannot use --dry-run and --in-place at the same time");
+    }
+  }
+
+  @Test
+  public void assumeFileNameOnlyWorksWithStdin() {
+    try {
+      Main.processArgs("--assume-filename=Foo.java", "Foo.java");
+      fail();
+    } catch (UsageException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .contains("--assume-filename is only supported when formatting standard input");
+    }
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/CommandLineOptionsParserTest.java b/core/src/test/java/com/google/googlejavaformat/java/CommandLineOptionsParserTest.java
new file mode 100644
index 0000000..8d71f4d
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/CommandLineOptionsParserTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2016 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.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Range;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** {@link CommandLineOptionsParser}Test */
+@RunWith(JUnit4.class)
+public class CommandLineOptionsParserTest {
+
+  @Rule public TemporaryFolder testFolder = new TemporaryFolder();
+
+  @Test
+  public void defaults() {
+    CommandLineOptions options = CommandLineOptionsParser.parse(Collections.<String>emptyList());
+    assertThat(options.files()).isEmpty();
+    assertThat(options.stdin()).isFalse();
+    assertThat(options.aosp()).isFalse();
+    assertThat(options.help()).isFalse();
+    assertThat(options.lengths()).isEmpty();
+    assertThat(options.lines().asRanges()).isEmpty();
+    assertThat(options.offsets()).isEmpty();
+    assertThat(options.inPlace()).isFalse();
+    assertThat(options.version()).isFalse();
+    assertThat(options.sortImports()).isTrue();
+    assertThat(options.removeUnusedImports()).isTrue();
+    assertThat(options.dryRun()).isFalse();
+    assertThat(options.setExitIfChanged()).isFalse();
+    assertThat(options.reflowLongStrings()).isTrue();
+    assertThat(options.formatJavadoc()).isTrue();
+  }
+
+  @Test
+  public void hello() {
+    CommandLineOptions options =
+        CommandLineOptionsParser.parse(
+            Arrays.asList("-lines=1:10,20:30", "-i", "Hello.java", "Goodbye.java"));
+    assertThat(options.lines().asRanges())
+        .containsExactly(Range.closedOpen(0, 10), Range.closedOpen(19, 30));
+    assertThat(options.inPlace()).isTrue();
+    assertThat(options.files()).containsExactly("Hello.java", "Goodbye.java");
+  }
+
+  @Test
+  public void stdin() {
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("-")).stdin()).isTrue();
+  }
+
+  @Test
+  public void aosp() {
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("-aosp")).aosp()).isTrue();
+  }
+
+  @Test
+  public void help() {
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("-help")).help()).isTrue();
+  }
+
+  @Test
+  public void lengths() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("-length", "1", "--length", "2"))
+                .lengths())
+        .containsExactly(1, 2);
+  }
+
+  @Test
+  public void lines() {
+    assertThat(
+            CommandLineOptionsParser.parse(
+                    Arrays.asList("--lines", "1:2", "-lines=4:5", "--line", "7:8", "-line=10:11"))
+                .lines()
+                .asRanges())
+        .containsExactly(
+            Range.closedOpen(0, 2),
+            Range.closedOpen(3, 5),
+            Range.closedOpen(6, 8),
+            Range.closedOpen(9, 11));
+  }
+
+  @Test
+  public void offset() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("-offset", "1", "--offset", "2"))
+                .offsets())
+        .containsExactly(1, 2);
+  }
+
+  @Test
+  public void inPlace() {
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("-i", "A.java")).inPlace()).isTrue();
+  }
+
+  @Test
+  public void version() {
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("-v")).version()).isTrue();
+  }
+
+  @Test
+  public void skipSortingImports() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("--skip-sorting-imports")).sortImports())
+        .isFalse();
+  }
+
+  @Test
+  public void skipRemovingUnusedImports() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("--skip-removing-unused-imports"))
+                .removeUnusedImports())
+        .isFalse();
+  }
+
+  @Test
+  public void dryRun() {
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("--dry-run")).dryRun()).isTrue();
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("-n")).dryRun()).isTrue();
+  }
+
+  @Test
+  public void setExitIfChanged() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("--set-exit-if-changed"))
+                .setExitIfChanged())
+        .isTrue();
+  }
+
+  // TODO(cushon): consider handling this in the parser and reporting a more detailed error
+  @Test
+  public void illegalLines() {
+    try {
+      CommandLineOptionsParser.parse(Arrays.asList("-lines=1:1", "-lines=1:1"));
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage()).contains("overlap");
+    }
+  }
+
+  @Test
+  public void paramsFile() throws IOException {
+    Path outer = testFolder.newFile("outer").toPath();
+    Path exit = testFolder.newFile("exit").toPath();
+    Path nested = testFolder.newFile("nested").toPath();
+
+    String[] args = {"--dry-run", "@" + exit, "L", "@" + outer, "Q"};
+
+    Files.write(exit, "--set-exit-if-changed".getBytes(UTF_8));
+    Files.write(outer, ("M\n@" + nested.toAbsolutePath() + "\nP").getBytes(UTF_8));
+    Files.write(nested, "ℕ\n\n   \n@@O\n".getBytes(UTF_8));
+
+    CommandLineOptions options = CommandLineOptionsParser.parse(Arrays.asList(args));
+    assertThat(options.files()).containsExactly("L", "M", "ℕ", "@O", "P", "Q");
+  }
+
+  @Test
+  public void assumeFilename() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("--assume-filename", "Foo.java"))
+                .assumeFilename())
+        .hasValue("Foo.java");
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("Foo.java")).assumeFilename())
+        .isEmpty();
+  }
+
+  @Test
+  public void skipReflowLongStrings() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("--skip-reflowing-long-strings"))
+                .reflowLongStrings())
+        .isFalse();
+  }
+
+  @Test
+  public void skipJavadocFormatting() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("--skip-javadoc-formatting"))
+                .formatJavadoc())
+        .isFalse();
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java b/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java
new file mode 100644
index 0000000..0b81ba6
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2015 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.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Joiner;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Locale;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for error reporting. */
+@RunWith(JUnit4.class)
+public class DiagnosticTest {
+  @Rule public TemporaryFolder testFolder = new TemporaryFolder();
+
+  private Locale backupLocale;
+
+  @Before
+  public void setUpLocale() throws Exception {
+    backupLocale = Locale.getDefault();
+    Locale.setDefault(Locale.ROOT);
+  }
+
+  @After
+  public void restoreLocale() throws Exception {
+    Locale.setDefault(backupLocale);
+  }
+
+  @Test
+  public void parseError() throws Exception {
+    String input =
+        Joiner.on('\n')
+            .join(
+                "public class InvalidSyntax {",
+                "  private static NumPrinter {",
+                "    public static void print(int n) {",
+                "      System.out.printf(\"%d%n\", n);",
+                "    }",
+                "  }",
+                "",
+                "  public static void main(String[] args) {",
+                "    NumPrinter.print(args.length);",
+                "  }",
+                "}");
+
+    StringWriter stdout = new StringWriter();
+    StringWriter stderr = new StringWriter();
+    Main main = new Main(new PrintWriter(stdout, true), new PrintWriter(stderr, true), System.in);
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("InvalidSyntax.java");
+    Files.write(path, input.getBytes(UTF_8));
+
+    int result = main.format(path.toString());
+    assertThat(stdout.toString()).isEmpty();
+    assertThat(stderr.toString()).contains("InvalidSyntax.java:2:29: error: <identifier> expected");
+    assertThat(result).isEqualTo(1);
+  }
+
+  @Test
+  public void lexError() throws Exception {
+    String input = "\\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuu00not-actually-a-unicode-escape-sequence";
+
+    StringWriter stdout = new StringWriter();
+    StringWriter stderr = new StringWriter();
+    Main main = new Main(new PrintWriter(stdout, true), new PrintWriter(stderr, true), System.in);
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("InvalidSyntax.java");
+    Files.write(path, input.getBytes(UTF_8));
+
+    int result = main.format(path.toString());
+    assertThat(stdout.toString()).isEmpty();
+    assertThat(stderr.toString())
+        .contains("InvalidSyntax.java:1:35: error: illegal unicode escape");
+    assertThat(result).isEqualTo(1);
+  }
+
+  @Test
+  public void oneFileParseError() throws Exception {
+    String one = "class One {\n";
+    String two = "class Two {}\n";
+
+    StringWriter stdout = new StringWriter();
+    StringWriter stderr = new StringWriter();
+    Main main = new Main(new PrintWriter(stdout, true), new PrintWriter(stderr, true), System.in);
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path pathOne = tmpdir.resolve("One.java");
+    Files.write(pathOne, one.getBytes(UTF_8));
+
+    Path pathTwo = tmpdir.resolve("Two.java");
+    Files.write(pathTwo, two.getBytes(UTF_8));
+
+    int result = main.format(pathOne.toString(), pathTwo.toString());
+    assertThat(stdout.toString()).isEqualTo(two);
+    assertThat(stderr.toString()).contains("One.java:1:13: error: reached end of file");
+    assertThat(result).isEqualTo(1);
+  }
+
+  @Test
+  public void oneFileParseErrorReplace() throws Exception {
+    String one = "class One {}}\n";
+    String two = "class Two {\n}\n";
+
+    StringWriter stdout = new StringWriter();
+    StringWriter stderr = new StringWriter();
+    Main main = new Main(new PrintWriter(stdout, true), new PrintWriter(stderr, true), System.in);
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path pathOne = tmpdir.resolve("One.java");
+    Files.write(pathOne, one.getBytes(UTF_8));
+
+    Path pathTwo = tmpdir.resolve("Two.java");
+    Files.write(pathTwo, two.getBytes(UTF_8));
+
+    int result = main.format("-i", pathOne.toString(), pathTwo.toString());
+    assertThat(stdout.toString()).isEmpty();
+    assertThat(stderr.toString()).contains("One.java:1:14: error: class, interface");
+    assertThat(result).isEqualTo(1);
+    // don't edit files with parse errors
+    assertThat(Files.readAllLines(pathOne, UTF_8)).containsExactly("class One {}}");
+    assertThat(Files.readAllLines(pathTwo, UTF_8)).containsExactly("class Two {}");
+  }
+
+  @Test
+  public void parseError2() throws FormatterException, IOException, UsageException {
+    String input = "class Foo { void f() {\n g() } }";
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("A.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {path.toString()};
+    int exitCode = main.format(args);
+
+    assertThat(exitCode).isEqualTo(1);
+    assertThat(err.toString()).contains("A.java:2:6: error: ';' expected");
+  }
+
+  @Test
+  public void parseErrorStdin() throws FormatterException, IOException, UsageException {
+    String input = "class Foo { void f() {\n g() } }";
+
+    InputStream inStream = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8));
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), inStream);
+    String[] args = {"-"};
+    int exitCode = main.format(args);
+
+    assertThat(exitCode).isEqualTo(1);
+    assertThat(err.toString()).contains("<stdin>:2:6: error: ';' expected");
+  }
+
+  @Test
+  public void lexError2() throws FormatterException, IOException, UsageException {
+    Str