diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1570,6 +1570,7 @@
     'PhabricatorApplicationDatasource' => 'applications/meta/typeahead/PhabricatorApplicationDatasource.php',
     'PhabricatorApplicationDetailViewController' => 'applications/meta/controller/PhabricatorApplicationDetailViewController.php',
     'PhabricatorApplicationEditController' => 'applications/meta/controller/PhabricatorApplicationEditController.php',
+    'PhabricatorApplicationEditEngine' => 'applications/transactions/editengine/PhabricatorApplicationEditEngine.php',
     'PhabricatorApplicationEmailCommandsController' => 'applications/meta/controller/PhabricatorApplicationEmailCommandsController.php',
     'PhabricatorApplicationLaunchView' => 'applications/meta/view/PhabricatorApplicationLaunchView.php',
     'PhabricatorApplicationPanelController' => 'applications/meta/controller/PhabricatorApplicationPanelController.php',
@@ -2063,6 +2064,7 @@
     'PhabricatorDataCacheSpec' => 'applications/cache/spec/PhabricatorDataCacheSpec.php',
     'PhabricatorDataNotAttachedException' => 'infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php',
     'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php',
+    'PhabricatorDatasourceEditField' => 'applications/transactions/editfield/PhabricatorDatasourceEditField.php',
     'PhabricatorDateTimeSettingsPanel' => 'applications/settings/panel/PhabricatorDateTimeSettingsPanel.php',
     'PhabricatorDebugController' => 'applications/system/controller/PhabricatorDebugController.php',
     'PhabricatorDefaultRequestExceptionHandler' => 'aphront/handler/PhabricatorDefaultRequestExceptionHandler.php',
@@ -2097,6 +2099,7 @@
     'PhabricatorEdgeTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php',
     'PhabricatorEdgeType' => 'infrastructure/edges/type/PhabricatorEdgeType.php',
     'PhabricatorEdgeTypeTestCase' => 'infrastructure/edges/type/__tests__/PhabricatorEdgeTypeTestCase.php',
+    'PhabricatorEditField' => 'applications/transactions/editfield/PhabricatorEditField.php',
     'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php',
     'PhabricatorElasticSearchEngine' => 'applications/search/engine/PhabricatorElasticSearchEngine.php',
     'PhabricatorElasticSearchSetupCheck' => 'applications/config/check/PhabricatorElasticSearchSetupCheck.php',
@@ -2554,6 +2557,7 @@
     'PhabricatorPasteController' => 'applications/paste/controller/PhabricatorPasteController.php',
     'PhabricatorPasteDAO' => 'applications/paste/storage/PhabricatorPasteDAO.php',
     'PhabricatorPasteEditController' => 'applications/paste/controller/PhabricatorPasteEditController.php',
+    'PhabricatorPasteEditEngine' => 'applications/paste/editor/PhabricatorPasteEditEngine.php',
     'PhabricatorPasteEditor' => 'applications/paste/editor/PhabricatorPasteEditor.php',
     'PhabricatorPasteListController' => 'applications/paste/controller/PhabricatorPasteListController.php',
     'PhabricatorPastePastePHIDType' => 'applications/paste/phid/PhabricatorPastePastePHIDType.php',
@@ -2654,6 +2658,7 @@
     'PhabricatorPolicyDAO' => 'applications/policy/storage/PhabricatorPolicyDAO.php',
     'PhabricatorPolicyDataTestCase' => 'applications/policy/__tests__/PhabricatorPolicyDataTestCase.php',
     'PhabricatorPolicyEditController' => 'applications/policy/controller/PhabricatorPolicyEditController.php',
+    'PhabricatorPolicyEditField' => 'applications/transactions/editfield/PhabricatorPolicyEditField.php',
     'PhabricatorPolicyException' => 'applications/policy/exception/PhabricatorPolicyException.php',
     'PhabricatorPolicyExplainController' => 'applications/policy/controller/PhabricatorPolicyExplainController.php',
     'PhabricatorPolicyFilter' => 'applications/policy/filter/PhabricatorPolicyFilter.php',
@@ -2917,6 +2922,7 @@
     'PhabricatorSearchWorker' => 'applications/search/worker/PhabricatorSearchWorker.php',
     'PhabricatorSecurityConfigOptions' => 'applications/config/option/PhabricatorSecurityConfigOptions.php',
     'PhabricatorSecuritySetupCheck' => 'applications/config/check/PhabricatorSecuritySetupCheck.php',
+    'PhabricatorSelectEditField' => 'applications/transactions/editfield/PhabricatorSelectEditField.php',
     'PhabricatorSendGridConfigOptions' => 'applications/config/option/PhabricatorSendGridConfigOptions.php',
     'PhabricatorSessionsSettingsPanel' => 'applications/settings/panel/PhabricatorSessionsSettingsPanel.php',
     'PhabricatorSettingsAddEmailAction' => 'applications/settings/action/PhabricatorSettingsAddEmailAction.php',
@@ -2957,6 +2963,7 @@
     'PhabricatorSlugTestCase' => 'infrastructure/util/__tests__/PhabricatorSlugTestCase.php',
     'PhabricatorSortTableUIExample' => 'applications/uiexample/examples/PhabricatorSortTableUIExample.php',
     'PhabricatorSourceCodeView' => 'view/layout/PhabricatorSourceCodeView.php',
+    'PhabricatorSpaceEditField' => 'applications/transactions/editfield/PhabricatorSpaceEditField.php',
     'PhabricatorSpacesApplication' => 'applications/spaces/application/PhabricatorSpacesApplication.php',
     'PhabricatorSpacesArchiveController' => 'applications/spaces/controller/PhabricatorSpacesArchiveController.php',
     'PhabricatorSpacesCapabilityCreateSpaces' => 'applications/spaces/capability/PhabricatorSpacesCapabilityCreateSpaces.php',
@@ -3063,6 +3070,8 @@
     'PhabricatorTestNoCycleEdgeType' => 'applications/transactions/edges/PhabricatorTestNoCycleEdgeType.php',
     'PhabricatorTestStorageEngine' => 'applications/files/engine/PhabricatorTestStorageEngine.php',
     'PhabricatorTestWorker' => 'infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php',
+    'PhabricatorTextAreaEditField' => 'applications/transactions/editfield/PhabricatorTextAreaEditField.php',
+    'PhabricatorTextEditField' => 'applications/transactions/editfield/PhabricatorTextEditField.php',
     'PhabricatorTime' => 'infrastructure/time/PhabricatorTime.php',
     'PhabricatorTimeGuard' => 'infrastructure/time/PhabricatorTimeGuard.php',
     'PhabricatorTimeTestCase' => 'infrastructure/time/__tests__/PhabricatorTimeTestCase.php',
@@ -3084,6 +3093,7 @@
     'PhabricatorTokenReceiverQuery' => 'applications/tokens/query/PhabricatorTokenReceiverQuery.php',
     'PhabricatorTokenTokenPHIDType' => 'applications/tokens/phid/PhabricatorTokenTokenPHIDType.php',
     'PhabricatorTokenUIEventListener' => 'applications/tokens/event/PhabricatorTokenUIEventListener.php',
