Repository: smithy-lang/smithy
Pull request #2142 (2024-02-15 20:00:12)

Fix issues with AbstractCodeWriter state stacks
Code writer context inheritance wasn't working in the previous implementation of the stack based inheritance system. We were able to resolve context from the current state and parent state (because of eagerly copying states and flattening them per/state), but we weren't able to look at parent states that used a CodeSection. Methods of a CodeSection state can be accessed in named template labels. However, when another state is pushed after setting a CodeSection value for a state, code writer wasn't crawling the state stack to see if previous CodeSections could provide a value for a named label that wasn't explicitly set as parameter in the context map. This was because instead of looking up values lazily by iterating over the state stack, we were making eager copies of different pieces of states. This matters when using things like for loops in templates that push an implicit state to capture the current loop iteration state.

This commit changes our approach to now actually use the stack itself and iterate over states when attempting to resolve context keys, formatters, iterators, and CodeSection values.

Some tests were added to ensure the behavior of the writer is the same. For example, when using inheritance based lookups, removing a context key now means we need to set a context key to an explicit null value to emulate the previous behavior. If we didn't do this, and we just called Map#remove(), the context key may have been set by a parent state, making the remove call do nothing (whereas the previous implementation would actually take effect).


#### Background
* What do these changes do? 
* Why are they important?

#### Testing
* How did you test these changes?

#### Links
* Links to additional context, if necessary
* Issue #, if applicable (see [here](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) for a list of keywords to use for linking issues)

---
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79eb4ba8e..59ab6f48c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,25 @@
 # Smithy Changelog
 