+    'PhabricatorTokenizerEditField' => 'applications/transactions/editfield/PhabricatorTokenizerEditField.php',
     'PhabricatorTokensApplication' => 'applications/tokens/application/PhabricatorTokensApplication.php',
     'PhabricatorTokensSettingsPanel' => 'applications/settings/panel/PhabricatorTokensSettingsPanel.php',
     'PhabricatorTooltipUIExample' => 'applications/uiexample/examples/PhabricatorTooltipUIExample.php',
@@ -5489,6 +5499,7 @@
     'PhabricatorApplicationDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorApplicationDetailViewController' => 'PhabricatorApplicationsController',
     'PhabricatorApplicationEditController' => 'PhabricatorApplicationsController',
+    'PhabricatorApplicationEditEngine' => 'Phobject',
     'PhabricatorApplicationEmailCommandsController' => 'PhabricatorApplicationsController',
     'PhabricatorApplicationLaunchView' => 'AphrontTagView',
     'PhabricatorApplicationPanelController' => 'PhabricatorApplicationsController',
@@ -6080,6 +6091,7 @@
     'PhabricatorDataCacheSpec' => 'PhabricatorCacheSpec',
     'PhabricatorDataNotAttachedException' => 'Exception',
     'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck',
+    'PhabricatorDatasourceEditField' => 'PhabricatorTokenizerEditField',
     'PhabricatorDateTimeSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorDebugController' => 'PhabricatorController',
     'PhabricatorDefaultRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
@@ -6113,6 +6125,7 @@
     'PhabricatorEdgeTestCase' => 'PhabricatorTestCase',
     'PhabricatorEdgeType' => 'Phobject',
     'PhabricatorEdgeTypeTestCase' => 'PhabricatorTestCase',
+    'PhabricatorEditField' => 'Phobject',
     'PhabricatorEditor' => 'Phobject',
     'PhabricatorElasticSearchEngine' => 'PhabricatorSearchEngine',
     'PhabricatorElasticSearchSetupCheck' => 'PhabricatorSetupCheck',
@@ -6647,6 +6660,7 @@
     'PhabricatorPasteController' => 'PhabricatorController',
     'PhabricatorPasteDAO' => 'PhabricatorLiskDAO',
     'PhabricatorPasteEditController' => 'PhabricatorPasteController',
+    'PhabricatorPasteEditEngine' => 'PhabricatorApplicationEditEngine',
     'PhabricatorPasteEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhabricatorPasteListController' => 'PhabricatorPasteController',
     'PhabricatorPastePastePHIDType' => 'PhabricatorPHIDType',
@@ -6762,6 +6776,7 @@
     'PhabricatorPolicyDAO' => 'PhabricatorLiskDAO',
     'PhabricatorPolicyDataTestCase' => 'PhabricatorTestCase',
     'PhabricatorPolicyEditController' => 'PhabricatorPolicyController',
+    'PhabricatorPolicyEditField' => 'PhabricatorEditField',
     'PhabricatorPolicyException' => 'Exception',
     'PhabricatorPolicyExplainController' => 'PhabricatorPolicyController',
     'PhabricatorPolicyFilter' => 'Phobject',
@@ -7090,6 +7105,7 @@
     'PhabricatorSearchWorker' => 'PhabricatorWorker',
     'PhabricatorSecurityConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorSecuritySetupCheck' => 'PhabricatorSetupCheck',
+    'PhabricatorSelectEditField' => 'PhabricatorEditField',
     'PhabricatorSendGridConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorSessionsSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorSettingsAddEmailAction' => 'PhabricatorSystemAction',
@@ -7140,6 +7156,7 @@
     'PhabricatorSlugTestCase' => 'PhabricatorTestCase',
     'PhabricatorSortTableUIExample' => 'PhabricatorUIExample',
     'PhabricatorSourceCodeView' => 'AphrontView',
+    'PhabricatorSpaceEditField' => 'PhabricatorEditField',
     'PhabricatorSpacesApplication' => 'PhabricatorApplication',
     'PhabricatorSpacesArchiveController' => 'PhabricatorSpacesController',
     'PhabricatorSpacesCapabilityCreateSpaces' => 'PhabricatorPolicyCapability',
@@ -7252,6 +7269,8 @@
     'PhabricatorTestNoCycleEdgeType' => 'PhabricatorEdgeType',
     'PhabricatorTestStorageEngine' => 'PhabricatorFileStorageEngine',
     'PhabricatorTestWorker' => 'PhabricatorWorker',
+    'PhabricatorTextAreaEditField' => 'PhabricatorEditField',
+    'PhabricatorTextEditField' => 'PhabricatorEditField',
     'PhabricatorTime' => 'Phobject',
     'PhabricatorTimeGuard' => 'Phobject',
     'PhabricatorTimeTestCase' => 'PhabricatorTestCase',
@@ -7278,6 +7297,7 @@
     'PhabricatorTokenReceiverQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorTokenTokenPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorTokenUIEventListener' => 'PhabricatorEventListener',
+    'PhabricatorTokenizerEditField' => 'PhabricatorEditField',
     'PhabricatorTokensApplication' => 'PhabricatorApplication',
     'PhabricatorTokensSettingsPanel' => 'PhabricatorSettingsPanel',
     'PhabricatorTooltipUIExample' => 'PhabricatorUIExample',
diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php
--- a/src/applications/base/controller/PhabricatorController.php
+++ b/src/applications/base/controller/PhabricatorController.php
@@ -521,6 +521,14 @@
   }
 
 
+  public function buildApplicationCrumbsForEditEngine() {
+    // TODO: This is kind of gross, I'm bascially just making this public so
+    // I can use it in EditEngine. We could do this without making it public
+    // by using controller delegation, or make it properly public.
+    return $this->buildApplicationCrumbs();
+  }
+
+
 /* -(  Deprecated  )--------------------------------------------------------- */
 
 
diff --git a/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php b/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php
--- a/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php
+++ b/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php
@@ -44,16 +44,11 @@
 
     $paste = PhabricatorPaste::initializeNewPaste($viewer);
 
-    $file = PhabricatorPasteEditor::initializeFileForPaste(
-      $viewer,
-      $title,
-      $content);
-
     $xactions = array();
 
     $xactions[] = id(new PhabricatorPasteTransaction())
       ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT)
-      ->setNewValue($file->getPHID());
+      ->setNewValue($content);
 
     $xactions[] = id(new PhabricatorPasteTransaction())
       ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE)
diff --git a/src/applications/paste/controller/PhabricatorPasteEditController.php b/src/applications/paste/controller/PhabricatorPasteEditController.php
--- a/src/applications/paste/controller/PhabricatorPasteEditController.php
+++ b/src/applications/paste/controller/PhabricatorPasteEditController.php
@@ -3,248 +3,9 @@
 final class PhabricatorPasteEditController extends PhabricatorPasteController {
 
   public function handleRequest(AphrontRequest $request) {
-    $viewer = $request->getViewer();
-    $id = $request->getURIData('id');
-
-    $parent = null;
-    $parent_id = null;
-    if (!$id) {
-      $is_create = true;
-
-      $paste = PhabricatorPaste::initializeNewPaste($viewer);
-
-      $parent_id = $request->getStr('parent');
-      if ($parent_id) {
-        // NOTE: If the Paste is forked from a paste which the user no longer
-        // has permission to see, we still let them edit it.
-        $parent = id(new PhabricatorPasteQuery())
-          ->setViewer($viewer)
-          ->withIDs(array($parent_id))
-          ->needContent(true)
-          ->needRawContent(true)
-          ->execute();
-        $parent = head($parent);
-
-        if ($parent) {
-          $paste->setParentPHID($parent->getPHID());
-          $paste->setViewPolicy($parent->getViewPolicy());
-        }
-      }
-
-      $paste->setAuthorPHID($viewer->getPHID());
-      $paste->attachRawContent('');
-    } else {
-      $is_create = false;
-
-      $paste = id(new PhabricatorPasteQuery())
-        ->setViewer($viewer)
-        ->requireCapabilities(
-          array(
-            PhabricatorPolicyCapability::CAN_VIEW,
-            PhabricatorPolicyCapability::CAN_EDIT,
-          ))
-        ->withIDs(array($id))
-        ->needRawContent(true)
-        ->executeOne();
-      if (!$paste) {
-        return new Aphront404Response();
-      }
-    }
-
-    $v_space = $paste->getSpacePHID();
-    if ($is_create && $parent) {
-      $v_title = pht('Fork of %s', $parent->getFullName());
-      $v_language = $parent->getLanguage();
-      $v_text = $parent->getRawContent();
-      $v_space = $parent->getSpacePHID();
-    } else {
-      $v_title = $paste->getTitle();
-      $v_language = $paste->getLanguage();
-      $v_text = $paste->getRawContent();
-    }
-    $v_view_policy = $paste->getViewPolicy();
-    $v_edit_policy = $paste->getEditPolicy();
-    $v_status = $paste->getStatus();
-
-    if ($is_create) {
-      $v_projects = array();
-    } else {
-      $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs(
-        $paste->getPHID(),
-        PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
-      $v_projects = array_reverse($v_projects);
-    }
-
-    $validation_exception = null;
-    if ($request->isFormPost()) {
-      $xactions = array();
-
-      $v_text = $request->getStr('text');
-      $v_title = $request->getStr('title');
-      $v_language = $request->getStr('language');
-      $v_view_policy = $request->getStr('can_view');
-      $v_edit_policy = $request->getStr('can_edit');
-      $v_projects = $request->getArr('projects');
-      $v_space = $request->getStr('spacePHID');
-      $v_status = $request->getStr('status');
-
-      // NOTE: The author is the only editor and can always view the paste,
-      // so it's impossible for them to choose an invalid policy.
-
-      if ($is_create || ($v_text !== $paste->getRawContent())) {
-        $file = PhabricatorPasteEditor::initializeFileForPaste(
-          $viewer,
-          $v_title,
-          $v_text);
-
-        $xactions[] = id(new PhabricatorPasteTransaction())
-          ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT)
-          ->setNewValue($file->getPHID());
-      }
-
-      $xactions[] = id(new PhabricatorPasteTransaction())
-        ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE)
-        ->setNewValue($v_title);
-      $xactions[] = id(new PhabricatorPasteTransaction())
-        ->setTransactionType(PhabricatorPasteTransaction::TYPE_LANGUAGE)
-        ->setNewValue($v_language);
-      $xactions[] = id(new PhabricatorPasteTransaction())
-        ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
-        ->setNewValue($v_view_policy);
-      $xactions[] = id(new PhabricatorPasteTransaction())
-        ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
-        ->setNewValue($v_edit_policy);
-      $xactions[] = id(new PhabricatorPasteTransaction())
-        ->setTransactionType(PhabricatorTransactions::TYPE_SPACE)
-        ->setNewValue($v_space);
-      $xactions[] = id(new PhabricatorPasteTransaction())
-        ->setTransactionType(PhabricatorPasteTransaction::TYPE_STATUS)
-        ->setNewValue($v_status);
-
-      $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
-      $xactions[] = id(new PhabricatorPasteTransaction())
-        ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
-        ->setMetadataValue('edge:type', $proj_edge_type)
-        ->setNewValue(array('=' => array_fuse($v_projects)));
-
-      $editor = id(new PhabricatorPasteEditor())
-        ->setActor($viewer)
-        ->setContentSourceFromRequest($request)
-        ->setContinueOnNoEffect(true);
-
-      try {
-        $xactions = $editor->applyTransactions($paste, $xactions);
-        return id(new AphrontRedirectResponse())->setURI($paste->getURI());
-      } catch (PhabricatorApplicationTransactionValidationException $ex) {
-        $validation_exception = $ex;
-      }
-    }
-
-    $form = new AphrontFormView();
-
-    $langs = array(
-      '' => pht('(Detect From Filename in Title)'),
-    ) + PhabricatorEnv::getEnvConfig('pygments.dropdown-choices');
-
-    $form
-      ->setUser($viewer)
-      ->addHiddenInput('parent', $parent_id)
-      ->appendChild(
-        id(new AphrontFormTextControl())
-          ->setLabel(pht('Title'))
-          ->setValue($v_title)
-          ->setName('title'))
-      ->appendChild(
-        id(new AphrontFormSelectControl())
-          ->setLabel(pht('Language'))
-          ->setName('language')
-          ->setValue($v_language)
-          ->setOptions($langs));
-
-    $policies = id(new PhabricatorPolicyQuery())
-      ->setViewer($viewer)
-      ->setObject($paste)
-      ->execute();
-
-    $form->appendChild(
-      id(new AphrontFormPolicyControl())
-        ->setUser($viewer)
-        ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
-        ->setPolicyObject($paste)
-        ->setPolicies($policies)
-        ->setValue($v_view_policy)
-        ->setSpacePHID($v_space)
-        ->setName('can_view'));
-
-    $form->appendChild(
-      id(new AphrontFormPolicyControl())
-        ->setUser($viewer)
-        ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
-        ->setPolicyObject($paste)
-        ->setPolicies($policies)
-        ->setValue($v_edit_policy)
-        ->setName('can_edit'));
-
-    $form->appendChild(
-        id(new AphrontFormSelectControl())
-          ->setLabel(pht('Status'))
-          ->setName('status')
-          ->setValue($v_status)
-          ->setOptions($paste->getStatusNameMap()));
-
-    $form->appendControl(
-      id(new AphrontFormTokenizerControl())
-        ->setLabel(pht('Projects'))
-        ->setName('projects')
-        ->setValue($v_projects)
-        ->setDatasource(new PhabricatorProjectDatasource()));
-
-    $form
-      ->appendChild(
-        id(new AphrontFormTextAreaControl())
-          ->setLabel(pht('Text'))
-          ->setValue($v_text)
-          ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
-          ->setCustomClass('PhabricatorMonospaced')
-          ->setName('text'));
-
-    $submit = new AphrontFormSubmitControl();
-
-    if (!$is_create) {
-      $submit->addCancelButton($paste->getURI());
-      $submit->setValue(pht('Save Paste'));
-      $title = pht('Edit %s', $paste->getFullName());
-      $short = pht('Edit');
-    } else {
-      $submit->setValue(pht('Create Paste'));
-      $title = pht('Create New Paste');
-      $short = pht('Create');
-    }
-
-    $form->appendChild($submit);
-
-    $form_box = id(new PHUIObjectBoxView())
-      ->setHeaderText($title)
-      ->setForm($form);
-
-    if ($validation_exception) {
-      $form_box->setValidationException($validation_exception);
-    }
-
-    $crumbs = $this->buildApplicationCrumbs();
-    if (!$is_create) {
-      $crumbs->addTextCrumb('P'.$paste->getID(), '/P'.$paste->getID());
-    }
-    $crumbs->addTextCrumb($short);
-
-    return $this->buildApplicationPage(
-      array(
-        $crumbs,
-        $form_box,
-      ),
-      array(
-        'title' => $title,
-      ));
+    return id(new PhabricatorPasteEditEngine())
+      ->setController($this)
+      ->buildResponse();
   }
 
 }