+## 1.45.0 (2024-02-14)
+
+### Features
+
+* Added new option to CLI to configure the format (`text` or `csv`) of validation output. ([#2133](https://github.com/smithy-lang/smithy/pull/2133))
+* Added options to CLI to hide or show validation events for specified validators. ([#2127](https://github.com/smithy-lang/smithy/pull/2127))
+* Added protocol tests for verifying serialization/deserialization behavior for maps with document values. ([#2125](https://github.com/smithy-lang/smithy/pull/2125))
+* Changed `UnreferencedShape` to be an opt-in linter instead of on-by-default to reduce friction when defining common shapes that are not connected to a service shape. Added a validator that allows you to configure what shape to check connectedness (defaults to service shape). ([#2119](https://github.com/smithy-lang/smithy/pull/2119))
+
+### Bug Fixes
+
+* Fixed headers when printing validation event output in csv format. ([#2136](https://github.com/smithy-lang/smithy/pull/2136))
+* Fixed `RestJsonZeroAndFalseQueryValues` protocol test to correctly include a params value for servers. ([#2132](https://github.com/smithy-lang/smithy/pull/2132))
+* Fixed regression in model validation that incorrectly allowed suppression of ERROR events. ([#2130](https://github.com/smithy-lang/smithy/pull/2130))
+
+### Documentation
+
+* Fixed typo in @pattern validation example. ([#2126](https://github.com/smithy-lang/smithy/pull/2126))
+
+
 ## 1.44.0 (2024-01-25)
 
 ### Features
diff --git a/VERSION b/VERSION
index 372cf402c..50aceaa7b 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-1.44.0
+1.45.0
diff --git a/docs/source-2.0/guides/gradle-plugin.rst b/docs/source-2.0/guides/gradle-plugin.rst
deleted file mode 100644
index 6a8fa7fa6..000000000
--- a/docs/source-2.0/guides/gradle-plugin.rst
+++ /dev/null
@@ -1,345 +0,0 @@
-.. _smithy-gradle-plugin:
-
-====================
-Smithy Gradle Plugin
-====================
-
-The `Smithy Gradle plugin`_ integrates Smithy with `Gradle`_. This plugin can
-build artifacts from Smithy models, generate JARs that contain Smithy models
-found in Java projects, and generate JARs that contain filtered :ref:`projections <projections>`
-of Smithy models.
-
-
-.. _plugin-install:
-
-Installation
-============
-
-The Smithy Gradle plugin is applied using the ``software.amazon.smithy`` plugin.
-The following example configures a project to use the Smithy Gradle plugin:
-
-.. code-block:: kotlin
-
-    plugins {
-        id("software.amazon.smithy").version("0.6.0")
-    }
-
-
-.. _smithy-model-sources:
-
-Smithy model sources
-====================
-
-When a JAR is generated for a project, any Smithy models found in the
-following directories will be added to the JAR:
-
-* ``model/``
-* ``src/main/smithy``
-* ``src/main/resources/META-INF/smithy``
-
-Models found in these directories are combined into a flattened directory
-structure and used to validate and build the Smithy model. A Smithy manifest
-file is automatically created for the detected models, and it along with the
-model files, are placed in the ``META-INF/smithy/`` resource of the created
-JAR. Any project that then depends on this created JAR will be able to find
-and use the Smithy models contained in the JAR when using *model discovery*.
-
-
-.. _building-smithy-models:
-
-Building Smithy models
-======================
-
-This plugin operates in two different modes:
-
-1. If no ``projection`` is specified for the ``SmithyExtension``, then the plugin
-   runs a "source" build using the "source" projection.
-2. If a ``projection`` is specified for the ``SmithyExtension``, then the plugin
-   runs in a "projection" mode.
-
-
-.. _smithy-extension-properties:
-
-Smithy extension properties
-===========================
-
-This plugin is configured using the ``software.amazon.smithy.gradle.SmithyExtension``
-extension:
-
-.. code-block:: kotlin
-
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-        projection = "foo"
-    }
-
-
-This extension supports the following properties:
-
-.. list-table::
-    :header-rows: 1
-    :widths: 10 20 70
-
-    * - Property
-      - Type
-      - Description
-    * - projection
-      - ``String``
-      - Sets the :ref:`projection <projections>` name to build. There must
-        be a corresponding projection definition in the :ref:`smithy-build.json <smithy-build-json>`
-        file in the project.
-    * - projectionSourceTags
-      - ``Set<String>``
-      - Get the tags that are searched for in classpaths when determining
-        which models are projected into the created JAR. This plugin will look
-        through the JARs in the buildscript classpath to see if they contain a
-        META-INF/MANIFEST.MF attribute named "Smithy-Tags" that matches any of
-        the given projection source tags. The Smithy models found in each
-        matching JAR are copied into the JAR being projected. This allows a
-        projection JAR to aggregate models into a single JAR.
-    * - tags
-      - ``Set<String>``
-      - Set the tags that are added to the JAR. These tags are placed in the
-        META-INF/MANIFEST.MF attribute named "Smithy-Tags" as a comma
-        separated list. JARs with Smithy-Tags can be queried when building
-        projections so that the Smithy models found in each matching JAR are
-        placed into the projection JAR.
-    * - smithyBuildConfigs
-      - ``FileCollection``
-      - Sets a custom collection of smithy-build.json files to use when
-        building the model.
-    * - allowUnknownTraits
-      - ``boolean``
-      - Sets whether or not unknown traits in the model should be ignored. By
-        default, the build will fail if unknown traits are encountered.
-    * - outputDirectory
-      - ``File``
-      - Defines where Smithy build artifacts are written.
-
-
-.. _building-source-model:
-
-Building a source model
------------------------
-
-A "source" build is run when no ``projection`` is configured in
-``SmithyExtension``. Because no projection was specified, **smithy-build** is
-executed using the ``compileClasspath`` plus the ``buildscript`` classpath. To
-prevent accidentally relying on Smithy models that are only available to
-build scripts, Smithy models are discovered using only the
-``compileClasspath`` and ``runtimeClasspath``.
-
-The following example ``build.gradle.kts`` will build a Smithy model using a
-"source" build:
-
-.. code-block:: kotlin
-
-    plugins {
-        id("software.amazon.smithy").version("0.6.0")
-    }
-
-    // The SmithyExtension is used to customize the build. This example
-    // doesn't set any values and can be completely omitted.
-    configure<software.amazon.smithy.gradle.SmithyExtension> {}
-
-    repositories {
-        mavenLocal()
-        mavenCentral()
-    }
-
-    dependencies {
-        implementation("software.amazon.smithy:smithy-model:__smithy_version__")
-
-        // These are just examples of dependencies. This model has a dependency on
-        // a "common" model package and uses the external AWS traits.
-        implementation("com.foo.baz:foo-model-internal-common:1.0.0")
-        implementation("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
-    }
-
-
-.. _generating-projection:
-
-Generating a projection
------------------------
-
-A "projection" build is run when a :ref:`projection <projections>` is
-specified in the ``SmithyExtension``. You might want to create a projection of
-a model if you need to maintain an internal version of a model that contains
-more information and features than an external version of a model published to
-your customers.
-
-A "projection" build is executed using only the ``buildscript`` classpath, and
-Smithy models are discovered using only the ``buildscript`` classpath. This
-prevents models discovered in the original model from appearing in the
-projected version of the model.
-
-The following example ``build.gradle.kts`` file will run a "projection"
-build that uses the "external" projection.
-
-.. code-block:: kotlin
-
-    plugins {
-        id("software.amazon.smithy").version("0.6.0")
-    }
-
-    buildscript {
-        repositories {
-            mavenLocal()
-            mavenCentral()
-        }
-        dependencies {
-            classpath("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
-
-            // Take a dependency on the internal model package. This
-            // dependency *must* be a buildscript only dependency to ensure
-            // that is does not appear in the generated JAR.
-            classpath("com.foo.baz:foo-internal-model:1.0.0")
-        }
-    }
-
-    // Use the "external" projection. This projection must be found in the
-    // smithy-build.json file. This also ensures that models found in the
-    // foo-internal-package that weren't filtered out are copied into the
-    // projection created by this package.
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-        projection = "external"
-        projectionSourceTags = setOf("com.foo.baz:foo-internal-model")
-    }
-
-    repositories {
-        mavenLocal()
-        mavenCentral()
-    }
-
-    dependencies {
-        implementation("software.amazon.smithy:smithy-model:__smithy_version__")
-
-        // Any dependencies that the projected model needs must be (re)declared
-        // here. For example, let's assume that the smithy-aws-traits package is
-        // needed in the projected model too.
-        implementation("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
-    }
-
-
-Because the ``projection`` of the ``SmithyExtension`` was set to ``external``, a
-``smithy-build.json`` file **must** be found that defines the ``external``
-projection. For example:
-
-.. code-block:: json
-
-    {
-        "version": "1.0",
-        "projections": {
-            "external": {
-                "transforms": [
-                    {
-                        "name": "excludeShapesByTag",
-                        "args": {
-                            "tags": ["internal"]
-                        }
-                    },
-                    {
-                        "name": "excludeShapesByTrait",
-                        "args": {
-                            "traits": ["internal"]
-                        }
-                    },
-                    {
-                        "name": "excludeMetadata",
-                        "args": {
-                            "keys": ["suppressions", "validators"]
-                        }
-                    },
-                    {
-                        "name": "removeUnusedShapes"
-                    }
-                ]
-            }
-        }
-    }
-
-
-.. _projection-tags:
-
-Projection tags
----------------
-
-Projections are meant to *project* and filter other models into another
-model. You need to specify which dependencies are being projected into your
-JAR by setting the ``projectionSourceTags`` property.
-
-.. code-block:: kotlin
-
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-            projection = "external"
-            projectionSourceTags = setOf("Foo", "com.baz:bar")
-    }
-
-
-Tags are used to logically group packages to make it easier to build
-projections. The ``tags`` property is used to add ``Smithy-Tags`` to a JAR.
-
-.. code-block:: kotlin
-
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-        tags = setOf("X", "foobaz-model")
-    }
-
-
-For example, if your service is made up of 10 packages, you can add the
-"foobaz-model" Smithy tag to each package so that the only value that needs
-to be provided for ``tags`` to correctly project your model is "foobaz-model".
-
-When building a model package, this plugin will automatically add the group
-name of the package being built, the group name + ":" + name of the package,
-and group name + ":" + name + ":" version. This allows models to always
-be queried by group and artifact names in addition to custom tags.
-
-
-.. _artifacts-from-smithy-models:
-
-Building artifacts from Smithy models
--------------------------------------
-
-If a ``smithy-build.json`` file is found at the root of the project, then it
-will be used to generate :ref:`artifacts <projection-artifacts>` from the Smithy model.
-
-The following example generates an OpenAPI model from a Smithy model:
-
-.. code-block:: json
-
-    {
-        "version": "1.0",
-        "plugins": {
-            "openapi": {
-                "service": "foo.baz#MyService"
-            }
-        }
-    }
-
-
-The above Smithy plugin also requires a ``buildscript`` dependency in
-``build.gradle.kts``:
-
-.. code-block:: kotlin
-
-    buildscript {
-        // ...
-        dependencies {
-            // ...
-
-            // This dependency is required in order to apply the "openapi"
-            // plugin in smithy-build.json
-            classpath("software.amazon.smithy:smithy-openapi:__smithy_version__")
-        }
-    }
-
-Complete Examples
-=================
-
-For several complete examples, see the `examples directory`_ of the Smithy
-Gradle plugin repository, or check out the :doc:`Quick start guide </quickstart>` for a tutorial on
-creating a Smithy model and building it with the Smithy Gradle plugin.
-
-.. _examples directory: https://github.com/awslabs/smithy-gradle-plugin/tree/main/examples
-.. _Smithy Gradle plugin: https://github.com/awslabs/smithy-gradle-plugin/
-.. _Gradle: https://gradle.org/
diff --git a/docs/source-2.0/guides/gradle-plugin/gradle-migration-guide.rst b/docs/source-2.0/guides/gradle-plugin/gradle-migration-guide.rst
new file mode 100644
index 000000000..293d7c6f4
--- /dev/null
+++ b/docs/source-2.0/guides/gradle-plugin/gradle-migration-guide.rst
@@ -0,0 +1,186 @@
+.. _gradle_migration_guide:
+
+==========================================
+Migrating to Gradle plugin version 0.10.0+
+==========================================
+
+This guide describes how to migrate your Gradle build files
+to use the `Smithy Gradle plugins`_ v0.10.0+ without breaking
+your existing Gradle projects.
+
+.. warning::
+    Versions 0.10.0+ of the Smithy Gradle plugins will automatically apply
+    the Smithy formatter to your Smithy source files. This can result in a
+    large number of formatting changes the first time you apply these plugins
+    to your project. For instructions on how to disable automatic formatting
+    see :ref:`Disabling the Smithy formatter <disable-smithy-formatter>`.
+
+Update plugin artifact
+======================
+
+The Smithy Gradle plugin has been broken up into separate capability
+(``smithy-jar``) and convention (``smithy-base``) plugins. The
+``smithy-jar`` plugin operates similarly to the previous ``smithy`` plugin,
+but no longer applies the ``java`` plugin automatically. To migrate to the
+new plugin, change the artifact id in the ``plugins`` block and apply the
+``java`` (or ``java-library``) plugin if it is not already applied to
+your project.
+
+.. tab:: Kotlin
+
+    .. code-block:: diff
+        :caption: build.gradle.kts
+
+        plugins {
+        -     id("software.amazon.smithy").version("0.7.0")
+        +     `java`
+        +     id("software.amazon.smithy.gradle.smithy-jar").version("0.10.0")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: diff
+        :caption: build.gradle
+
+        plugins {
+        -      id 'software.amazon.smithy' version '0.7.0'
+        +      id 'java'
+        +      id 'software.amazon.smithy.gradle.smithy-jar' version '0.10.0'
+        }
+
+Remove Buildscript Dependencies
+===============================
+
+Pre-0.10.0 versions of the Smithy Gradle plugins use the buildscript block
+for build-only dependencies such as Smithy build plugins. The 0.10.0+
+Gradle plugins create a new Gradle `Configuration`_ ``smithyBuild``
+for these build-only dependencies.
+
+.. tab:: Kotlin
+
+    .. code-block:: diff
+        :caption: build.gradle.kts
+
+        -buildscript {
+        -       repositories {
+        -           mavenLocal()
+        -           mavenCentral()
+        -       }
+        -
+        -       dependencies {
+        -           // Dependency repeated here and below because model discovery previously
+        -           // only ran on buildscript classpath for projections
+        -           classpath("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
+        -           // Take a dependency on the internal model package. This
+        -           // dependency *must* be a buildscript only dependency to ensure
+        -           // that is does not appear in the generated JAR.
+        -           classpath("com.foo.baz:foo-internal-model:1.0.0")
+        -       }
+        -}
+
+        dependencies {
+            implementation("software.amazon.smithy:smithy-model:__smithy_version__")
+
+            implementation("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
+        +   smithyBuild("com.foo.baz:foo-internal-model:1.0.0")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: diff
+        :caption: build.gradle
+
+        -buildscript {
+        -       repositories {
+        -           mavenLocal()
+        -           mavenCentral()
+        -       }
+        -
+        -       dependencies {
+        -           // Dependency repeated here and below because model discovery previously
+        -           // only ran on buildscript classpath for projections
+        -           classpath 'software.amazon.smithy:smithy-aws-traits:__smithy_version__'
+        -           // Take a dependency on the internal model package. This
+        -           // dependency *must* be a buildscript only dependency to ensure
+        -           // that is does not appear in the generated JAR.
+        -           classpath 'com.foo.baz:foo-internal-model:1.0.0'
+        -       }
+        -}
+
+        dependencies {
+            implementation 'software.amazon.smithy:smithy-model:__smithy_version__'
+
+            implementation 'software.amazon.smithy:smithy-aws-traits:__smithy_version__'
+        +   smithyBuild 'com.foo.baz:foo-internal-model:1.0.0'
+        }
+
+Change ``projection`` property name
+===================================
+
+The property ``projection`` has also been updated to ``sourceProjection``.
+
+.. tab:: Kotlin
+
+        .. code-block:: diff
+            :caption: build.gradle.kts
+
+            -configure<software.amazon.smithy.gradle.SmithyExtension> {
+            +smithy {
+            -    projection = "foo"
+            +    sourceProjection.set("foo")
+            }
+
+.. tab:: Groovy
+
+        .. code-block:: diff
+            :caption: build.gradle
+
+            -configure<software.amazon.smithy.gradle.SmithyExtension> {
+            +smithy {
+            -    projection = "foo"
+            +    sourceProjection = "foo"
+            }
+
+Change ``smithyBuildJar`` task name
+===================================
+
+The ``smithyBuildJar`` task has been removed. Instead, the plugin now
+executes separate ``smithyBuild`` and ``smithyJarStaging`` tasks. If
+your project previously configured the ``smithyBuildJar``,
+configure the ``smithyBuild`` task instead. Tasks that depended on
+``smithyBuildJar`` should now depend on the ``jar`` task.
+
+.. tab:: Kotlin
+
+        .. code-block:: diff
+            :caption: build.gradle.kts
+
+            tasks {
+            -   smithyBuildJar {
+            +   smithyBuild {
+                    smithyBuildConfigs.set(files("smithy-build.json", other))
+                }
+                // ..
+            }
+
+            -tasks["smithyBuildJar"].dependsOn("otherTask")
+            +tasks["jar"].dependsOn("otherTask")
+
+.. tab:: Groovy
+
+        .. code-block:: diff
+            :caption: build.gradle
+
+            tasks {
+            -   smithyBuildJar {
+            +   smithyBuild {
+                    smithyBuildConfigs = files("smithy-build.json", other)
+                }
+                // ..
+            }
+
+            -tasks["smithyBuildJar"].dependsOn("otherTask")
+            +tasks["jar"].dependsOn("otherTask")
+
+.. _Smithy Gradle plugins: https://github.com/awslabs/smithy-gradle-plugin/
+.. _Configuration: https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.Configuration.html
diff --git a/docs/source-2.0/guides/gradle-plugin/index.rst b/docs/source-2.0/guides/gradle-plugin/index.rst
new file mode 100644
index 000000000..4c6db0a8a
--- /dev/null
+++ b/docs/source-2.0/guides/gradle-plugin/index.rst
@@ -0,0 +1,760 @@
+.. _smithy-gradle-plugin:
+
+=====================
+Smithy Gradle Plugins
+=====================
+
+The `Smithy Gradle plugins`_ integrate Smithy with the `Gradle`_ build system. These plugin can
+build artifacts from Smithy models, and generate JARs for Smithy models and model
+:ref:`projections <projections>`.
+
+.. toctree::
+    :maxdepth: 1
+    :caption: Migrate to version 0.8.0+
+
+    gradle-migration-guide
+
+Plugins
+=======
+
+Two official `Gradle`_ plugins are available:
+
+* `smithy-base`_:
+    This plugin configures the `SourceSet`_'s and `Configuration`_'s
+    for a Smithy project. It also creates the base ``smithyBuild`` task for the project that
+    builds the Smithy models and build artifacts for the project.
+* `smithy-jar`_:
+    Adds Smithy files to an existing JAR created by a ``jar`` task (usually created
+    by the `Java plugin`_ or `Kotlin JVM plugin`_). The ``smithy-jar`` plugin also adds build metadata
+    and tags to the JAR's MANIFEST. The ``smithy-jar`` plugin applies the ``smithy-base`` plugin when
+    applied.
+
+.. admonition:: Which plugin should I use?
+    :class: note
+
+    Most users should use the ``smithy-jar`` plugin. If you are building shared model packages,
+    custom traits, custom linters, or codegen integrations you should use the ``smithy-jar`` plugin.
+    If you are writing a code generator you can use the ``smithy-base`` plugin to set up basic
+    `Configuration`_'s and provide access to Smithy gradle tasks.
+
+.. _plugin-apply:
+
+Applying Plugins
+----------------
+
+The Smithy Gradle plugins are applied using the ``plugins`` block of a gradle build script.
+The following example configures a project to use the ``smithy-base`` Gradle plugin:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        plugins {
+            id("software.amazon.smithy.gradle.smithy-base").version("0.10.0")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        plugins {
+            id 'software.amazon.smithy.gradle.smithy-base' version '0.10.0'
+        }
+
+
+.. _smithy-model-sources:
+
+.. note::
+
+    The ``smithy-jar`` plugin requires that a `jar` task be registered
+    before it is applied. The `Java Plugin`_ and `Kotlin JVM plugin`_
+    are commonly used plugins that both register a `jar` task.
+
+Complete Examples
+=================
+
+For several complete examples, see the `examples directory`_ of the `Smithy
+Gradle plugins`_ repository, or check out the :doc:`Quick start guide </quickstart>` for a tutorial on
+creating a Smithy model and building it with the Smithy Gradle plugin.
+
+The examples in from the `examples directory`_  can be copied into your workspace using
+the :ref:`Smithy CLI <smithy-cli>` ``init`` command as follows:
+
+.. code-block:: text
+
+    smithy init -t <EXAMPLE_NAME> -o <OUTPUT_DIRECTORY> --url https://github.com/smithy-lang/smithy-gradle-plugin
+
+You can list all examples available in the `Smithy Gradle plugins`_ repository with the following command:
+
+.. code-block:: text
+
+    smithy init --list --url https://github.com/smithy-lang/smithy-gradle-plugin
+
+.. _building-smithy-models:
+
+Building Smithy models
+======================
+
+The ``smithyBuild`` task that builds smithy models operates in two different modes:
+
+1. If no ``projection`` is specified for the ``SmithyExtension``, then the task
+   runs a "source" build using the "source" projection.
+2. If a ``projection`` is specified for the ``SmithyExtension``, then the task
+   runs in "projection" mode.
+
+.. _building-source-model:
+
+Building a source model
+-----------------------
+
+A "source" build is run when no ``sourceProjection`` is configured in
+``SmithyExtension``. To prevent accidentally relying on Smithy models
+that are only available as build dependencies, Smithy models are discovered
+using only the :ref:`model sources <model-sources>` and ``runtimeClasspath``.
+
+The following example ``build.gradle.kts`` will build a Smithy model using a
+"source" build:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        plugins {
+            `java-library`
+            id("software.amazon.smithy.gradle.smithy-jar").version("0.10.0")
+        }
+
+        // The SmithyExtension is used to customize the build. This example
+        // doesn't set any values and can be completely omitted.
+        smithy {}
+
+        repositories {
+            mavenLocal()
+            mavenCentral()
+        }
+
+        dependencies {
+            implementation("software.amazon.smithy:smithy-model:__smithy_version__")
+
+            // These are just examples of dependencies. This model has a dependency on
+            // a "common" model package and uses the external AWS traits.
+            implementation("com.foo.baz:foo-model-internal-common:1.0.0")
+            implementation("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        plugins {
+            id 'java-library'
+            'software.amazon.smithy.gradle.smithy-jar' version '0.10.0'
+        }
+
+        // The SmithyExtension is used to customize the build. This example
+        // doesn't set any values and can be completely omitted.
+        smithy {}
+
+        repositories {
+            mavenLocal()
+            mavenCentral()
+        }
+
+        dependencies {
+            implementation 'software.amazon.smithy:smithy-model:__smithy_version__'
+
+            // These are just examples of dependencies. This model has a dependency on
+            // a "common" model package and uses the external AWS traits.
+            implementation 'com.foo.baz:foo-model-internal-common:1.0.0'
+            implementation 'software.amazon.smithy:smithy-aws-traits:__smithy_version__'
+        }
+
+.. _generating-projection:
+
+Generating a projection
+-----------------------
+
+A "projection" build is run when a :ref:`projection <projections>` is
+specified in the ``SmithyExtension``. You might create a projection of
+a model if you need to maintain an internal version of a model that contains
+more information and features than an external version of a model published to
+your customers.
+
+Any projected models should be added to the ``smithyBuild`` configuration. This
+prevents packages with projected models from appearing as dependencies of the
+projected version of the model.
+
+The following example gradle build script will run a "projection"
+build using the "external" projection.
+
+.. tab:: Kotlin
+
+        .. code-block:: kotlin
+            :caption: build.gradle.kts
+
+            plugins {
+                `java-library`
+                id("software.amazon.smithy.gradle.smithy-jar").version("0.10.0")
+            }
+
+            repositories {
+                mavenLocal()
+                mavenCentral()
+            }
+
+            dependencies {
+                implementation("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
+
+                // Take a dependency on the internal model package. This
+                // dependency *must* be a smithyBuild dependency to ensure
+                // that is does not appear in the generated JAR.
+                smithyBuild("com.foo.baz:foo-internal-model:1.0.0")
+            }
+
+            smithy {
+                // Use the "external" projection. This projection must be found in the
+                // smithy-build.json file. This also ensures that models found in the
+                // foo-internal-package that weren't filtered out are copied into the
+                // projection created by this package.
+                sourceProjection.set("external")
+                projectionSourceTags.addAll("com.foo.baz:foo-internal-model")
+            }
+
+.. tab:: Groovy
+
+        .. code-block:: groovy
+            :caption: build.gradle
+
+            plugins {
+                id 'java-library'
+                id 'software.amazon.smithy.gradle.smithy-jar' version '0.10.0'
+            }
+
+            repositories {
+                mavenLocal()
+                mavenCentral()
+            }
+
+            dependencies {
+                implementation 'software.amazon.smithy:smithy-aws-traits:__smithy_version__'
+
+                // Take a dependency on the internal model package. This
+                // dependency *must* be a smithyBuild dependency to ensure
+                // that is does not appear in the generated JAR.
+                smithyBuild 'com.foo.baz:foo-internal-model:1.0.0'
+            }
+
+            smithy {
+                // Use the "external" projection. This projection must be found in the
+                // smithy-build.json file. This also ensures that models found in the
+                // foo-internal-package that weren't filtered out are copied into the
+                // projection created by this package.
+                sourceProjection = "external"
+                projectionSourceTags += ["com.foo.baz:foo-internal-model"]
+            }
+
+Because the ``sourceProjection`` of the ``SmithyExtension`` was set to ``external``, a
+``smithy-build.json`` file **must** be found that defines the ``external``
+projection. For example:
+
+.. code-block:: json
+    :caption: smithy-build.json
+
+    {
+        "version": "1.0",
+        "projections": {
+            "external": {
+                "transforms": [
+                    {
+                        "name": "excludeShapesByTag",
+                        "args": {
+                            "tags": ["internal"]
+                        }
+                    },
+                    {
+                        "name": "excludeShapesByTrait",
+                        "args": {
+                            "traits": ["internal"]
+                        }
+                    },
+                    {
+                        "name": "excludeMetadata",
+                        "args": {
+                            "keys": ["suppressions", "validators"]
+                        }
+                    },
+                    {
+                        "name": "removeUnusedShapes"
+                    }
+                ]
+            }
+        }
+    }
+
+.. _projection-tags:
+
+Projection tags
+^^^^^^^^^^^^^^^
+
+Projections can *project* and filter shapes from dependencies into the output model.
+"Projecting" a dependency into your output JAR will include that model in the output
+sources of your project. Downstream consumers of your JAR will then no longer need to
+include the "projected" dependency as a dependency of their project to resolve the
+"projected" shapes. You need to specify which dependencies are being projected into
+your JAR by setting the ``projectionSourceTags`` property.
+
+.. tab:: Kotlin
+
+        .. code-block:: kotlin
+            :caption: build.gradle.kts
+
+            smithy {
+                sourceProjection.set("external")
+                projectionSourceTags.addAll("Foo", "com.baz:bar")
+            }
+
+.. tab:: Groovy
+
+        .. code-block:: groovy
+            :caption: build.gradle
+
+            smithy {
+                projection = "external"
+                projectionSourceTags += setOf("Foo", "com.baz:bar")
+            }
+
+
+Tags are used to logically group packages to make it easier to build
+projections. The ``tags`` property is used to add ``Smithy-Tags`` attribute
+to the JAR MANIFEST.
+
+.. tab:: Kotlin
+
+        .. code-block:: kotlin
+            :caption: build.gradle.kts
+
+            smithy {
+                tags.addAll("X", "foobaz-model")
+            }
+
+.. tab:: Groovy
+
+        .. code-block:: groovy
+            :caption: build.gradle
+
+            smithy {
+                tags += ["X", "foobaz-model"]
+            }
+
+
+For example, if your service is made up of 10 packages, you can add the
+"foobaz-model" Smithy tag to each package so that the only value that needs
+to be provided for ``tags`` to correctly project your model is "foobaz-model".
+
+When building a model package, the Smithy Gradle plugin will automatically add
+the group name of the package being built, the group name + ":" + name of the package,
+and group name + ":" + name + ":" version. This allows models to always
+be queried by group and artifact names in addition to custom tags.
+
+.. _artifacts-from-smithy-models:
+
+Building artifacts from Smithy models
+-------------------------------------
+
+If a :ref:`smithy-build.json <smithy-build>` file is found at the root of the project,
+then it will be used to generate :ref:`artifacts <projection-artifacts>` from the Smithy
+model. Smithy uses :ref:`build plugins <plugins>` to generate projection artifacts. To
+use a smithy build plugin with Gradle, first add it as a ``smithyBuild`` dependency in
+the Gradle build script. The plugin will then be discoverable and can be configured
+in the ``smithy-build.json`` file.
+
+For example, to generate a Typescript client from a Smithy model, add the
+``smithy-typescript-codegen`` package as a ``smithyBuild`` dependency:
+
+.. tab:: Kotlin
+
+        .. code-block:: kotlin
+            :caption: build.gradle.kts
+
+            dependencies {
+                // ...
+
+                // This dependency is required in order to apply the "typescript-client-codegen"
+                // plugin in smithy-build.json
+                smithyBuild("software.amazon.smithy.typescript:smithy-typescript-codegen:0.19.0")
+            }
+
+.. tab:: Groovy
+
+        .. code-block:: groovy
+            :caption: build.gradle
+
+            dependencies {
+                // ...
+
+                // This dependency is required in order to apply the "typescript-client-codegen"
+                // plugin in smithy-build.json
+                smithyBuild 'software.amazon.smithy.typescript:smithy-typescript-codegen:0.19.0'
+            }
+
+The plugin can then be configured in the ``smithy-build.json`` to generate a typescript client
+from a Smithy model:
+
+.. code-block:: json
+    :caption: smithy-build.json
+
+    {
+        "version": "1.0",
+        "plugins": {
+            "typescript-client-codegen": {
+                "package": "@smithy/typescript-client-codegen-example",
+                "packageVersion": "0.0.1",
+                "packageJson": {
+                    "license": "MIT"
+                }
+            }
+        }
+    }
+
+.. _gradle-plugin-configuration:
+
+Configuration
+=============
+
+.. _model-sources:
+
+Smithy model sources
+--------------------
+
+Smithy gradle plugins assume Smithy model files (``*.smithy``) are organized in a similar way as Java source files,
+in sourceSets. The smithy-base plugin adds a new sources block named smithy to every sourceSet.
+By default, this source block will include Smithy models in:
+
+* ``model/``
+* ``src/$sourceSetName/smithy``
+* ``src/$sourceSetName/resources/META-INF/smithy``
+
+New source directories can be added to a smithy sources block as follows:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        sourceSets {
+            main {
+                smithy {
+                    srcDir("includes")
+                }
+            }
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        sourceSets {
+            main {
+                smithy {
+                    srcDir 'includes'
+                }
+            }
+        }
+
+Models found in these directories are combined into a flattened directory
+structure and used to validate and build the Smithy model. A Smithy manifest
+file is automatically created for the detected models, and it, along with the
+model files, are placed in the ``META-INF/smithy/`` resource of the created
+JAR. Any project that depends on this created JAR will be able to find
+and use the Smithy models contained in the JAR when using *model discovery*.
+
+Dependencies
+------------
+
+The Smithy build plugins use two different configurations to search for dependencies
+such as shared models or :ref:`smithy build plugins <plugins>`:
+
+* ``runtimeClasspath``:
+    Runtime dependencies that will be required by any output JARS or publications.
+    For example, a shared model package or Java trait definition.
+* ``smithyBuild``:
+    Build dependencies that are not required by output models.
+
+Runtime dependencies can be added directly to the ``runtimeClasspath`` configuration
+or to a configuration that extends ``runtimeClasspath``, such as the ``implementation``
+configuration added by the ``java-library`` plugin.
+
+.. _dependencies:
+
+Build Dependencies
+^^^^^^^^^^^^^^^^^^
+
+The ``smithy-base`` plugin adds a ``smithyBuild`` `Configuration`_ that can be used to specify
+dependencies used when calling ``smithy build``. These dependencies will not be included in any output JARs or publications. They are akin to `compileOnly`
+dependencies for Java projects. Smithy build plugins and projected dependencies should be
+included in the `smithyBuild` configuration. For example:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        dependencies {
+            smithyBuild("com.example.software:build-only:1.0.0")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        dependencies {
+            smithyBuild 'com.example.software:build-only:1.0.0'
+        }
+
+.. _smithy-extension-properties:
+
+Smithy extension properties
+---------------------------
+
+The Smithy Gradle plugins are configured using the `SmithyExtension`_ extension:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        smithy {
+            projection.set("foo")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            projection = "foo"
+        }
+
+
+This extension supports the following properties:
+
+.. list-table::
+    :header-rows: 1
+    :widths: 10 20 70
+
+    * - Property
+      - Type
+      - Description
+    * - format
+      - ``boolean``
+      - Flag indicating whether to format Smithy source files. By default, the :ref:`Smithy CLI <smithy-cli>` ``format``
+        command is executed on all source directories. This opinionated formatter follows the best practices
+        recommended by the Smithy team. It is possible to disable the formatter by setting this flag to ``false``.
+    * - smithyBuildConfigs
+      - ``FileCollection``
+      - Sets a custom collection of smithy-build.json files to use when building the model.
+    * - sourceProjection
+      - ``String``
+      - Sets the :ref:`projection <projections>` name to use as the source (primary) projection.
+        The smithy sources for this projection will be packaged in output JARs by the ``smithy-jar``
+        plugin. There must be a corresponding projection definition in the :ref:`smithy-build.json <smithy-build-json>`
+        file in the project. Defaults to ``"source"``.
+    * - projectionSourceTags
+      - ``Set<String>``
+      - Get the tags that are searched for in classpaths when determining
+        which models are projected into the created JAR. This plugin will look
+        through the JARs in the buildscript classpath to see if they contain a
+        META-INF/MANIFEST.MF attribute named "Smithy-Tags" that matches any of
+        the given projection source tags. The Smithy models found in each
+        matching JAR are copied into the JAR being projected. This allows a
+        projection JAR to aggregate models into a single JAR.
+    * - tags
+      - ``Set<String>``
+      - Set the tags that are added to the JAR. These tags are placed in the
+        META-INF/MANIFEST.MF attribute named "Smithy-Tags" as a comma
+        separated list. JARs with Smithy-Tags can be queried when building
+        projections so that the Smithy models found in each matching JAR are
+        placed into the projection JAR.
+    * - allowUnknownTraits
+      - ``boolean``
+      - Sets whether or not unknown traits in the model should be ignored. By
+        default, the build will fail if unknown traits are encountered.
+    * - fork
+      - ``boolean``
+      - By default, the CLI is run in the same process as Gradle,
+        but inside a thread with a custom class loader. This should
+        work in most cases, but there is an option to run inside a
+        process if necessary.
+    * - outputDirectory
+      - ``Directory``
+      - Defines where Smithy build artifacts are written.
+
+
+Customize output directory
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+By default, Smithy build artifacts will be placed in the project build
+directory in a ``smithyprojections/`` directory. There are two ways to override
+the output directory. The first method is to set the ``outputDirectory`` property
+in the ``smithy-build.json`` config for your Smithy project. For example:
+
+.. code-block:: json
+    :caption: smithy-build.json
+
+    {
+        "outputDirectory": "build/output"
+    }
+
+The second method is to configure the plugin using the ``smithy`` extension:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        smithy {
+            outputDirectory.set(file("path/to/output"))
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            outputDirectory = file("path/to/output")
+        }
+
+.. note::
+
+    Setting the output directory on the plugin extension will
+    override any ``outputDirectory`` property set in the
+    smithy-build config.
+
+Set ``smithy-build`` configs to use
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+By default, the plugin will look for a file called ``smithy-build.json``
+at the project's root and will use that as the :ref:`smithy-build config <smithy-build>`
+for your project. If no smithy-build.json file is found, then an empty build config
+is used to build the project.
+
+Alternatively, you can explicitly configure one or more smithy-build configs to use for
+your project as follows:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        smithy {
+            smithyBuildConfigs.set(files("smithy-build-config.json"))
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            smithyBuildConfigs = files("smithy-build-config.json")
+        }
+
+Set Smithy Tags to add to a JAR
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When the smithy-jar plugin is applied to a project, it can add a number of Smithy
+tags to the MANIFEST of a generated JAR. These tags can be used by downstream
+consumers to filter which models to include in projections. Tags can be configured
+for the plugin as follows:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        smithy {
+            tags.addAll("tag1", "anotherTag", "anotherTag2")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            tags += ["tag1", "anotherTag", "anotherTag2"]
+        }
+
+Fork a new process when executing Smithy CLI commands
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+By default, :ref:`Smithy CLI <smithy-cli>` commands are run in the
+same process as Gradle, but inside a thread with a custom class loader.
+This should work in most cases, but there is an option to run inside a
+process if necessary. To run Smithy CLI commands in a process set the
+``fork`` configuration option to true:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        smithy {
+            fork.set(true)
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            fork = true
+        }
+
+.. _disable-smithy-formatter:
+
+Disable Smithy Formatter
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+By default, the :ref:`Smithy CLI <smithy-cli>` ``format`` command is
+executed on all source directories. This opinionated formatter follows
+the best practices recommended by the Smithy team. It is possible to disable
+the formatter by setting the format setting on the plugin extension to false:
+
+.. tab:: Kotlin
+
+    .. code-block::
+        :caption: build.gradle.kts
+
+        smithy {
+            format.set(false)
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            format = false
+        }
+
+.. _examples directory: https://github.com/awslabs/smithy-gradle-plugin/tree/main/examples
+.. _Smithy Gradle plugins: https://github.com/awslabs/smithy-gradle-plugin/
+.. _Gradle: https://gradle.org/
+.. _smithy-base: https://github.com/smithy-lang/smithy-gradle-plugin#smithy-base-plugin
+.. _smithy-jar: https://github.com/smithy-lang/smithy-gradle-plugin#smithy-jar-plugin
+.. _Configuration: https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.Configuration.html
+.. _SourceSet: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.SourceSet.html
+.. _Java plugin: https://docs.gradle.org/current/userguide/java_plugin.html
+.. _Kotlin JVM plugin: https://plugins.gradle.org/plugin/org.jetbrains.kotlin.jvm
+.. _SmithyExtension: https://github.com/smithy-lang/smithy-gradle-plugin/blob/main/smithy-base/src/main/java/software/amazon/smithy/gradle/SmithyExtension.java
diff --git a/docs/source-2.0/guides/index.rst b/docs/source-2.0/guides/index.rst
index dee6887df..a45214311 100644
--- a/docs/source-2.0/guides/index.rst
+++ b/docs/source-2.0/guides/index.rst
@@ -7,7 +7,7 @@ Guides
 
     smithy-cli/index
     smithy-build-json
-    gradle-plugin
+    gradle-plugin/index
     using-code-generation/index
     model-linters
     model-validation-examples
diff --git a/docs/source-2.0/quickstart.rst b/docs/source-2.0/quickstart.rst
index c99297472..4f4643147 100644
--- a/docs/source-2.0/quickstart.rst
+++ b/docs/source-2.0/quickstart.rst
@@ -93,10 +93,31 @@ weather service.
 4. This service closure contains the following operations:
    ``ListCities``, ``GetCity``, ``GetForecast``, ``GetCurrentTime``.
 
-``Weather`` is a :ref:`service` shape that is defined inside of a
-:ref:`namespace <namespaces>`.
+First, create a directory called `smithy-quickstart` with a `model` directory
+and a weather model file such that your `smithy-quickstart` directory has the
+following file structure:
+
+.. code-block:: text
+
+    smithy-quickstart/
+    └── model/
+        └── weather.smithy
+
+.. tip::
+
+    Run the following command to create the quickstart directory
+
+    .. code-block:: text
+
+        mkdir -p smithy-quickstart/model \
+        && touch smithy-quickstart/model/weather.smithy \
+        && cd smithy-quickstart
+
+Next, we will start to model a ``Weather`` service in the `weather.smithy` file.
+``Weather`` is a :ref:`service` shape that is defined inside of a :ref:`namespace <namespaces>`.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     $version: "2"
     namespace example.weather
@@ -129,6 +150,7 @@ A resource is contained within a service or another resource. Resources have
 identifiers, operations, and any number of child resources.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     $version: "2"
     namespace example.weather
@@ -163,6 +185,7 @@ Each ``City`` has a single ``Forecast``. This can be defined by adding the
 ``Forecast`` to the ``resources`` property of the ``City``.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     resource City {
         identifiers: { cityId: CityId }
@@ -189,6 +212,7 @@ members of resource operations map to resource properties or identifiers to
 perform updates on or examine the state of a resource.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     resource City {
         identifiers: { cityId: CityId }
@@ -244,6 +268,7 @@ your API.
 Let's define the operation used to "read" a ``City``.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     @readonly
     operation GetCity {
@@ -291,6 +316,7 @@ Let's define the operation used to "read" a ``City``.
 And define the operation used to "read" a ``Forecast``.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     @readonly
     operation GetForecast {
@@ -332,6 +358,7 @@ bind the identifier of a ``City`` to its input structure; we are listing
 cities, so there's no way we could provide a ``City`` identifier.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     /// Provides weather forecasts.
     @paginated(inputToken: "nextToken", outputToken: "nextToken",
@@ -413,6 +440,7 @@ service.
 
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     /// Provides weather forecasts.
     @paginated(inputToken: "nextToken", outputToken: "nextToken",
@@ -442,51 +470,80 @@ service.
 Building the Model
 ==================
 
-Now that you have a model, you'll want to build it and generate things from it.
-Building the model creates projections of the model, applies plugins to
-generate artifacts, runs validation, and creates a JAR that contains the
-filtered projection. The `Smithy Gradle Plugin`_ is the best way to get started
-building a Smithy model. First, create a ``smithy-build.json`` file:
+Now that you have a model, you'll want to build it and generate additional
+artifacts from it. Building the model creates projections of the model, applies plugins to
+generate artifacts, and runs validation.
 
-.. code-block:: json
+.. tab:: Smithy CLI
 
-    {
-        "version": "1.0"
-    }
+    .. admonition:: Install required tools
+        :class: tip
 
-Then create a new ``build.gradle.kts`` file:
+        Before you proceed, make sure you have the :ref:`Smithy CLI installed <cli_installation>`.
 
-.. code-block:: kotlin
+    To build a Smithy model using the :ref:`the Smithy CLI <smithy-cli>`,
+    create a :ref:`smithy-build.json <smithy-build-json>` file in the ``smithy-quickstart`` directory:
 
-    plugins {
-        id("software.amazon.smithy").version("0.6.0")
-    }
+    .. code-block:: json
+        :caption: smithy-build.json
 
-    repositories {
-        mavenLocal()
-        mavenCentral()
-    }
+        {
+            // Version of the smithy-build.json file specification
+            "version": "1.0",
+            // Location to search for Smithy model source files
+            "sources": ["model"]
+        }
 
-    dependencies {
-        implementation("software.amazon.smithy:smithy-model:__smithy_version__")
-    }
+    Next, run ``smithy build``. That's it! We just created a simple, read-only, ``Weather`` service.
 
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-        // Uncomment this to use a custom projection when building the JAR.
-        // projection = "foo"
-    }
+.. tab:: Gradle
+
+    .. admonition:: Install required tools
+        :class: tip
+
+        Before you proceed, make sure you have `gradle installed`_.
+
+
+    To build a Smithy model using the :ref:`Smithy Gradle Plugin <smithy-gradle-plugin>`,
+    first, create a gradle build script file in the ``smithy-quickstart`` directory:
+
+    .. tab:: Kotlin
+
+        .. code-block:: kotlin
+            :caption: build.gradle.kts
 
-    // Uncomment to disable creating a JAR.
-    //tasks["jar"].enabled = false
+            plugins {
+                `java-library`
+                id("software.amazon.smithy.gradle.smithy-jar").version("0.10.0")
+            }
+
+            repositories {
+                mavenLocal()
+                mavenCentral()
+            }
+
+    .. tab:: Groovy
+
+        .. code-block:: groovy
+            :caption: build.gradle
+
+            plugins {
+                id 'java-library'
+                id 'software.amazon.smithy.gradle.smithy-jar' version '0.10.0'
+            }
+
+            repositories {
+                mavenLocal()
+                mavenCentral()
+            }
+
+
+    Next, run ``gradle build``. That's it! We just created a simple, read-only, ``Weather`` service.
 
-Finally, copy the weather model to ``model/weather.smithy`` and
-then run ``gradle build`` (make sure you have `gradle installed`_).
 
 Next steps
 ==========
 
-That's it! We just created a simple, read-only, ``Weather`` service.
-
 1. Try adding a "create" lifecycle operation to ``City``.
 2. Try adding a "delete" lifecycle operation to ``City``.
 3. Try adding :ref:`HTTP binding traits <http-traits>` to the API.
@@ -511,49 +568,46 @@ There's plenty more to explore in Smithy.
 Complete example
 ================
 
-If you followed all the steps in this guide, you should have three files, laid
-out like so::
 
-    .
-    ├── build.gradle.kts
-    ├── model
-    │   └── weather.smithy
-    └── smithy-build.json
 
-The ``smithy-build.json`` should have the following contents:
+.. note::
 
-.. code-block:: json
+    You can clone a working version of this quickstart example using the
+    :ref:`Smithy CLI <smithy-cli>` ``init`` command.
 
-    {
-        "version": "1.0"
-    }
+    .. tab:: Quickstart with Smithy CLI
 
-The ``build.gradle.kts`` should have the following contents:
+        .. code-block::
 
-.. code-block:: kotlin
+            smithy init -o <output_directory>
 
-    plugins {
-        id("software.amazon.smithy").version("0.6.0")
-    }
+    .. tab:: Quickstart with Gradle
 
-    repositories {
-        mavenLocal()
-        mavenCentral()
-    }
+        .. code-block::
 
-    dependencies {
-        implementation("software.amazon.smithy:smithy-model:__smithy_version__")
-    }
+            smithy init -t quickstart-gradle -o <output_directory>
 
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-        // Uncomment this to use a custom projection when building the JAR.
-        // projection = "foo"
-    }
+If you followed all the steps in this guide, your working directory should be laid out like so:
+
+.. tab:: Smithy CLI
+
+    .. code-block:: text
+
+        .
+        ├── smithy-build.json
+        └── model
+            └── weather.smithy
+
+.. tab:: Gradle
+
+    .. code-block:: text
 
-    // Uncomment to disable creating a JAR.
-    //tasks["jar"].enabled = false
+        .
+        ├── build.gradle.kts
+        └── model
+            └── weather.smithy
 
-Finally, the complete ``weather.smithy`` model should look like:
+The complete ``weather.smithy`` model should look like:
 
 .. code-block:: smithy
 
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlStringLexer.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlStringLexer.java
index d6128d045..bcd30b909 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlStringLexer.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlStringLexer.java
@@ -237,22 +237,16 @@ final class IdlStringLexer {
     /**
      * Computes the number of leading whitespace characters in a string.
      *
-     * <p>This method returns -1 if the string is empty or if the string
-     * contains only whitespace. When determining the whitespace of the
-     * last line, the length of the line is returned if the entire
-     * line consists of only spaces. This ensures that the whitespace
-     * of the last line can be used to influence the indentation of the
-     * content as a whole (a significant trailing line policy).
+     * <p>This method returns -1 if the string contains only whitespace. When determining the whitespace of the
+     * last line, the length of the line is returned if the entire line consists of only spaces. This ensures that
+     * the whitespace of the last line can be used to influence the indentation of the content as a whole
+     * (a significant trailing line policy).
      *
      * @param line Line to search for whitespace.
      * @param isLastLine Whether or not this is the last line.
      * @return Returns the last whitespace index starting at 0 or -1.
      */
     private static int computeLeadingWhitespace(CharSequence line, boolean isLastLine) {
-        if (line.length() == 0) {
-            return -1;
-        }
-
         for (int offset = 0; offset < line.length(); offset++) {
             if (line.charAt(offset) != ' ') {
                 return offset;
diff --git a/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java b/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java
index 2df51058e..a9b1a47f6 100644
--- a/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java
+++ b/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java
@@ -17,14 +17,13 @@ package software.amazon.smithy.syntax;
 
 import com.opencastsoftware.prettier4j.Doc;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.utils.StringUtils;
 
 final class FormatVisitor {
 
@@ -447,11 +446,49 @@ final class FormatVisitor {
             case TEXT_BLOCK: {
                 // Dispersing the lines of the text block preserves any indentation applied from formatting parent
                 // nodes.
-                List<Doc> lines = Arrays.stream(tree.concatTokens().split(System.lineSeparator()))
-                        .map(String::trim)
-                        .map(Doc::text)
-                        .collect(Collectors.toList());
-                return Doc.intersperse(Doc.line(), lines);
+
+                // We need to rebuild the text block to remove any incidental leading whitespace. The easiest way to
+                // do that is to use the already parsed and resolved value from the lexer.
+                String stringValue = cursor.getTree()
+                        .tokens()
+                        .findFirst()
+                        .orElseThrow(() -> new RuntimeException("TEXT_BLOCK cursor does not have an IDL token"))
+                        .getStringContents();
+
+                // If the last character is a newline, then the closing triple quote must be on the next line.
+                boolean endQuoteOnNextLine = stringValue.endsWith("\n") || stringValue.endsWith("\r");
+
+                List<Doc> resultLines = new ArrayList<>();
+                resultLines.add(Doc.text("\"\"\""));
+
+                String[] inputLines = stringValue.split("\\r?\\n", -1);
+                for (int i = 0; i < inputLines.length; i++) {
+                    boolean lastLine = i == inputLines.length - 1;
+
+                    // If this is the last line and the ending quote is on the next line, then skip the extra line.
+                    if (endQuoteOnNextLine && lastLine) {
+                        break;
+                    }
+
+                    String lineValue = inputLines[i];
+
+                    // Trim trailing whitespace.
+                    // TODO: This may need to be configurable.
+                    lineValue = StringUtils.stripEnd(lineValue, null);
+
+                    // Add the closing quote to this line if it needs to be on the last line.
+                    if (lastLine) {
+                        lineValue += "\"\"\"";
+                    }
+
+                    resultLines.add(Doc.text(lineValue));
+                }
+
+                if (endQuoteOnNextLine) {
+                    resultLines.add(Doc.text("\"\"\""));
+                }
+
+                return Doc.intersperse(Doc.line(), resultLines);
             }
 
             case TOKEN:
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java
index d5f5ce0d3..df1a00d68 100644
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java
+++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java
@@ -17,9 +17,12 @@ package software.amazon.smithy.utils;
 
 import java.lang.reflect.Method;
 import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Deque;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -574,6 +577,12 @@ import java.util.regex.Pattern;
  */
 public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
 
+    // Valid formatter characters that can be registered. Must be sorted for binary search to work.
+    static final char[] VALID_FORMATTER_CHARS = {
+            '!', '%', '&', '*', '+', ',', '-', '.', ';', '=', '@',
+            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
+            'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '_', '`'};
+
     private static final Pattern LINES = Pattern.compile("\\r?\\n");
     private static final Map<Character, BiFunction<Object, String, String>> DEFAULT_FORMATTERS = MapUtils.of(
             'L', (s, i) -> formatLiteral(s),
@@ -603,22 +612,19 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
     /**
      * Copies settings from the given AbstractCodeWriter into this AbstractCodeWriter.
      *
-     * <p>The settings of the {@code other} AbstractCodeWriter will overwrite
-     * both global and state-based settings of this AbstractCodeWriter. Formatters of
-     * the {@code other} AbstractCodeWriter will be merged with the formatters of this
-     * AbstractCodeWriter, and in the case of conflicts, the formatters of the
-     * {@code other} will take precedence.
+     * <p>The settings of the {@code other} AbstractCodeWriter will overwrite both global and state-based settings
+     * of this AbstractCodeWriter.
      *
-     * <p>Stateful settings of the {@code other} AbstractCodeWriter are copied into
-     * the <em>current</em> state of this AbstractCodeWriter. Only the settings of
-     * the top-most state is copied. Other states, and the contents of the
-     * top-most state are not copied.
+     * <p>Stateful settings of the {@code other} AbstractCodeWriter like formatters, interceptors, and context are
+     * flattened and then copied into the <em>current</em> state of this AbstractCodeWriter. Any conflicts between
+     * formatters, interceptors, or context of the current writer are overwritten by the other writer. The stack of
+     * states and the contents written to {@code other} are not copied.
      *
      * <pre>{@code
-     * SimpleCodeWritera = new SimpleCodeWriter();
+     * SimpleCodeWriter a = new SimpleCodeWriter();
      * a.setExpressionStart('#');
      *
-     * SimpleCodeWriterb = new SimpleCodeWriter();
+     * SimpleCodeWriter b = new SimpleCodeWriter();
      * b.copySettingsFrom(a);
      *
      * assert(b.getExpressionStart() == '#');
@@ -634,6 +640,16 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
 
         // Copy the current state settings of other into the current state.
         currentState.copyStateFrom(other.currentState);
+
+        // Flatten containers into the current state. This is done in reverse order to ensure that more recent
+        // state changes supersede earlier changes.
+        Iterator<State> reverseOtherStates = other.states.descendingIterator();
+        while (reverseOtherStates.hasNext()) {
+            State otherState = reverseOtherStates.next();
+            currentState.interceptors.addAll(otherState.interceptors);
+            currentState.formatters.putAll(otherState.formatters);
+            currentState.context.putAll(otherState.context);
+        }
     }
 
     /**
@@ -686,7 +702,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
      */
     @SuppressWarnings("unchecked")
     public T putFormatter(char identifier, BiFunction<Object, String, String> formatFunction) {
-        this.currentState.formatters.get().putFormatter(identifier, formatFunction);
+        this.currentState.putFormatter(identifier, formatFunction);
         return (T) this;
     }
 
@@ -962,9 +978,15 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
 
             // Don't attempt to intercept anonymous sections.
             if (!(sectionValue instanceof AnonymousCodeSection)) {
-                for (CodeInterceptor<CodeSection, T> interceptor : popped.interceptors.peek().get(sectionValue)) {
-                    result = interceptSection(popped, interceptor, result);
+                // Ensure the remaining parent interceptors are applied in the order they were inserted.
+                // This is the reverse order used when normally iterating over the states deque.
+                Iterator<State> insertionOrderedStates = states.descendingIterator();
+                while (insertionOrderedStates.hasNext()) {
+                    State state = insertionOrderedStates.next();
+                    result = applyPoppedInterceptors(popped, state, sectionValue, result);
                 }
+                // Now ensure the popped state's interceptors are applied.
+                result = applyPoppedInterceptors(popped, popped, sectionValue, result);
             }
 
             if (popped.isInline) {
@@ -986,6 +1008,13 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
         return (T) this;
     }
 
+    private String applyPoppedInterceptors(State popped, State state, CodeSection sectionValue, String result) {
+        for (CodeInterceptor<CodeSection, T> interceptor : state.getInterceptors(sectionValue)) {
+            result = interceptSection(popped, interceptor, result);
+        }
+        return result;
+    }
+
     // This method exists because inlining in popSection is impossible due to needing to mutate a result variable.
     @SuppressWarnings("unchecked")
     private String interceptSection(State popped, CodeInterceptor<CodeSection, T> interceptor, String previous) {
@@ -1070,7 +1099,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
      */
     @SuppressWarnings("unchecked")
     public T onSection(String sectionName, Consumer<Object> interceptor) {
-        currentState.interceptors.get().putInterceptor(CodeInterceptor.forName(sectionName, (w, p) -> {
+        currentState.putInterceptor(CodeInterceptor.forName(sectionName, (w, p) -> {
             String trimmedContent = removeTrailingNewline(p);
             interceptor.accept(trimmedContent);
         }));
@@ -1098,7 +1127,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
      */
     @SuppressWarnings("unchecked")
     public <S extends CodeSection> T onSection(CodeInterceptor<S, T> interceptor) {
-        currentState.interceptors.get().putInterceptor(interceptor);
+        currentState.putInterceptor(interceptor);
         return (T) this;
     }
 
@@ -1857,7 +1886,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
      */
     @SuppressWarnings("unchecked")
     public T putContext(String key, Object value) {
-        currentState.context.get().put(key, value);
+        currentState.context.put(key, value);
         return (T) this;
     }
 
@@ -1879,40 +1908,45 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
     /**
      * Removes a named key-value pair from the context of the current state.
      *
+     * <p>This method has no effect if the parent state defines the context key value pair.
+     *
      * @param key Key to add to remove from the current context.
      * @return Returns self.
      */
     @SuppressWarnings("unchecked")
     public T removeContext(String key) {
-        if (currentState.context.peek().containsKey(key)) {
-            currentState.context.get().remove(key);
+        if (currentState.context.containsKey(key)) {
+            currentState.context.remove(key);
+        } else {
+            // Parent states might have a value for this context key, so explicitly set it to null in this context.
+            currentState.context.put(key, null);
         }
         return (T) this;
     }
 
     /**
-     * Gets a named contextual key-value pair from the current state.
+     * Gets a named contextual key-value pair from the current state or any parent states.
      *
      * @param key Key to retrieve.
      * @return Returns the associated value or null if not present.
      */
     public Object getContext(String key) {
-        CodeSection section = currentState.sectionValue;
-        Map<String, Object> currentContext = currentState.context.peek();
-        if (currentContext.containsKey(key)) {
-            return currentContext.get(key);
-        } else if (section != null) {
-            Method method = findContextMethod(section, key);
-            if (method != null) {
-                try {
-                    return method.invoke(section);
-                } catch (ReflectiveOperationException e) {
-                    String message = String.format(
-                            "Unable to get context '%s' from a matching method of the current CodeSection: %s %s",
-                            key,
-                            e.getCause() != null ? e.getCause().getMessage() : e.getMessage(),
-                            getDebugInfo());
-                    throw new RuntimeException(message, e);
+        for (State state : states) {
+            if (state.context.containsKey(key)) {
+                return state.context.get(key);
+            } else if (state.sectionValue != null) {
+                Method method = findContextMethod(state.sectionValue, key);
+                if (method != null) {
+                    try {
+                        return method.invoke(state.sectionValue);
+                    } catch (ReflectiveOperationException e) {
+                        String message = String.format(
+                                "Unable to get context '%s' from a matching method of the current CodeSection: %s %s",
+                                key,
+                                e.getCause() != null ? e.getCause().getMessage() : e.getMessage(),
+                                getDebugInfo());
+                        throw new RuntimeException(message, e);
+                    }
                 }
             }
         }
@@ -1984,7 +2018,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
     // Used only by CodeFormatter to apply formatters.
     @SuppressWarnings("unchecked")
     String applyFormatter(char identifier, Object value) {
-        BiFunction<Object, String, String> f = currentState.formatters.peek().getFormatter(identifier);
+        BiFunction<Object, String, String> f = resolveFormatter(identifier);
         if (f != null) {
             return f.apply(value, getIndentText());
         } else if (identifier == 'C') {
@@ -2009,7 +2043,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
                 throw new ClassCastException(String.format(
                         "Expected value for 'C' formatter to be an instance of %s or %s, but found %s %s",
                         Runnable.class.getName(), Consumer.class.getName(),
-                        value.getClass().getName(), getDebugInfo()));
+                        value == null ? "null" : value.getClass().getName(), getDebugInfo()));
             }
         } else {
             // Return null if no formatter was found.
@@ -2017,6 +2051,16 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
         }
     }
 
+    BiFunction<Object, String, String> resolveFormatter(char identifier) {
+        for (State state : states) {
+            BiFunction<Object, String, String> result = state.getFormatter(identifier);
+            if (result != null) {
+                return result;
+            }
+        }
+        return null;
+    }
+
     private final class State {
         private final boolean isRoot;
         private String indentText = "    ";
@@ -2030,9 +2074,9 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
         private boolean needsIndentation;
 
         private CodeSection sectionValue;
-        private CopyOnWriteRef<Map<String, Object>> context;
-        private CopyOnWriteRef<CodeWriterFormatterContainer> formatters;
-        private CopyOnWriteRef<CodeInterceptorContainer<T>> interceptors;
+        private final Map<String, Object> context = new HashMap<>();
+        private final Map<Character, BiFunction<Object, String, String>> formatters = new HashMap<>();
+        private final List<CodeInterceptor<CodeSection, T>> interceptors = new ArrayList<>();
 
         private StringBuilder builder;
 
@@ -2046,11 +2090,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
         State() {
             builder = new StringBuilder();
             isRoot = true;
-            CodeWriterFormatterContainer formatterContainer = new CodeWriterFormatterContainer();
-            DEFAULT_FORMATTERS.forEach(formatterContainer::putFormatter);
-            this.formatters = CopyOnWriteRef.fromOwned(formatterContainer);
-            this.context = CopyOnWriteRef.fromOwned(new HashMap<>());
-            this.interceptors = CopyOnWriteRef.fromOwned(new CodeInterceptorContainer<>());
+            DEFAULT_FORMATTERS.forEach(this::putFormatter);
         }
 
         @SuppressWarnings("CopyConstructorMissesField")
@@ -2060,10 +2100,11 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
             this.builder = copy.builder;
         }
 
+        // This does not copy context, interceptors, or formatters.
+        // State inheritance relies on stacks of States in an AbstractCodeWriter.
         private void copyStateFrom(State copy) {
             this.newline = copy.newline;
             this.expressionStart = copy.expressionStart;
-            this.context = copy.context;
             this.indentText = copy.indentText;
             this.leadingIndentString = copy.leadingIndentString;
             this.indentation = copy.indentation;
@@ -2071,9 +2112,6 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
             this.trimTrailingSpaces = copy.trimTrailingSpaces;
             this.disableNewline = copy.disableNewline;
             this.needsIndentation = copy.needsIndentation;
-            this.context = CopyOnWriteRef.fromBorrowed(copy.context.peek(), HashMap::new);
-            this.formatters = CopyOnWriteRef.fromBorrowed(copy.formatters.peek(), CodeWriterFormatterContainer::new);
-            this.interceptors = CopyOnWriteRef.fromBorrowed(copy.interceptors.peek(), CodeInterceptorContainer::new);
         }
 
         @Override
@@ -2200,6 +2238,48 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
                 return sectionValue.sectionName();
             }
         }
+
+        void putFormatter(Character identifier, BiFunction<Object, String, String> formatFunction) {
+            if (Arrays.binarySearch(VALID_FORMATTER_CHARS, identifier) < 0) {
+                throw new IllegalArgumentException("Invalid formatter identifier: " + identifier);
+            }
+            formatters.put(identifier, formatFunction);
+        }
+
+        BiFunction<Object, String, String> getFormatter(char identifier) {
+            return formatters.get(identifier);
+        }
+
+        @SuppressWarnings("unchecked")
+        void putInterceptor(CodeInterceptor<? extends CodeSection, T> interceptor) {
+            interceptors.add((CodeInterceptor<CodeSection, T>) interceptor);
+        }
+
+        /**
+         * Gets a list of interceptors that match the given type and for which the
+         * result of {@link CodeInterceptor#isIntercepted(CodeSection)} returns true
+         * when given {@code forSection}.
+         *
+         * @param forSection The section that is being intercepted.
+         * @param <S> The type of section being intercepted.
+         * @return Returns the list of matching interceptors.
+         */
+        <S extends CodeSection> List<CodeInterceptor<CodeSection, T>> getInterceptors(S forSection) {
+            // Add in parent interceptors.
+            List<CodeInterceptor<CodeSection, T>> result = new ArrayList<>();
+            // Merge in local interceptors.
+            for (CodeInterceptor<CodeSection, T> interceptor : interceptors) {
+                // Add the interceptor only if it's the right type.
+                if (interceptor.sectionType().isInstance(forSection)) {
+                    // Only add if the filter passes.
+                    if (interceptor.isIntercepted(forSection)) {
+                        result.add(interceptor);
+                    }
+                }
+            }
+
+            return result;
+        }
     }
 
     String removeTrailingNewline(String value) {
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeFormatter.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeFormatter.java
index 8a7efaf99..04355e05a 100644
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeFormatter.java
+++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeFormatter.java
@@ -635,7 +635,7 @@ final class CodeFormatter {
             // Parse the formatter and apply it.
             int line = parser.line();
             int column = parser.column();
-            char identifier = parser.expect(CodeWriterFormatterContainer.VALID_FORMATTER_CHARS);
+            char identifier = parser.expect(AbstractCodeWriter.VALID_FORMATTER_CHARS);
 
             // The error message needs to be created here and given to the operation in way that it can
             // throw with an appropriate message.
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeInterceptorContainer.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeInterceptorContainer.java
deleted file mode 100644
index 2a0425ae0..000000000
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeInterceptorContainer.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates. 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.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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 software.amazon.smithy.utils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A container for CodeSection.Interceptors registered with AbstractCodeWriter.
- */
-@SmithyInternalApi
-final class CodeInterceptorContainer<W extends AbstractCodeWriter<W>> {
-
-    private final List<CodeInterceptor<CodeSection, W>> interceptors = new ArrayList<>();
-    private final CodeInterceptorContainer<W> parent;
-
-    CodeInterceptorContainer() {
-        this(null);
-    }
-
-    CodeInterceptorContainer(CodeInterceptorContainer<W> parent) {
-        this.parent = parent;
-    }
-
-    @SuppressWarnings("unchecked")
-    void putInterceptor(CodeInterceptor<? extends CodeSection, W> interceptor) {
-        interceptors.add((CodeInterceptor<CodeSection, W>) interceptor);
-    }
-
-    /**
-     * Gets a list of interceptors that match the given type and for which the
-     * result of {@link CodeInterceptor#isIntercepted(CodeSection)} returns true
-     * when given {@code forSection}.
-     *
-     * @param forSection The section that is being intercepted.
-     * @param <S> The type of section being intercepted.
-     * @return Returns the list of matching interceptors.
-     */
-    <S extends CodeSection> List<CodeInterceptor<CodeSection, W>> get(S forSection) {
-        // Add in parent interceptors.
-        List<CodeInterceptor<CodeSection, W>> result = parent == null
-                ? new ArrayList<>()
-                : parent.get(forSection);
-        // Merge in local interceptors.
-        for (CodeInterceptor<CodeSection, W> interceptor : interceptors) {
-            // Add the interceptor only if it's the right type.
-            if (interceptor.sectionType().isInstance(forSection)) {
-                // Only add if the filter passes.
-                if (interceptor.isIntercepted(forSection)) {
-                    result.add(interceptor);
-                }
-            }
-        }
-
-        return result;
-    }
-}
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterDebugInfo.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterDebugInfo.java
index e1a4ecbf3..7f25e9981 100644
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterDebugInfo.java
+++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterDebugInfo.java
@@ -42,7 +42,7 @@ public final class CodeWriterDebugInfo {
     /**
      * Get the CodeWriter state path from which the debug information was collected.
      *
-     * @return Returns the path as returned by {@link CodeWriter#getStateDebugPath()};
+     * @return Returns the path as returned by {@link AbstractCodeWriter#getDebugInfo()};
      */
     public String getStateDebugPath() {
         return getMetadata("path");
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterFormatterContainer.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterFormatterContainer.java
deleted file mode 100644
index e2dfffebb..000000000
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterFormatterContainer.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates. 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.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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 software.amazon.smithy.utils;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.BiFunction;
-
-/**
- * A container for formatters registered with CodeWriter.
- */
-@SmithyInternalApi
-final class CodeWriterFormatterContainer {
-
-    // Must be sorted for binary search to work.
-    static final char[] VALID_FORMATTER_CHARS = {
-            '!', '%', '&', '*', '+', ',', '-', '.', ';', '=', '@',
-            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
-            'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '_', '`'};
-
-    private final Map<Character, BiFunction<Object, String, String>> formatters = new HashMap<>();
-    private final CodeWriterFormatterContainer parent;
-
-    CodeWriterFormatterContainer() {
-        this(null);
-    }
-
-    CodeWriterFormatterContainer(CodeWriterFormatterContainer parent) {
-        this.parent = parent;
-    }
-
-    void putFormatter(Character identifier, BiFunction<Object, String, String> formatFunction) {
-        if (Arrays.binarySearch(VALID_FORMATTER_CHARS, identifier) < 0) {
-            throw new IllegalArgumentException("Invalid formatter identifier: " + identifier);
-        }
-        formatters.put(identifier, formatFunction);
-    }
-
-    BiFunction<Object, String, String> getFormatter(char identifier) {
-        BiFunction<Object, String, String> result = formatters.get(identifier);
-
-        if (result == null && parent != null) {
-            result = parent.getFormatter(identifier);
-        }
-
-        return result;
-    }
-}


Test patch:
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlInternalTokenizerTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlInternalTokenizerTest.java
index dfce68688..a0ae485b7 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlInternalTokenizerTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlInternalTokenizerTest.java
@@ -21,9 +21,13 @@ import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.nullValue;
 import static org.hamcrest.Matchers.startsWith;
 
+import java.util.stream.Stream;
 import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 public class IdlInternalTokenizerTest {
 
@@ -181,4 +185,81 @@ public class IdlInternalTokenizerTest {
         assertThat(lines, equalTo("Hi\nThere\n123\n456"));
         MatcherAssert.assertThat(tokenizer.removePendingDocCommentLines(), nullValue());
     }
+
+    public static Stream<Arguments> textBlockTests() {
+        return Stream.of(
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "    Hello\n"
+                        + "        - Indented\"\"\"\n",
+                        "Hello\n    - Indented"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "    Hello\n"
+                        + "        - Indented\n"
+                        + "    \"\"\"\n",
+                        "Hello\n    - Indented\n"
+                ),
+                Arguments.of(
+                    "\"\"\"\n"
+                    + "    Hello\n"
+                    + "        - Indented\n"
+                    + "\"\"\"\n",
+                    "    Hello\n        - Indented\n"
+                ),
+                Arguments.of(
+                    "\"\"\"\n"
+                    + "    Hello\"\"\"\n",
+                    "Hello"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "    Hello\n"
+                        + "\n"
+                        + "        - Indented\n"
+                        + "\"\"\"\n",
+                        "    Hello\n\n        - Indented\n"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "                        \n" // only WS doesn't influence line length calculations.
+                        + "          Hello\n"
+                        + "                        \n" // only WS doesn't influence line length calculations.
+                        + "          \"\"\"",
+                        "\nHello\n\n"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "\n" // empty lines are incidental whitespace.
+                        + "          Hello\n"
+                        + "                        \n" // only WS doesn't influence line length calculations.
+                        + "          \"\"\"",
+                        "\nHello\n\n"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "\n" // empty lines are incidental whitespace.
+                        + "Hello\n"
+                        + "\n"
+                        + "\n"
+                        + "\"\"\"",
+                        "\nHello\n\n\n"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "\"\"\"",
+                        ""
+                )
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("textBlockTests")
+    public void parsesTextBlocks(String model, String stringValue) {
+        IdlInternalTokenizer tokenizer = new IdlInternalTokenizer("a.smithy", model);
+        tokenizer.expect(IdlToken.TEXT_BLOCK);
+
+        assertThat(tokenizer.getCurrentTokenStringSlice().toString(), equalTo(stringValue));
+    }
 }
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy
index 9a39712d8..0e4d4b241 100644
--- a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy
@@ -9,7 +9,7 @@ metadata validators = [
         configuration: {
             selector: """
                 operation -[input]-> structure > member
-                :test(member > string:not([trait|enum]))
+                    :test(member > string:not([trait|enum]))
                 :test(member > string:not([trait|length]))
                 :test(member > string:not([trait|aws.api#arnReference]))
                 :test(member > string:not([trait|aws.api#providesPassRole]))
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy
index 899acc3ee..242e7b96f 100644
--- a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy
@@ -2,6 +2,7 @@ $version: "2.0"
 
 namespace smithy.example
 
+// This will look exactly the same when formatted.
 @documentation(
     """
     This is the documentation for Foo.
@@ -9,6 +10,7 @@ namespace smithy.example
 )
 string Foo
 
+// This will look exactly the same when formatted.
 @documentation(
     """
     This is the documentation for Bar.
@@ -17,6 +19,7 @@ string Foo
 )
 string Bar
 
+// The quotes and text will be indented inside the parens when formatted.
 @documentation(
     """
     This is the documentation for Baz.
@@ -25,10 +28,61 @@ string Bar
 )
 string Baz
 
+// The opening quotes will be indented and move to the next line, the contents of the block will remain indented
+// because indentation is based on the closing quote, the closing quote will be indented, and the closing paren on
+// the next line.
 @documentation(
     """
-    This is the documentation for Bux.
-    Lorem ipsum dolor.
+        This is the documentation for Bux.
+        Lorem ipsum dolor.
     """
 )
 string Bux
+
+// The opening quote will move to the next line, indentend, the content will remain the same, the closing quote
+// will stay on last line, and the closing paren will move to the next line.
+@documentation(
+    """
+    This doc string must not have the leading whitespace altered.
+
+    {
+        "foo": true,
+        bar: [
+            false
+        ]
+    }"""
+)
+string JsonDocsNoTrailingNewLine
+
+// The opening quote will move to the next line, indentend, the content will remain the same, the closing quote
+// will stay on its own line, and the closing paren will move to the next line.
+@documentation(
+    """
+    This doc string must not have the leading whitespace altered.
+
+    {
+        "foo": true,
+        bar: [
+            false
+        ]
+    }
+    """
+)
+string JsonDocsTrailingNewLine
+
+// Ensure extra newlines at the end are included.
+@documentation(
+    """
+    Hello with extra newlines.
+
+
+    """
+)
+string ExtraTrailingNewlines
+
+// Moves the quotes to the same indentation level and the closing parens to the next line.
+@documentation(
+    """
+    """
+)
+string EmptyTextBlock
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy
index 834b3579b..2527d7609 100644
--- a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy
@@ -2,6 +2,7 @@ $version: "2.0"
 
 namespace smithy.example
 
+// This will look exactly the same when formatted.
 @documentation(
     """
     This is the documentation for Foo.
@@ -9,6 +10,7 @@ namespace smithy.example
 )
 string Foo
 
+// This will look exactly the same when formatted.
 @documentation(
     """
     This is the documentation for Bar.
@@ -17,6 +19,7 @@ string Foo
 )
 string Bar
 
+// The quotes and text will be indented inside the parens when formatted.
 @documentation(
 """
 This is the documentation for Baz.
@@ -25,8 +28,51 @@ Lorem ipsum dolor.
 )
 string Baz
 
+// The opening quotes will be indented and move to the next line, the contents of the block will remain indented
+// because indentation is based on the closing quote, the closing quote will be indented, and the closing paren on
+// the next line.
 @documentation("""
     This is the documentation for Bux.
     Lorem ipsum dolor.
 """)
 string Bux
+
+// The opening quote will move to the next line, indentend, the content will remain the same, the closing quote
+// will stay on last line, and the closing paren will move to the next line.
+@documentation("""
+    This doc string must not have the leading whitespace altered.
+
+    {
+        "foo": true,
+        bar: [
+            false
+        ]
+    }""")
+string JsonDocsNoTrailingNewLine
+
+// The opening quote will move to the next line, indentend, the content will remain the same, the closing quote
+// will stay on its own line, and the closing paren will move to the next line.
+@documentation("""
+    This doc string must not have the leading whitespace altered.
+
+    {
+        "foo": true,
+        bar: [
+            false
+        ]
+    }
+    """)
+string JsonDocsTrailingNewLine
+
+// Ensure extra newlines at the end are included.
+@documentation("""
+    Hello with extra newlines.
+
+
+    """)
+string ExtraTrailingNewlines
+
+// Moves the quotes to the same indentation level and the closing parens to the next line.
+@documentation("""
+    """)
+string EmptyTextBlock
diff --git a/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java b/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java
index af0faf970..c87738c88 100644
--- a/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java
+++ b/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java
@@ -23,8 +23,10 @@ import static org.hamcrest.Matchers.startsWith;
 
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.Locale;
 import java.util.Optional;
+import java.util.function.Consumer;
 import java.util.stream.Stream;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -982,6 +984,26 @@ public class CodeWriterTest {
         assertThat(a.toString(), equalTo("\n"));
     }
 
+    @Test
+    public void flattensStatesOfOtherWhenCopying() {
+        CodeWriter a = new CodeWriter();
+        a.pushState();
+        a.putFormatter('A', (value, indent) -> value.toString().toUpperCase(Locale.ENGLISH));
+        a.putContext("a", 1);
+        a.putContext("b", "B");
+        a.pushState();
+        a.putContext("b", 2); // override "b"
+        a.putFormatter('B', (value, indent) -> value.toString().toLowerCase(Locale.ENGLISH));
+
+        CodeWriter b = new CodeWriter();
+        b.putContext("c", 3);
+        b.copySettingsFrom(a);
+        b.writeInline("${1A} ${2B} ${a:L} ${b:L} ${c:L}", "Hello", "Goodbye");
+
+        assertThat(b.toString(), equalTo("HELLO goodbye 1 2 3\n"));
+        assertThat(a.toString(), equalTo("\n"));
+    }
+
     @Test
     public void canPassRunnableToFormatters() {
         CodeWriter writer = new CodeWriter();
@@ -1451,6 +1473,34 @@ public class CodeWriterTest {
         assertThat(writer.toString(), equalTo("foo: ok actually\n"));
     }
 
+    @Test
+    public void canAccessStateClassesOfInheritedStates() {
+        SimpleCodeWriter writer = new SimpleCodeWriter();
+        writer.putContext("a", "A");
+        writer.pushState();
+        writer.pushState(new CustomSection());
+        writer.putContext("b", "B");
+        writer.pushState();
+        writer.write("${a:L}, ${b:L}, ${hello:L}${partial:C|}");
+        writer.popState();
+
+        assertThat(writer.toString(), equalTo("A, B, Hello, 0T, 1T, 2T\n"));
+    }
+
+    @Test
+    public void canRemoveContextFromTheCurrentState() {
+        SimpleCodeWriter writer = new SimpleCodeWriter();
+        writer.putContext("a", "A");
+        writer.pushState();
+        writer.putContext("b", "B");
+        writer.removeContext("a");
+
+        writer.write("${a:L}, ${b:L}");
+        writer.popState();
+
+        assertThat(writer.toString(), equalTo(", B\n"));
+    }
+
     private static final class MySection implements CodeSection {
         public String getFoo() {
             return "foo";
@@ -1496,4 +1546,25 @@ public class CodeWriterTest {
             return false;
         }
     }
+
+    public static final class CustomSection implements CodeSection {
+        public String hello() {
+            return "Hello";
+        }
+
+        public List<String> values() {
+            return ListUtils.of("a", "b", "c");
+        }
+
+        public String test() {
+            return "T";
+        }
+
+        public Consumer<AbstractCodeWriter<?>> partial() {
+            // This partial ensures that nested contexts can reach up to parent contexts and sections to get values.
+            return writer -> {
+                writer.write("${#values}, ${key:L}${test:L}${/values}");
+            };
+        }
+    }
 }