diff --git a/src/applications/paste/controller/PhabricatorPasteViewController.php b/src/applications/paste/controller/PhabricatorPasteViewController.php
--- a/src/applications/paste/controller/PhabricatorPasteViewController.php
+++ b/src/applications/paste/controller/PhabricatorPasteViewController.php
@@ -137,9 +137,7 @@
       $paste,
       PhabricatorPolicyCapability::CAN_EDIT);
 
-    $can_fork = $viewer->isLoggedIn();
     $id = $paste->getID();
-    $fork_uri = $this->getApplicationURI('/create/?parent='.$id);
 
     return id(new PhabricatorActionListView())
       ->setUser($viewer)
@@ -154,13 +152,6 @@
           ->setHref($this->getApplicationURI("edit/{$id}/")))
       ->addAction(
         id(new PhabricatorActionView())
-          ->setName(pht('Fork This Paste'))
-          ->setIcon('fa-code-fork')
-          ->setDisabled(!$can_fork)
-          ->setWorkflow(!$can_fork)
-          ->setHref($fork_uri))
-      ->addAction(
-        id(new PhabricatorActionView())
           ->setName(pht('View Raw File'))
           ->setIcon('fa-file-text-o')
           ->setHref($this->getApplicationURI("raw/{$id}/")));
diff --git a/src/applications/paste/editor/PhabricatorPasteEditEngine.php b/src/applications/paste/editor/PhabricatorPasteEditEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/paste/editor/PhabricatorPasteEditEngine.php
@@ -0,0 +1,69 @@
+<?php
+
+final class PhabricatorPasteEditEngine
+  extends PhabricatorApplicationEditEngine {
+
+  protected function newEditableObject() {
+    return PhabricatorPaste::initializeNewPaste($this->getViewer());
+  }
+
+  protected function newObjectQuery() {
+    return id(new PhabricatorPasteQuery())
+      ->needRawContent(true);
+  }
+
+  protected function getObjectCreateTitleText($object) {
+    return pht('Create New Paste');
+  }
+
+  protected function getObjectEditTitleText($object) {
+    return pht('Edit %s %s', $object->getMonogram(), $object->getTitle());
+  }
+
+  protected function getObjectEditShortText($object) {
+    return $object->getMonogram();
+  }
+
+  protected function getObjectCreateShortText($object) {
+    return pht('Create Paste');
+  }
+
+  protected function getObjectViewURI($object) {
+    return '/P'.$object->getID();
+  }
+
+  protected function buildCustomEditFields($object) {
+    $langs = array(
+      '' => pht('(Detect From Filename in Title)'),
+    ) + PhabricatorEnv::getEnvConfig('pygments.dropdown-choices');
+
+    return array(
+      id(new PhabricatorTextEditField())
+        ->setKey('title')
+        ->setLabel(pht('Title'))
+        ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE)
+        ->setValue($object->getTitle()),
+      id(new PhabricatorSelectEditField())
+        ->setKey('language')
+        ->setLabel(pht('Language'))
+        ->setAliases(array('lang'))
+        ->setTransactionType(PhabricatorPasteTransaction::TYPE_LANGUAGE)
+        ->setValue($object->getLanguage())
+        ->setOptions($langs),
+      id(new PhabricatorSelectEditField())
+        ->setKey('status')
+        ->setLabel(pht('Status'))
+        ->setTransactionType(PhabricatorPasteTransaction::TYPE_STATUS)
+        ->setValue($object->getStatus())
+        ->setOptions(PhabricatorPaste::getStatusNameMap()),
+      id(new PhabricatorTextAreaEditField())
+        ->setKey('text')
+        ->setLabel(pht('Text'))
+        ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT)
+        ->setMonospaced(true)
+        ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
+        ->setValue($object->getRawContent()),
+    );
+  }
+
+}
diff --git a/src/applications/paste/editor/PhabricatorPasteEditor.php b/src/applications/paste/editor/PhabricatorPasteEditor.php
--- a/src/applications/paste/editor/PhabricatorPasteEditor.php
+++ b/src/applications/paste/editor/PhabricatorPasteEditor.php
@@ -3,6 +3,8 @@
 final class PhabricatorPasteEditor
   extends PhabricatorApplicationTransactionEditor {
 
+  private $fileName;
+
   public function getEditorApplicationClass() {
     return 'PhabricatorPasteApplication';
   }
@@ -41,6 +43,35 @@
     return $types;
   }
 
+  protected function shouldApplyInitialEffects(
+    PhabricatorLiskDAO $object,
+    array $xactions) {
+    return true;
+  }
+
+  protected function applyInitialEffects(
+    PhabricatorLiskDAO $object,
+    array $xactions) {
+
+    // Find the most user-friendly filename we can by examining the title of
+    // the paste and the pending transactions. We'll use this if we create a
+    // new file to store raw content later.
+
+    $name = $object->getTitle();
+    if (!strlen($name)) {
+      $name = 'paste.raw';
+    }
+
+    $type_title = PhabricatorPasteTransaction::TYPE_TITLE;
+    foreach ($xactions as $xaction) {
+      if ($xaction->getTransactionType() == $type_title) {
+        $name = $xaction->getNewValue();
+      }
+    }
+
+    $this->fileName = $name;
+  }
+
   protected function getCustomTransactionOldValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
@@ -62,11 +93,26 @@
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
-      case PhabricatorPasteTransaction::TYPE_CONTENT:
       case PhabricatorPasteTransaction::TYPE_TITLE:
       case PhabricatorPasteTransaction::TYPE_LANGUAGE:
       case PhabricatorPasteTransaction::TYPE_STATUS:
         return $xaction->getNewValue();
+      case PhabricatorPasteTransaction::TYPE_CONTENT:
+        // If this transaction does not really change the paste content, return
+        // the current file PHID so this transaction no-ops.
+        $new_content = $xaction->getNewValue();
+        $old_content = $object->getRawContent();
+        $file_phid = $object->getFilePHID();
+        if (($new_content === $old_content) && $file_phid) {
+          return $file_phid;
+        }
+
+        $file = self::initializeFileForPaste(
+          $this->getActor(),
+          $this->fileName,
+          $xaction->getNewValue());
+
+        return $file->getPHID();
     }
   }
 
diff --git a/src/applications/paste/mail/PasteCreateMailReceiver.php b/src/applications/paste/mail/PasteCreateMailReceiver.php
--- a/src/applications/paste/mail/PasteCreateMailReceiver.php
+++ b/src/applications/paste/mail/PasteCreateMailReceiver.php
@@ -21,16 +21,11 @@
       $title = pht('Email Paste');
     }
 
-    $file = PhabricatorPasteEditor::initializeFileForPaste(
-      $sender,
-      $title,
-      $mail->getCleanTextBody());
-
     $xactions = array();
 
     $xactions[] = id(new PhabricatorPasteTransaction())
       ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT)
-      ->setNewValue($file->getPHID());
+      ->setNewValue($mail->getCleanTextBody());
 
     $xactions[] = id(new PhabricatorPasteTransaction())
       ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE)
diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php
--- a/src/applications/paste/storage/PhabricatorPaste.php
+++ b/src/applications/paste/storage/PhabricatorPaste.php
@@ -45,7 +45,8 @@
       ->setAuthorPHID($actor->getPHID())
       ->setViewPolicy($view_policy)
       ->setEditPolicy($edit_policy)
-      ->setSpacePHID($actor->getDefaultSpacePHID());
+      ->setSpacePHID($actor->getDefaultSpacePHID())
+      ->attachRawContent(null);
   }
 
   public static function getStatusNameMap() {
diff --git a/src/applications/transactions/editengine/PhabricatorApplicationEditEngine.php b/src/applications/transactions/editengine/PhabricatorApplicationEditEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/transactions/editengine/PhabricatorApplicationEditEngine.php
@@ -0,0 +1,303 @@
+<?php
+
+abstract class PhabricatorApplicationEditEngine extends Phobject {
+
+  private $viewer;
+  private $controller;
+
+  final public function setViewer(PhabricatorUser $viewer) {
+    $this->viewer = $viewer;
+    return $this;
+  }
+
+  final public function getViewer() {
+    return $this->viewer;
+  }
+
+  final public function setController(PhabricatorController $controller) {
+    $this->controller = $controller;
+    $this->setViewer($controller->getViewer());
+    return $this;
+  }
+
+  final public function getController() {
+    return $this->controller;
+  }
+
+  final protected function buildEditFields($object) {
+    $viewer = $this->getViewer();
+    $editor = $object->getApplicationTransactionEditor();
+
+    $types = $editor->getTransactionTypesForObject($object);
+    $types = array_fuse($types);
+
+    $fields = $this->buildCustomEditFields($object);
+
+    if ($object instanceof PhabricatorPolicyInterface) {
+      $policies = id(new PhabricatorPolicyQuery())
+        ->setViewer($viewer)
+        ->setObject($object)
+        ->execute();
+
+      $map = array(
+        PhabricatorTransactions::TYPE_VIEW_POLICY => array(
+          'key' => 'policy.view',
+          'aliases' => array('view'),
+          'capability' => PhabricatorPolicyCapability::CAN_VIEW,
+        ),
+        PhabricatorTransactions::TYPE_EDIT_POLICY => array(
+          'key' => 'policy.edit',
+          'aliases' => array('edit'),
+          'capability' => PhabricatorPolicyCapability::CAN_EDIT,
+        ),
+        PhabricatorTransactions::TYPE_JOIN_POLICY => array(
+          'key' => 'policy.join',
+          'aliases' => array('join'),
+          'capability' => PhabricatorPolicyCapability::CAN_JOIN,
+        ),
+      );
+
+      foreach ($map as $type => $spec) {
+        if (empty($types[$type])) {
+          continue;
+        }
+
+        $capability = $spec['capability'];
+        $key = $spec['key'];
+        $aliases = $spec['aliases'];
+
+        $policy_field = id(new PhabricatorPolicyEditField())
+          ->setKey($key)
+          ->setAliases($aliases)
+          ->setCapability($capability)
+          ->setPolicies($policies)
+          ->setTransactionType($type)
+          ->setValue($object->getPolicy($capability));
+        $fields[] = $policy_field;
+
+        if ($object instanceof PhabricatorSpacesInterface) {
+          if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
+            $type_space = PhabricatorTransactions::TYPE_SPACE;
+            if (isset($types[$type_space])) {
+              $space_field = id(new PhabricatorSpaceEditField())
+                ->setKey('spacePHID')
+                ->setAliases(array('space', 'policy.space'))
+                ->setTransactionType($type_space)
+                ->setValue($object->getSpacePHID());
+              $fields[] = $space_field;
+
+              $policy_field->setSpaceField($space_field);
+            }
+          }
+        }
+      }
+    }
+
+    $edge_type = PhabricatorTransactions::TYPE_EDGE;
+    $object_phid = $object->getPHID();
+
+    $project_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
+
+    if ($object instanceof PhabricatorProjectInterface) {
+      if (isset($types[$edge_type])) {
+        if ($object_phid) {
+          $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
+            $object_phid,
+            $project_edge_type);
+          $project_phids = array_reverse($project_phids);
+        } else {
+          $project_phids = array();
+        }
+
+        $edge_field = id(new PhabricatorDatasourceEditField())
+          ->setKey('projectPHIDs')
+          ->setLabel(pht('Projects'))
+          ->setDatasource(new PhabricatorProjectDatasource())
+          ->setAliases(array('project', 'projects'))
+          ->setTransactionType($edge_type)
+          ->setMetadataValue('edge:type', $project_edge_type)
+          ->setValue($project_phids);
+        $fields[] = $edge_field;
+      }
+    }
+
+    $subscribers_type = PhabricatorTransactions::TYPE_SUBSCRIBERS;
+
+    if ($object instanceof PhabricatorSubscribableInterface) {
+      if (isset($types[$subscribers_type])) {
+        if ($object_phid) {
+          $sub_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID(
+            $object_phid);
+        } else {
+          // TODO: Allow applications to provide default subscribers; Maniphest
+          // does this at a minimum.
+          $sub_phids = array();
+        }
+
+        $subscribers_field = id(new PhabricatorDatasourceEditField())
+          ->setKey('subscriberPHIDs')
+          ->setLabel(pht('Subscribers'))
+          ->setDatasource(new PhabricatorMetaMTAMailableDatasource())
+          ->setAliases(array('subscriber', 'subscribers'))
+          ->setTransactionType($subscribers_type)
+          ->setValue($sub_phids);
+        $fields[] = $subscribers_field;
+      }
+    }
+
+    return $fields;
+  }
+
+  abstract protected function newEditableObject();
+  abstract protected function newObjectQuery();
+  abstract protected function buildCustomEditFields($object);
+
+  abstract protected function getObjectCreateTitleText($object);
+  abstract protected function getObjectEditTitleText($object);
+  abstract protected function getObjectCreateShortText($object);
+  abstract protected function getObjectEditShortText($object);
+  abstract protected function getObjectViewURI($object);
+
+  protected function getObjectCreateCancelURI($object) {
+    return $this->getController()->getApplicationURI();
+  }
+
+  protected function getObjectEditCancelURI($object) {
+    return $this->getObjectViewURI($object);
+  }
+
+  protected function getObjectCreateButtonText($object) {
+    return $this->getObjectCreateTitleText($object);
+  }
+
+  protected function getObjectEditButtonText($object) {
+    return pht('Save Changes');
+  }
+
+  final public function buildResponse() {
+    $controller = $this->getController();
+    $viewer = $this->getViewer();
+    $request = $controller->getRequest();
+
+    $id = $request->getURIData('id');
+    if ($id) {
+      $object = $this->newObjectQuery()
+        ->setViewer($viewer)
+        ->withIDs(array($id))
+        ->requireCapabilities(
+          array(
+            PhabricatorPolicyCapability::CAN_VIEW,
+            PhabricatorPolicyCapability::CAN_EDIT,
+          ))
+        ->executeOne();
+      if (!$object) {
+        return new Aphront404Response();
+      }
+
+      $is_create = false;
+    } else {
+      $object = $this->newEditableObject();
+
+      $is_create = true;
+    }
+
+    $fields = $this->buildEditFields($object);
+
+    foreach ($fields as $field) {
+      $field
+        ->setViewer($viewer)
+        ->setObject($object);
+    }
+
+    $validation_exception = null;
+    if ($request->isFormPost()) {
+      foreach ($fields as $field) {
+        $field->readValueFromSubmit($request);
+      }
+
+      $template = $object->getApplicationTransactionTemplate();
+
+      $xactions = array();
+      foreach ($fields as $field) {
+        $xactions[] = $field->generateTransaction(clone $template);
+      }
+
+      $editor = $object->getApplicationTransactionEditor()
+        ->setActor($viewer)
+        ->setContentSourceFromRequest($request)
+        ->setContinueOnNoEffect(true)
+        ->setContinueOnMissingFields(false);
+
+      try {
+
+        $editor->applyTransactions($object, $xactions);
+
+        return id(new AphrontRedirectResponse())
+          ->setURI($this->getObjectViewURI($object));
+      } catch (PhabricatorApplicationTransactionValidationException $ex) {
+        $validation_exception = $ex;
+      }
+    } else {
+      if ($is_create) {
+        foreach ($fields as $field) {
+          $field->readValueFromRequest($request);
+        }
+      } else {
+        foreach ($fields as $field) {
+          $field->readValueFromObject($object);
+        }
+      }
+    }
+
+    $box = id(new PHUIObjectBoxView())
+      ->setUser($viewer);
+
+    $crumbs = $controller->buildApplicationCrumbsForEditEngine();
+
+    if ($is_create) {
+      $header_text = $this->getObjectCreateTitleText($object);
+
+      $crumbs->addTextCrumb(
+        $this->getObjectCreateShortText($object));
+
+      $cancel_uri = $this->getObjectCreateCancelURI($object);
+      $submit_button = $this->getObjectCreateButtonText($object);
+    } else {
+      $header_text = $this->getObjectEditTitleText($object);
+
+      $crumbs->addTextCrumb(
+        $this->getObjectEditShortText($object),
+        $this->getObjectViewURI($object));
+
+      $cancel_uri = $this->getObjectEditCancelURI($object);
+      $submit_button = $this->getObjectEditButtonText($object);
+    }
+
+    $box->setHeaderText($header_text);
+
+    $form = id(new AphrontFormView())
+      ->setUser($viewer);
+
+    foreach ($fields as $field) {
+      $field->appendToForm($form);
+    }
+
+    $form->appendControl(
+      id(new AphrontFormSubmitControl())
+        ->addCancelButton($cancel_uri)
+        ->setValue($submit_button));
+
+    $box->appendChild($form);
+
+    if ($validation_exception) {
+      $box->setValidationException($validation_exception);
+    }
+
+    return $controller->newPage()
+      ->setTitle($header_text)
+      ->setCrumbs($crumbs)
+      ->appendChild($box);
+  }
+
+
+}
diff --git a/src/applications/transactions/editfield/PhabricatorDatasourceEditField.php b/src/applications/transactions/editfield/PhabricatorDatasourceEditField.php
new file mode 100644
--- /dev/null
+++ b/src/applications/transactions/editfield/PhabricatorDatasourceEditField.php
@@ -0,0 +1,21 @@
+<?php
+
+final class PhabricatorDatasourceEditField
+  extends PhabricatorTokenizerEditField {
+
+  private $datasource;
+
+  public function setDatasource(PhabricatorTypeaheadDatasource $datasource) {
+    $this->datasource = $datasource;
+    return $this;
+  }
+
+  public function getDatasource() {
+    return $this->datasource;
+  }
+
+  protected function newDatasource() {
+    return id(clone $this->getDatasource());
+  }
+
+}
diff --git a/src/applications/transactions/editfield/PhabricatorEditField.php b/src/applications/transactions/editfield/PhabricatorEditField.php
new file mode 100644
--- /dev/null
+++ b/src/applications/transactions/editfield/PhabricatorEditField.php
@@ -0,0 +1,212 @@
+<?php
+
+abstract class PhabricatorEditField extends Phobject {
+
+  private $key;
+  private $viewer;
+  private $label;
+  private $aliases = array();
+  private $value;
+  private $hasValue = false;
+  private $object;
+  private $transactionType;
+  private $metadata = array();
+
+  public function setKey($key) {
+    $this->key = $key;
+    return $this;
+  }
+
+  public function getKey() {
+    return $this->key;
+  }
+
+  public function setLabel($label) {
+    $this->label = $label;
+    return $this;
+  }
+
+  public function getLabel() {
+    return $this->label;
+  }
+
+  public function setViewer(PhabricatorUser $viewer) {
+    $this->viewer = $viewer;
+    return $this;
+  }
+
+  public function getViewer() {
+    return $this->viewer;
+  }
+
+  public function setAliases(array $aliases) {
+    $this->aliases = $aliases;
+    return $this;
+  }
+
+  public function getAliases() {
+    return $this->aliases;
+  }
+
+  public function setObject($object) {
+    $this->object = $object;
+    return $this;
+  }
+
+  public function getObject() {
+    return $this->object;
+  }
+
+  abstract protected function newControl();
+
+  protected function renderControl() {
+    $control = $this->newControl();
+    if ($control === null) {
+      return null;
+    }
+
+    $control
+      ->setValue($this->getValueForControl())
+      ->setName($this->getKey());
+
+    if (!$control->getLabel()) {
+      $control->setLabel($this->getLabel());
+    }
+
+    return $control;
+  }
+
+  public function appendToForm(AphrontFormView $form) {
+    $control = $this->renderControl();
+    if ($control !== null) {
+      $form->appendControl($control);
+    }
+    return $this;
+  }
+
+  protected function getValueForControl() {
+    return $this->getValue();
+  }
+
+  protected function getValue() {
+    return $this->value;
+  }
+
+  public function setValue($value) {
+    $this->hasValue = true;
+    $this->value = $value;
+    return $this;
+  }
+
+  public function generateTransaction(
+    PhabricatorApplicationTransaction $xaction) {
+
+    $xaction
+      ->setTransactionType($this->getTransactionType())
+      ->setNewValue($this->getValueForTransaction());
+
+    foreach ($this->metadata as $key => $value) {
+      $xaction->setMetadataValue($key, $value);
+    }
+
+    return $xaction;
+  }
+
+  public function setMetadataValue($key, $value) {
+    $this->metadata[$key] = $value;
+    return $this;
+  }
+
+  protected function getValueForTransaction() {
+    return $this->getValue();
+  }
+
+  public function getTransactionType() {
+    if (!$this->transactionType) {
+      throw new PhutilInvalidStateException('setTransactionType');
+    }
+    return $this->transactionType;
+  }
+
+  public function setTransactionType($type) {
+    $this->transactionType = $type;
+    return $this;
+  }
+
+  public function readValueFromRequest(AphrontRequest $request) {
+    $check = array_merge(array($this->getKey()), $this->getAliases());
+    foreach ($check as $key) {
+      if (!$this->getValueExistsInRequest($request, $key)) {
+        continue;
+      }
+
+      $this->value = $this->getValueFromRequest($request, $key);
+      return;
+    }
+
+    $this->readValueFromObject($this->getObject());
+
+    return $this;
+  }
+
+  public function readValueFromObject($object) {
+    $this->value = $this->getValueFromObject($object);
+    return $this;
+  }
+
+  protected function getValueFromObject($object) {
+    if ($this->hasValue) {
+      return $this->value;
+    } else {
+      return $this->getDefaultValue();
+    }
+  }
+
+  protected function getValueExistsInRequest(AphrontRequest $request, $key) {
+    return $this->getValueExistsInSubmit($request, $key);
+  }
+
+  protected function getValueFromRequest(AphrontRequest $request, $key) {
+    return $this->getValueFromSubmit($request, $key);
+  }
+
+  public function readValueFromSubmit(AphrontRequest $request) {
+    $key = $this->getKey();
+    if ($this->getValueExistsInSubmit($request, $key)) {
+      $value = $this->getValueFromSubmit($request, $key);
+    } else {
+      $value = $this->getDefaultValue();
+    }
+    $this->value = $value;
+    return $this;
+  }
+
+  protected function getValueExistsInSubmit(AphrontRequest $request, $key) {
+    return $request->getExists($key);
+  }
+
+  protected function getValueFromSubmit(AphrontRequest $request, $key) {
+    return $request->getStr($key);
+  }
+
+  protected function getDefaultValue() {
+    return null;
+  }
+
+  protected function getListFromRequest(
+    AphrontRequest $request,
+    $key) {
+
+    $list = $request->getArr($key, null);
+    if ($list === null) {
+      $list = $request->getStrList($key);
+    }
+
+    if (!$list) {
+      return array();
+    }
+
+    return $list;
+  }
+
+}
diff --git a/src/applications/transactions/editfield/PhabricatorPolicyEditField.php b/src/applications/transactions/editfield/PhabricatorPolicyEditField.php
new file mode 100644
--- /dev/null
+++ b/src/applications/transactions/editfield/PhabricatorPolicyEditField.php
@@ -0,0 +1,54 @@
+<?php
+
+final class PhabricatorPolicyEditField
+  extends PhabricatorEditField {
+
+  private $policies;
+  private $capability;
+  private $spaceField;
+
+  public function setPolicies(array $policies) {
+    $this->policies = $policies;
+    return $this;
+  }
+
+  public function getPolicies() {
+    if ($this->policies === null) {
+      throw new PhutilInvalidStateException('setPolicies');
+    }
+    return $this->policies;
+  }
+
+  public function setCapability($capability) {
+    $this->capability = $capability;
+    return $this;
+  }
+
+  public function getCapability() {
+    return $this->capability;
+  }
+
+  public function setSpaceField(PhabricatorSpaceEditField $space_field) {
+    $this->spaceField = $space_field;
+    return $this;
+  }
+
+  public function getSpaceField() {
+    return $this->spaceField;
+  }
+
+  protected function newControl() {
+    $control = id(new AphrontFormPolicyControl())
+      ->setCapability($this->getCapability())
+      ->setPolicyObject($this->getObject())
+      ->setPolicies($this->getPolicies());
+
+    $space_field = $this->getSpaceField();
+    if ($space_field) {
+      $control->setSpacePHID($space_field->getValueForControl());
+    }
+
+    return $control;
+  }
+
+}
diff --git a/src/applications/transactions/editfield/PhabricatorSelectEditField.php b/src/applications/transactions/editfield/PhabricatorSelectEditField.php
new file mode 100644
--- /dev/null
+++ b/src/applications/transactions/editfield/PhabricatorSelectEditField.php
@@ -0,0 +1,25 @@
+<?php
+
+final class PhabricatorSelectEditField
+  extends PhabricatorEditField {
+
+  private $options;
+
+  public function setOptions(array $options) {
+    $this->options = $options;
+    return $this;
+  }
+
+  public function getOptions() {
+    if ($this->options === null) {
+      throw new PhutilInvalidStateException('setOptions');
+    }
+    return $this->options;
+  }
+
+  protected function newControl() {
+    return id(new AphrontFormSelectControl())
+      ->setOptions($this->getOptions());
+  }
+
+}
diff --git a/src/applications/transactions/editfield/PhabricatorSpaceEditField.php b/src/applications/transactions/editfield/PhabricatorSpaceEditField.php
new file mode 100644
--- /dev/null
+++ b/src/applications/transactions/editfield/PhabricatorSpaceEditField.php
@@ -0,0 +1,12 @@
+<?php
+
+final class PhabricatorSpaceEditField
+  extends PhabricatorEditField {
+
+  protected function newControl() {
+    // NOTE: This field doesn't do anything on its own, it just serves as a
+    // companion to the associated View Policy field.
+    return null;
+  }
+
+}
diff --git a/src/applications/transactions/editfield/PhabricatorTextAreaEditField.php b/src/applications/transactions/editfield/PhabricatorTextAreaEditField.php
new file mode 100644
--- /dev/null
+++ b/src/applications/transactions/editfield/PhabricatorTextAreaEditField.php
@@ -0,0 +1,42 @@
+<?php
+
+final class PhabricatorTextAreaEditField
+  extends PhabricatorEditField {
+
+  private $monospaced;
+  private $height;
+
+  public function setMonospaced($monospaced) {
+    $this->monospaced = $monospaced;
+    return $this;
+  }
+
+  public function getMonospaced() {
+    return $this->monospaced;
+  }
+
+  public function setHeight($height) {
+    $this->height = $height;
+    return $this;
+  }
+
+  public function getHeight() {
+    return $this->height;
+  }
+
+  protected function newControl() {
+    $control = new AphrontFormTextAreaControl();
+
+    if ($this->getMonospaced()) {
+      $control->setCustomClass('PhabricatorMonospaced');
+    }
+
+    $height = $this->getHeight();
+    if ($height) {
+      $control->setHeight($height);
+    }
+
+    return $control;
+  }
+
+}
diff --git a/src/applications/transactions/editfield/PhabricatorTextEditField.php b/src/applications/transactions/editfield/PhabricatorTextEditField.php
new file mode 100644
--- /dev/null
+++ b/src/applications/transactions/editfield/PhabricatorTextEditField.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorTextEditField
+  extends PhabricatorEditField {
+
+  protected function newControl() {
+    return new AphrontFormTextControl();
+  }
+
+}
diff --git a/src/applications/transactions/editfield/PhabricatorTokenizerEditField.php b/src/applications/transactions/editfield/PhabricatorTokenizerEditField.php
new file mode 100644
--- /dev/null
+++ b/src/applications/transactions/editfield/PhabricatorTokenizerEditField.php
@@ -0,0 +1,90 @@
+<?php
+
+abstract class PhabricatorTokenizerEditField
+  extends PhabricatorEditField {
+
+  private $originalValue;
+
+  abstract protected function newDatasource();
+
+  protected function newControl() {
+    $control = id(new AphrontFormTokenizerControl())
+      ->setDatasource($this->newDatasource());
+
+    if ($this->originalValue !== null) {
+      $control->setOriginalValue($this->originalValue);
+    }
+
+    return $control;
+  }
+
+  public function setOriginalValue(array $value) {
+    $this->originalValue = $value;
+    return $this;
+  }
+
+  public function setValue($value) {
+    $this->originalValue = $value;
+    return parent::setValue($value);
+  }
+
+  protected function getValueFromSubmit(AphrontRequest $request, $key) {
+    // TODO: Maybe move this unusual read somewhere else so subclassing this
+    // correctly is easier?
+    $this->originalValue = $request->getArr($key.'.original');
+
+    return $this->getListFromRequest($request, $key);
+  }
+
+  protected function getDefaultValue() {
+    return array();
+  }
+
+  protected function getValueForTransaction() {
+    $new = parent::getValueForTransaction();
+
+    $edge_types = array(
+      PhabricatorTransactions::TYPE_EDGE => true,
+      PhabricatorTransactions::TYPE_SUBSCRIBERS => true,
+    );
+
+    if (isset($edge_types[$this->getTransactionType()])) {
+      if ($this->originalValue !== null) {
+        // If we're building an edge transaction and the request has data
+        // about the original value the user saw when they loaded the form,
+        // interpret the edit as a mixture of "+" and "-" operations instead
+        // of a single "=" operation. This limits our exposure to race
+        // conditions by making most concurrent edits merge correctly.
+
+        $new = parent::getValueForTransaction();
+        $old = $this->originalValue;
+
+        $add = array_diff($new, $old);
+        $rem = array_diff($old, $new);
+
+        $value = array();
+
+        if ($add) {
+          $value['+'] = array_fuse($add);
+        }
+        if ($rem) {
+          $value['-'] = array_fuse($rem);
+        }
+
+        return $value;
+      } else {
+
+        if (!is_array($new)) {
+          throw new Exception(print_r($new, true));
+        }
+
+        return array(
+          '=' => array_fuse($new),
+        );
+      }
+    }
+
+    return $new;
+  }
+
+}
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
@@ -242,6 +242,19 @@
     return $this->applicationEmail;
   }
 
+  public function getTransactionTypesForObject($object) {
+    $old = $this->object;
+    try {
+      $this->object = $object;
+      $result = $this->getTransactionTypes();
+      $this->object = $old;
+    } catch (Exception $ex) {
+      $this->object = $old;
+      throw $ex;
+    }
+    return $result;
+  }
+
   public function getTransactionTypes() {
     $types = array();
 
@@ -1650,7 +1663,7 @@
       throw new Exception(
         pht(
           "Invalid '%s' value for PHID transaction. Value should contain only ".
-          "keys '%s' (add PHIDs), '%' (remove PHIDs) and '%s' (set PHIDS).",
+          "keys '%s' (add PHIDs), '%s' (remove PHIDs) and '%s' (set PHIDS).",
           'new',
           '+',
           '-',
diff --git a/src/view/control/AphrontTokenizerTemplateView.php b/src/view/control/AphrontTokenizerTemplateView.php
--- a/src/view/control/AphrontTokenizerTemplateView.php
+++ b/src/view/control/AphrontTokenizerTemplateView.php
@@ -6,6 +6,7 @@
   private $name;
   private $id;
   private $browseURI;
+  private $originalValue;
 
   public function setBrowseURI($browse_uri) {
     $this->browseURI = $browse_uri;
@@ -36,6 +37,15 @@
     return $this->name;
   }
 
+  public function setOriginalValue(array $original_value) {
+    $this->originalValue = $original_value;
+    return $this;
+  }
+
+  public function getOriginalValue() {
+    return $this->originalValue;
+  }
+
   public function render() {
     require_celerity_resource('aphront-tokenizer-control-css');
 
@@ -85,6 +95,20 @@
       $classes[] = 'has-browse';
     }
 
+    $original = array();
+    $original_value = $this->getOriginalValue();
+    if ($original_value) {
+      foreach ($this->getOriginalValue() as $value) {
+        $original[] = phutil_tag(
+          'input',
+          array(
+            'type' => 'hidden',
+            'name' => $name.'.original[]',
+            'value' => $value,
+          ));
+      }
+    }
+
     $frame = javelin_tag(
       'div',
       array(
@@ -94,6 +118,7 @@
       array(
         $container,
         $browse,
+        $original,
       ));
 
     return $frame;
diff --git a/src/view/form/control/AphrontFormTokenizerControl.php b/src/view/form/control/AphrontFormTokenizerControl.php
--- a/src/view/form/control/AphrontFormTokenizerControl.php
+++ b/src/view/form/control/AphrontFormTokenizerControl.php
@@ -7,6 +7,7 @@
   private $limit;
   private $placeholder;
   private $handles;
+  private $originalValue;
 
   public function setDatasource(PhabricatorTypeaheadDatasource $datasource) {
     $this->datasource = $datasource;
@@ -32,6 +33,15 @@
     return $this;
   }
 
+  public function setOriginalValue(array $original_value) {
+    $this->originalValue = $original_value;
+    return $this;
+  }
+
+  public function getOriginalValue() {
+    return $this->originalValue;
+  }
+
   public function willRender() {
     // Load the handles now so we'll get a bulk load later on when we actually
     // render them.
@@ -69,10 +79,15 @@
       $token->setInputName($this->getName());
     }
 
-    $template = new AphrontTokenizerTemplateView();
-    $template->setName($name);
-    $template->setID($id);
-    $template->setValue($tokens);
+    $template = id(new AphrontTokenizerTemplateView())
+      ->setName($name)
+      ->setID($id)
+      ->setValue($tokens);
+
+    $original_value = $this->getOriginalValue();
+    if ($original_value !== null) {
+      $template->setOriginalValue($original_value);
+    }
 
     $username = null;
     if ($this->user) {