diff --git src/wp-admin/admin-ajax.php src/wp-admin/admin-ajax.php
index 15c352de94..4a18fcf714 100644
--- src/wp-admin/admin-ajax.php
+++ src/wp-admin/admin-ajax.php
@@ -64,7 +64,7 @@ $core_actions_post = array(
 	'parse-media-shortcode', 'destroy-sessions', 'install-plugin', 'update-plugin', 'crop-image',
 	'generate-password', 'save-wporg-username', 'delete-plugin', 'search-plugins',
 	'search-install-plugins', 'activate-plugin', 'update-theme', 'delete-theme', 'install-theme',
-	'get-post-thumbnail-html', 'get-community-events',
+	'get-post-thumbnail-html', 'get-community-events', 'edit-theme-plugin-file',
 );
 
 // Deprecated
diff --git src/wp-admin/css/common.css src/wp-admin/css/common.css
index 1469097a8a..d91e8a1368 100644
--- src/wp-admin/css/common.css
+++ src/wp-admin/css/common.css
@@ -2217,14 +2217,16 @@ h1.nav-tab-wrapper, /* Back-compat for pre-4.4 */
 #template > div {
 	margin-right: 190px;
 }
-#template .active-plugin-edit-warning {
+#template .notice {
 	margin-top: 1em;
-	margin-right: 30%;
-	margin-right: calc( 184px + 3% );
+	margin-right: 3%;
 }
-#template .active-plugin-edit-warning p {
+#template .notice p {
 	width: auto;
 }
+#template .submit .spinner {
+	float: none;
+}
 
 .metabox-holder .stuffbox > h3, /* Back-compat for pre-4.4 */
 .metabox-holder .postbox > h3, /* Back-compat for pre-4.4 */
@@ -3032,10 +3034,14 @@ img {
 #template textarea,
 #template .CodeMirror {
 	width: 97%;
-	height: calc( 100vh - 220px );
+	height: calc( 100vh - 280px );
+}
+#templateside {
+	margin-top: 31px;
+	overflow: scroll;
 }
 
-#template label {
+#theme-plugin-editor-label {
 	display: inline-block;
 	margin-bottom: 1em;
 	font-weight: 600;
@@ -3047,6 +3053,14 @@ img {
 	direction: ltr;
 }
 
+.fileedit-sub #theme,
+.fileedit-sub #plugin {
+	max-width: 40%;
+}
+.fileedit-sub .alignright {
+	text-align: right;
+}
+
 #template p {
 	width: 97%;
 }
@@ -3624,7 +3638,7 @@ img {
 	}
 
 	#template > div,
-	#template  .active-plugin-edit-warning {
+	#template .notice {
 		float: none;
 		margin: 1em 0;
 		width: auto;
diff --git src/wp-admin/includes/ajax-actions.php src/wp-admin/includes/ajax-actions.php
index 53fd8671d1..8dbd75c5a3 100644
--- src/wp-admin/includes/ajax-actions.php
+++ src/wp-admin/includes/ajax-actions.php
@@ -3966,3 +3966,26 @@ function wp_ajax_search_install_plugins() {
 
 	wp_send_json_success( $status );
 }
+
+/**
+ * Ajax handler for editing a theme or plugin file.
+ *
+ * @since 4.9.0
+ * @see wp_edit_theme_plugin_file()
+ */
+function wp_ajax_edit_theme_plugin_file() {
+	$r = wp_edit_theme_plugin_file( wp_unslash( $_POST ) ); // Validation of args is done in wp_edit_theme_plugin_file().
+	if ( is_wp_error( $r ) ) {
+		wp_send_json_error( array_merge(
+			array(
+				'code' => $r->get_error_code(),
+				'message' => $r->get_error_message(),
+			),
+			(array) $r->get_error_data()
+		) );
+	} else {
+		wp_send_json_success( array(
+			'message' => __( 'File edited successfully.' ),
+		) );
+	}
+}
diff --git src/wp-admin/includes/file.php src/wp-admin/includes/file.php
index 05bfde46a4..553880a46c 100644
--- src/wp-admin/includes/file.php
+++ src/wp-admin/includes/file.php
@@ -70,7 +70,7 @@ $wp_file_descriptions = array(
  * @since 1.5.0
  *
  * @global array $wp_file_descriptions Theme file descriptions.
- * @global array $allowed_files        List of allowed files. 
+ * @global array $allowed_files        List of allowed files.
  * @param string $file Filesystem path or filename
  * @return string Description of file from $wp_file_descriptions or basename of $file if description doesn't exist.
  *                Appends 'Page Template' to basename of $file if the file is a page template
@@ -152,6 +152,398 @@ function list_files( $folder = '', $levels = 100 ) {
 	return $files;
 }
 
+/**
+ * Get list of file extensions that are editable in plugins.
+ *
+ * @since 4.9.0
+ *
+ * @param string $plugin Plugin.
+ * @return array File extensions.
+ */
+function wp_get_plugin_file_editable_extensions( $plugin ) {
+
+	$editable_extensions = array(
+		'bash',
+		'conf',
+		'css',
+		'diff',
+		'htm',
+		'html',
+		'http',
+		'inc',
+		'include',
+		'js',
+		'json',
+		'jsx',
+		'less',
+		'md',
+		'patch',
+		'php',
+		'php3',
+		'php4',
+		'php5',
+		'php7',
+		'phps',
+		'phtml',
+		'sass',
+		'scss',
+		'sh',
+		'sql',
+		'svg',
+		'text',
+		'txt',
+		'xml',
+		'yaml',
+		'yml',
+	);
+
+	/**
+	 * Filters file type extensions editable in the plugin editor.
+	 *
+	 * @since 2.8.0
+	 * @since 4.9.0 Adds $plugin param.
+	 *
+	 * @param string $plugin Plugin file.
+	 * @param array $editable_extensions An array of editable plugin file extensions.
+	 */
+	$editable_extensions = (array) apply_filters( 'editable_extensions', $editable_extensions, $plugin );
+
+	return $editable_extensions;
+}
+
+/**
+ * Get list of file extensions that are editable for a given theme.
+ *
+ * @param WP_Theme $theme Theme.
+ * @return array File extensions.
+ */
+function wp_get_theme_file_editable_extensions( $theme ) {
+
+	$default_types = array(
+		'bash',
+		'conf',
+		'css',
+		'diff',
+		'htm',
+		'html',
+		'http',
+		'inc',
+		'include',
+		'js',
+		'json',
+		'jsx',
+		'less',
+		'md',
+		'patch',
+		'php',
+		'php3',
+		'php4',
+		'php5',
+		'php7',
+		'phps',
+		'phtml',
+		'sass',
+		'scss',
+		'sh',
+		'sql',
+		'svg',
+		'text',
+		'txt',
+		'xml',
+		'yaml',
+		'yml',
+	);
+
+	/**
+	 * Filters the list of file types allowed for editing in the Theme editor.
+	 *
+	 * @since 4.4.0
+	 *
+	 * @param array    $default_types List of file types. Default types include 'php' and 'css'.
+	 * @param WP_Theme $theme         The current Theme object.
+	 */
+	$file_types = apply_filters( 'wp_theme_editor_filetypes', $default_types, $theme );
+
+	// Ensure that default types are still there.
+	return array_unique( array_merge( $file_types, $default_types ) );
+}
+
+/**
+ * Print file editor templates (for plugins and themes).
+ *
+ * @since 4.9.0
+ */
+function wp_print_file_editor_templates() {
+	?>
+	<script type="text/html" id="tmpl-wp-file-editor-notice">
+		<div class="notice inline notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }} {{ data.classes || '' }}">
+			<# if ( 'php_error' === data.code ) { #>
+				<p>
+					<?php
+					printf(
+						/* translators: %$1s is line number and %1$s is file path. */
+						__( 'Your PHP code changes were rolled back due to an error on line %1$s of file %2$s. Please fix and try saving again.' ),
+						'{{ data.line }}',
+						'{{ data.file }}'
+					);
+					?>
+				</p>
+				<pre>{{ data.message }}</pre>
+			<# } else if ( 'file_not_writable' === data.code ) { #>
+				<p><?php _e( 'You need to make this file writable before you can save your changes. See <a href="https://codex.wordpress.org/Changing_File_Permissions">the Codex</a> for more information.' ); ?></p>
+			<# } else { #>
+				<p>{{ data.message || data.code }}</p>
+
+				<# if ( 'lint_errors' === data.code ) { #>
+					<p>
+						<# var elementId = 'el-' + String( Math.random() ); #>
+						<input id="{{ elementId }}"  type="checkbox">
+						<label for="{{ elementId }}"><?php _e( 'Update anyway, even though it might break your site?' ); ?></label>
+					</p>
+				<# } #>
+			<# } #>
+			<# if ( data.dismissible ) { #>
+				<button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php _e( 'Dismiss' ); ?></span></button>
+			<# } #>
+		</div>
+	</script>
+	<?php
+}
+
+/**
+ * Attempt to edit a file for a theme or plugin.
+ *
+ * When editing a PHP file, loopback requests will be made to the admin and the homepage
+ * to attempt to see if there is a fatal error introduced. If so, the PHP change will be
+ * reverted.
+ *
+ * @since 4.9.0
+ *
+ * @param array $args {
+ *     Args. Note that all of the arg values are already unslashed. They are, however,
+ *     coming straight from $_POST and are not validated or sanitized in any way.
+ *
+ *     @type string $file       Relative path to file.
+ *     @type string $plugin     Plugin being edited.
+ *     @type string $theme      Theme being edited.
+ *     @type string $newcontent New content for the file.
+ *     @type string $nonce      Nonce.
+ * }
+ * @return true|WP_Error True on success or `WP_Error` on failure.
+ */
+function wp_edit_theme_plugin_file( $args ) {
+	if ( empty( $args['file'] ) ) {
+		return new WP_Error( 'missing_file' );
+	}
+	$file = $args['file'];
+	if ( 0 !== validate_file( $file ) ) {
+		return new WP_Error( 'bad_file' );
+	}
+
+	if ( ! isset( $args['newcontent'] ) ) {
+		return new WP_Error( 'missing_content' );
+	}
+	$content = $args['newcontent'];
+
+	if ( ! isset( $args['nonce'] ) ) {
+		return new WP_Error( 'missing_nonce' );
+	}
+
+	$plugin = null;
+	$theme = null;
+	$real_file = null;
+	if ( ! empty( $args['plugin'] ) ) {
+		$plugin = $args['plugin'];
+
+		if ( ! current_user_can( 'edit_plugins' ) ) {
+			return new WP_Error( 'unauthorized', __( 'Sorry, you are not allowed to edit plugins for this site.' ) );
+		}
+
+		if ( ! wp_verify_nonce( $args['nonce'], 'edit-plugin_' . $file ) ) {
+			return new WP_Error( 'nonce_failure' );
+		}
+
+		if ( ! array_key_exists( $plugin, get_plugins() ) ) {
+			return new WP_Error( 'invalid_plugin' );
+		}
+
+		if ( 0 !== validate_file( $file, get_plugin_files( $plugin ) ) ) {
+			return new WP_Error( 'bad_plugin_file_path', __( 'Sorry, that file cannot be edited.' ) );
+		}
+
+		$editable_extensions = wp_get_plugin_file_editable_extensions( $plugin );
+
+		$real_file = WP_PLUGIN_DIR . '/' . $file;
+
+		$is_active = in_array(
+			$plugin,
+			(array) get_option( 'active_plugins', array() ),
+			true
+		);
+
+	} elseif ( ! empty( $args['theme'] ) ) {
+		$stylesheet = $args['theme'];
+		if ( 0 !== validate_file( $stylesheet ) ) {
+			return new WP_Error( 'bad_theme_path' );
+		}
+
+		if ( ! current_user_can( 'edit_themes' ) ) {
+			return new WP_Error( 'unauthorized', __( 'Sorry, you are not allowed to edit templates for this site.' ) );
+		}
+
+		$theme = wp_get_theme( $stylesheet );
+		if ( ! $theme->exists() ) {
+			return new WP_Error( 'non_existent_theme', __( 'The requested theme does not exist.' ) );
+		}
+
+		$real_file = $theme->get_stylesheet_directory() . '/' . $file;
+		if ( ! wp_verify_nonce( $args['nonce'], 'edit-theme_' . $real_file . $stylesheet ) ) {
+			return new WP_Error( 'nonce_failure' );
+		}
+
+		if ( $theme->errors() && 'theme_no_stylesheet' === $theme->errors()->get_error_code() ) {
+			return new WP_Error(
+				'theme_no_stylesheet',
+				__( 'The requested theme does not exist.' ) . ' ' . $theme->errors()->get_error_message()
+			);
+		}
+
+		$editable_extensions = wp_get_theme_file_editable_extensions( $theme );
+
+		$allowed_files = array();
+		foreach ( $editable_extensions as $type ) {
+			switch ( $type ) {
+				case 'php':
+					$allowed_files = array_merge( $allowed_files, $theme->get_files( 'php', 1 ) );
+					break;
+				case 'css':
+					$style_files = $theme->get_files( 'css' );
+					$allowed_files['style.css'] = $style_files['style.css'];
+					$allowed_files = array_merge( $allowed_files, $style_files );
+					break;
+				default:
+					$allowed_files = array_merge( $allowed_files, $theme->get_files( $type ) );
+					break;
+			}
+		}
+
+		if ( 0 !== validate_file( $real_file, $allowed_files ) ) {
+			return new WP_Error( 'disallowed_theme_file', __( 'Sorry, that file cannot be edited.' ) );
+		}
+
+		$is_active = ( get_stylesheet() === $stylesheet || get_template() === $stylesheet );
+	} else {
+		return new WP_Error( 'missing_theme_or_plugin' );
+	}
+
+	// Ensure file is real.
+	if ( ! is_file( $real_file ) ) {
+		return new WP_Error( 'file_does_not_exist', __( 'No such file exists! Double check the name and try again.' ) );
+	}
+
+	// Ensure file extension is allowed.
+	$extension = null;
+	if ( preg_match( '/\.([^.]+)$/', $real_file, $matches ) ) {
+		$extension = strtolower( $matches[1] );
+		if ( ! in_array( $extension, $editable_extensions, true ) ) {
+			return new WP_Error( 'illegal_file_type', __( 'Files of this type are not editable.' ) );
+		}
+	}
+
+	$previous_content = file_get_contents( $real_file );
+
+	if ( ! is_writeable( $real_file ) ) {
+		return new WP_Error( 'file_not_writable' );
+	}
+
+	$f = fopen( $real_file, 'w+' );
+	if ( false === $f ) {
+		return new WP_Error( 'file_not_writable' );
+	}
+
+	$written = fwrite( $f, $content );
+	fclose( $f );
+	if ( false === $written ) {
+		return new WP_Error( 'unable_to_write', __( 'Unable to write to file.' ) );
+	}
+	if ( 'php' === $extension && function_exists( 'opcache_invalidate' ) ) {
+		opcache_invalidate( $real_file, true );
+	}
+
+	if ( $is_active && 'php' === $extension ) {
+
+		$scrape_key = md5( rand() );
+		$transient = 'scrape_key_' . $scrape_key;
+		$scrape_nonce = strval( rand() );
+		set_transient( $transient, $scrape_nonce, 60 ); // It shouldn't take more than 60 seconds to make the two loopback requests.
+
+		$cookies = wp_unslash( $_COOKIE );
+		$scrape_params = array(
+			'wp_scrape_key' => $scrape_key,
+			'wp_scrape_nonce' => $scrape_nonce,
+		);
+		$headers = array(
+			'Cache-Control' => 'no-cache',
+		);
+
+		$needle = "###### begin_scraped_error:$scrape_key ######";
+
+		// Attempt loopback request to editor to see if user just whitescreened themselves.
+		if ( $plugin ) {
+			$url = add_query_arg( compact( 'plugin', 'file' ), admin_url( 'plugin-editor.php' ) );
+		} elseif ( isset( $stylesheet ) ) {
+			$url = add_query_arg(
+				array(
+					'theme' => $stylesheet,
+					'file' => $file,
+				),
+				admin_url( 'theme-editor.php' )
+			);
+		} else {
+			$url = admin_url();
+		}
+		$url = add_query_arg( $scrape_params, $url );
+		$r = wp_remote_get( $url, compact( 'cookies', 'headers' ) );
+		$body = wp_remote_retrieve_body( $r );
+		$error_position = strpos( $body, $needle );
+
+		// Try making request to homepage as well to see if visitors have been whitescreened.
+		if ( false === $error_position ) {
+			$url = home_url( '/' );
+			$url = add_query_arg( $scrape_params, $url );
+			$r = wp_remote_get( $url, compact( 'cookies', 'headers' ) );
+			$body = wp_remote_retrieve_body( $r );
+			$error_position = strpos( $body, $needle );
+		}
+
+		delete_transient( $transient );
+
+		if ( false !== $error_position ) {
+			file_put_contents( $real_file, $previous_content );
+			if ( function_exists( 'opcache_invalidate' ) ) {
+				opcache_invalidate( $real_file, true );
+			}
+
+			$error_output = trim( substr( $body, $error_position + strlen( $needle ) ) );
+			$error = json_decode( $error_output, true );
+			if ( ! isset( $error['message'] ) ) {
+				$message = $error_output;
+			} else {
+				$message = $error['message'];
+				unset( $error['message'] );
+			}
+			return new WP_Error( 'php_error', $message, $error );
+		}
+	}
+
+	if ( $theme instanceof WP_Theme ) {
+		$theme->cache_delete();
+	}
+
+	return true;
+}
+
+
 /**
  * Returns a filename of a Temporary unique file.
  * Please note that the calling function must unlink() this itself.
diff --git src/wp-admin/js/theme-plugin-editor.js src/wp-admin/js/theme-plugin-editor.js
index 8e016c3837..3bb0788a6b 100644
--- src/wp-admin/js/theme-plugin-editor.js
+++ src/wp-admin/js/theme-plugin-editor.js
@@ -12,25 +12,227 @@ wp.themePluginEditor = (function( $ ) {
 			lintError: {
 				singular: '',
 				plural: ''
-			}
+			},
+			saveAlert: ''
 		},
-		instance: null
+		codeEditor: {},
+		instance: null,
+		noticeElements: {},
+		dirty: false,
+		lintErrors: []
 	};
 
 	/**
 	 * Initialize component.
 	 *
-	 * @param {object} settings Settings.
+	 * @since 4.9.0
+	 *
+	 * @param {jQuery}         form - Form element.
+	 * @param {object}         settings - Settings.
+	 * @param {object|boolean} settings.codeEditor - Code editor settings (or `false` if syntax highlighting is disabled).
+	 * @returns {void}
+	 */
+	component.init = function init( form, settings ) {
+
+		component.form = form;
+		if ( settings ) {
+			$.extend( component, settings );
+		}
+
+		component.noticeTemplate = wp.template( 'wp-file-editor-notice' );
+		component.noticesContainer = component.form.find( '.editor-notices' );
+		component.submitButton = component.form.find( ':input[name=submit]' );
+		component.spinner = component.form.find( '.submit .spinner' );
+		component.form.on( 'submit', component.submit );
+		component.textarea = component.form.find( '#newcontent' );
+		component.textarea.on( 'change', component.onChange );
+
+		if ( false !== component.codeEditor ) {
+			/*
+			 * Defer adding notices until after DOM ready as workaround for WP Admin injecting
+			 * its own managed dismiss buttons and also to prevent the editor from showing a notice
+			 * when the file had linting errors to begin with.
+			 */
+			_.defer( function() {
+				component.initCodeEditor();
+			} );
+		}
+
+		$( window ).on( 'beforeunload', function() {
+			if ( component.dirty ) {
+				return component.l10n.saveAlert;
+			}
+			return undefined;
+		} );
+	};
+
+	/**
+	 * Callback for when a change happens.
+	 *
+	 * @since 4.9.0
+	 * @returns {void}
+	 */
+	component.onChange = function() {
+		component.dirty = true;
+		component.removeNotice( 'file_saved' );
+	};
+
+	/**
+	 * Submit file via Ajax.
+	 *
+	 * @since 4.9.0
+	 * @param {jQuery.Event} event - Event.
+	 * @returns {void}
+	 */
+	component.submit = function( event ) {
+		var data = {}, request;
+		event.preventDefault(); // Prevent form submission in favor of Ajax below.
+		$.each( component.form.serializeArray(), function() {
+			data[ this.name ] = this.value;
+		} );
+
+		// Use value from codemirror if present.
+		if ( component.instance ) {
+			data.newcontent = component.instance.codemirror.getValue();
+		}
+
+		if ( component.isSaving ) {
+			return;
+		}
+
+		// Scroll ot the line that has the error.
+		if ( component.lintErrors.length ) {
+			component.instance.codemirror.setCursor( component.lintErrors[0].from.line );
+			return;
+		}
+
+		component.isSaving = true;
+		component.textarea.prop( 'readonly', true );
+		if ( component.instance ) {
+			component.instance.codemirror.setOption( 'readOnly', true );
+		}
+
+		component.spinner.addClass( 'is-active' );
+		request = wp.ajax.post( 'edit-theme-plugin-file', data );
+
+		// Remove previous save notice before saving.
+		if ( component.lastSaveNoticeCode ) {
+			component.removeNotice( component.lastSaveNoticeCode );
+		}
+
+		request.done( function ( response ) {
+			component.lastSaveNoticeCode = 'file_saved';
+			component.addNotice({
+				code: component.lastSaveNoticeCode,
+				type: 'success',
+				message: response.message,
+				dismissible: true
+			});
+			component.dirty = false;
+		} );
+
+		request.fail( function ( response ) {
+			var notice = $.extend(
+				{
+					code: 'save_error'
+				},
+				response,
+				{
+					type: 'error',
+					dismissible: true
+				}
+			);
+			component.lastSaveNoticeCode = notice.code;
+			component.addNotice( notice );
+		} );
+
+		request.always( function() {
+			component.spinner.removeClass( 'is-active' );
+			component.isSaving = false;
+
+			component.textarea.prop( 'readonly', false );
+			if ( component.instance ) {
+				component.instance.codemirror.setOption( 'readOnly', false );
+			}
+		} );
+	};
+
+	/**
+	 * Add notice.
+	 *
+	 * @since 4.9.0
+	 *
+	 * @param {object}   notice - Notice.
+	 * @param {string}   notice.code - Code.
+	 * @param {string}   notice.type - Type.
+	 * @param {string}   notice.message - Message.
+	 * @param {boolean}  [notice.dismissible=false] - Dismissible.
+	 * @param {Function} [notice.onDismiss] - Callback for when a user dismisses the notice.
+	 * @returns {jQuery} Notice element.
+	 */
+	component.addNotice = function( notice ) {
+		var noticeElement;
+
+		if ( ! notice.code ) {
+			throw new Error( 'Missing code.' );
+		}
+
+		// Only let one notice of a given type be displayed at a time.
+		component.removeNotice( notice.code );
+
+		noticeElement = $( component.noticeTemplate( notice ) );
+		noticeElement.hide();
+
+		noticeElement.find( '.notice-dismiss' ).on( 'click', function() {
+			component.removeNotice( notice.code );
+			if ( notice.onDismiss ) {
+				notice.onDismiss( notice );
+			}
+		} );
+
+		wp.a11y.speak( notice.message );
+
+		component.noticesContainer.append( noticeElement );
+		noticeElement.slideDown( 'fast' );
+		component.noticeElements[ notice.code ] = noticeElement;
+		return noticeElement;
+	};
+
+	/**
+	 * Remove notice.
+	 *
+	 * @since 4.9.0
+	 *
+	 * @param {string} code - Notice code.
+	 * @returns {boolean} Whether a notice was removed.
+	 */
+	component.removeNotice = function( code ) {
+		if ( component.noticeElements[ code ] ) {
+			component.noticeElements[ code ].slideUp( 'fast', function() {
+				$( this ).remove();
+			} );
+			delete component.noticeElements[ code ];
+			return true;
+		}
+		return false;
+	};
+
+	/**
+	 * Initialize code editor.
+	 *
+	 * @since 4.9.0
 	 * @returns {void}
 	 */
-	component.init = function( settings ) {
-		var codeEditorSettings, noticeContainer, errorNotice = [], editor;
+	component.initCodeEditor = function initCodeEditor() {
+		var codeEditorSettings, editor;
 
-		codeEditorSettings = $.extend( {}, settings );
+		codeEditorSettings = $.extend( {}, component.codeEditor );
 
 		/**
 		 * Handle tabbing to the field before the editor.
 		 *
+		 * @since 4.9.0
+		 *
 		 * @returns {void}
 		 */
 		codeEditorSettings.onTabPrevious = function() {
@@ -40,48 +242,67 @@ wp.themePluginEditor = (function( $ ) {
 		/**
 		 * Handle tabbing to the field after the editor.
 		 *
+		 * @since 4.9.0
+		 *
 		 * @returns {void}
 		 */
 		codeEditorSettings.onTabNext = function() {
 			$( '#template' ).find( ':tabbable:not(.CodeMirror-code)' ).first().focus();
 		};
 
-		// Create the error notice container.
-		noticeContainer = $( '<div id="file-editor-linting-error"></div>' );
-		errorNotice = $( '<div class="inline notice notice-error"></div>' );
-		noticeContainer.append( errorNotice );
-		noticeContainer.hide();
-		$( 'p.submit' ).before( noticeContainer );
+		/**
+		 * Handle change to the linting errors.
+		 *
+		 * @since 4.9.0
+		 *
+		 * @param {Array} errors - List of linting errors.
+		 * @returns {void}
+		 */
+		codeEditorSettings.onChangeLintingErrors = function( errors ) {
+			component.lintErrors = errors;
+
+			// Only disable the button in onUpdateErrorNotice when there are errors so users can still feel they can click the button.
+			if ( 0 === errors.length ) {
+				component.submitButton.toggleClass( 'disabled', false );
+			}
+		};
 
 		/**
 		 * Update error notice.
 		 *
+		 * @since 4.9.0
+		 *
 		 * @param {Array} errorAnnotations - Error annotations.
 		 * @returns {void}
 		 */
 		codeEditorSettings.onUpdateErrorNotice = function onUpdateErrorNotice( errorAnnotations ) {
-			var message;
+			var message, noticeElement;
 
-			$( '#submit' ).prop( 'disabled', 0 !== errorAnnotations.length );
+			component.submitButton.toggleClass( 'disabled', errorAnnotations.length > 0 );
 
 			if ( 0 !== errorAnnotations.length ) {
-				errorNotice.empty();
 				if ( 1 === errorAnnotations.length ) {
-					message = component.l10n.singular.replace( '%d', '1' );
+					message = component.l10n.lintError.singular.replace( '%d', '1' );
 				} else {
-					message = component.l10n.plural.replace( '%d', String( errorAnnotations.length ) );
+					message = component.l10n.lintError.plural.replace( '%d', String( errorAnnotations.length ) );
 				}
-				errorNotice.append( $( '<p></p>', {
-					text: message
-				} ) );
-				noticeContainer.slideDown( 'fast' );
-				wp.a11y.speak( message );
+				noticeElement = component.addNotice({
+					code: 'lint_errors',
+					type: 'error',
+					message: message,
+					dismissible: false
+				});
+				noticeElement.find( 'input[type=checkbox]' ).on( 'click', function() {
+					codeEditorSettings.onChangeLintingErrors( [] );
+					component.removeNotice( 'lint_errors' );
+				} );
 			} else {
-				noticeContainer.slideUp( 'fast' );
+				component.removeNotice( 'lint_errors' );
 			}
 		};
 
 		editor = wp.codeEditor.initialize( $( '#newcontent' ), codeEditorSettings );
+		editor.codemirror.on( 'change', component.onChange );
 
 		// Improve the editor accessibility.
 		$( editor.codemirror.display.lineDiv )
diff --git src/wp-admin/plugin-editor.php src/wp-admin/plugin-editor.php
index bc21642153..636e1cf739 100644
--- src/wp-admin/plugin-editor.php
+++ src/wp-admin/plugin-editor.php
@@ -68,113 +68,38 @@ if ( empty( $plugin ) ) {
 
 $plugin_files = get_plugin_files($plugin);
 
-if ( empty($file) )
+if ( empty( $file ) ) {
 	$file = $plugin_files[0];
+}
 
 $file = validate_file_to_edit($file, $plugin_files);
 $real_file = WP_PLUGIN_DIR . '/' . $file;
-$scrollto = isset($_REQUEST['scrollto']) ? (int) $_REQUEST['scrollto'] : 0;
-
-if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) {
-
-	check_admin_referer('edit-plugin_' . $file);
-
-	$newcontent = wp_unslash( $_POST['newcontent'] );
-	if ( is_writeable($real_file) ) {
-		$f = fopen($real_file, 'w+');
-		fwrite($f, $newcontent);
-		fclose($f);
-
-		if ( preg_match( '/\.php$/', $real_file ) && function_exists( 'opcache_invalidate' ) ) {
-			opcache_invalidate( $real_file, true );
-		}
 
-		$network_wide = is_plugin_active_for_network( $file );
-
-		// Deactivate so we can test it.
-		if ( is_plugin_active( $plugin ) || isset( $_POST['phperror'] ) ) {
-			if ( is_plugin_active( $plugin ) ) {
-				deactivate_plugins( $plugin, true );
-			}
-
-			if ( ! is_network_admin() ) {
-				update_option( 'recently_activated', array( $file => time() ) + (array) get_option( 'recently_activated' ) );
-			} else {
-				update_site_option( 'recently_activated', array( $file => time() ) + (array) get_site_option( 'recently_activated' ) );
-			}
-
-			wp_redirect( add_query_arg( '_wpnonce', wp_create_nonce( 'edit-plugin-test_' . $file ), "plugin-editor.php?file=$file&plugin=$plugin&liveupdate=1&scrollto=$scrollto&networkwide=" . $network_wide ) );
-			exit;
+// Handle fallback editing of file when JavaScript is not available.
+$edit_error = null;
+$posted_content = null;
+if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) {
+	$r = wp_edit_theme_plugin_file( wp_unslash( $_POST ) );
+	if ( is_wp_error( $r ) ) {
+		$edit_error = $r;
+		if ( check_ajax_referer( 'edit-plugin_' . $file, 'nonce', false ) && isset( $_POST['newcontent'] ) ) {
+			$posted_content = wp_unslash( $_POST['newcontent'] );
 		}
-		wp_redirect( self_admin_url( "plugin-editor.php?file=$file&plugin=$plugin&a=te&scrollto=$scrollto" ) );
 	} else {
-		wp_redirect( self_admin_url( "plugin-editor.php?file=$file&plugin=$plugin&scrollto=$scrollto" ) );
-	}
-	exit;
-
-} else {
-
-	if ( isset($_GET['liveupdate']) ) {
-		check_admin_referer('edit-plugin-test_' . $file);
-
-		$error = validate_plugin( $plugin );
-
-		if ( is_wp_error( $error ) ) {
-			wp_die( $error );
-		}
-
-		if ( ( ! empty( $_GET['networkwide'] ) && ! is_plugin_active_for_network( $file ) ) || ! is_plugin_active( $file ) ) {
-			activate_plugin( $plugin, "plugin-editor.php?file=" . urlencode( $file ) . "&phperror=1", ! empty( $_GET['networkwide'] ) );
-		} // we'll override this later if the plugin can be included without fatal error
-
-		wp_redirect( self_admin_url( 'plugin-editor.php?file=' . urlencode( $file ) . '&plugin=' . urlencode( $plugin ) . "&a=te&scrollto=$scrollto" ) );
+		wp_redirect( add_query_arg(
+			array(
+				'a' => 1, // This means "success" for some reason.
+				'plugin' => $plugin,
+				'file' => $file,
+			),
+			admin_url( 'plugin-editor.php' )
+		) );
 		exit;
 	}
+}
 
 	// List of allowable extensions
-	$editable_extensions = array(
-		'bash',
-		'conf',
-		'css',
-		'diff',
-		'htm',
-		'html',
-		'http',
-		'inc',
-		'include',
-		'js',
-		'json',
-		'jsx',
-		'less',
-		'md',
-		'patch',
-		'php',
-		'php3',
-		'php4',
-		'php5',
-		'php7',
-		'phps',
-		'phtml',
-		'sass',
-		'scss',
-		'sh',
-		'sql',
-		'svg',
-		'text',
-		'txt',
-		'xml',
-		'yaml',
-		'yml',
-	);
-
-	/**
-	 * Filters file type extensions editable in the plugin editor.
-	 *
-	 * @since 2.8.0
-	 *
-	 * @param array $editable_extensions An array of editable plugin file extensions.
-	 */
-	$editable_extensions = (array) apply_filters( 'editable_extensions', $editable_extensions );
+	$editable_extensions = wp_get_plugin_file_editable_extensions( $plugin );
 
 	if ( ! is_file($real_file) ) {
 		wp_die(sprintf('<p>%s</p>', __('No such file exists! Double check the name and try again.')));
@@ -212,17 +137,21 @@ if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) {
 		'<p>' . __('<a href="https://wordpress.org/support/">Support Forums</a>') . '</p>'
 	);
 
-	$settings = wp_enqueue_code_editor( array( 'file' => $real_file ) );
-	if ( ! empty( $settings ) ) {
-		wp_enqueue_script( 'wp-theme-plugin-editor' );
-		wp_add_inline_script( 'wp-theme-plugin-editor', sprintf( 'jQuery( function() { wp.themePluginEditor.init( %s ); } )', wp_json_encode( $settings ) ) );
-	}
+	$settings = array(
+		'codeEditor' => wp_enqueue_code_editor( array( 'file' => $real_file ) ),
+	);
+	wp_enqueue_script( 'wp-theme-plugin-editor' );
+	wp_add_inline_script( 'wp-theme-plugin-editor', sprintf( 'jQuery( function( $ ) { wp.themePluginEditor.init( $( "#template" ), %s ); } )', wp_json_encode( $settings ) ) );
 
 	require_once(ABSPATH . 'wp-admin/admin-header.php');
 
 	update_recently_edited(WP_PLUGIN_DIR . '/' . $file);
 
-	$content = file_get_contents( $real_file );
+	if ( ! empty( $posted_content ) ) {
+		$content = $posted_content;
+	} else {
+		$content = file_get_contents( $real_file );
+	}
 
 	if ( '.php' == substr( $real_file, strrpos( $real_file, '.' ) ) ) {
 		$functions = wp_doc_link_parse( $content );
@@ -239,25 +168,20 @@ if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) {
 
 	$content = esc_textarea( $content );
 	?>
-<?php if (isset($_GET['a'])) : ?>
- <div id="message" class="updated notice is-dismissible"><p><?php _e('File edited successfully.') ?></p></div>
-<?php elseif (isset($_GET['phperror'])) : ?>
- <div id="message" class="notice notice-error"><p><?php _e( 'This plugin has been deactivated because your changes resulted in a <strong>fatal error</strong>.' ); ?></p>
-	<?php
-		if ( wp_verify_nonce( $_GET['_error_nonce'], 'plugin-activation-error_' . $plugin ) ) {
-			$iframe_url = add_query_arg( array(
-				'action'   => 'error_scrape',
-				'plugin'   => urlencode( $plugin ),
-				'_wpnonce' => urlencode( $_GET['_error_nonce'] ),
-			), admin_url( 'plugins.php' ) );
-			?>
-	<iframe style="border:0" width="100%" height="70px" src="<?php echo esc_url( $iframe_url ); ?>"></iframe>
-	<?php } ?>
-</div>
-<?php endif; ?>
 <div class="wrap">
 <h1><?php echo esc_html( $title ); ?></h1>
 
+<?php if ( isset( $_GET['a'] ) ) : ?>
+	<div id="message" class="updated notice is-dismissible">
+		<p><?php _e( 'File edited successfully.' ); ?></p>
+	</div>
+<?php elseif ( is_wp_error( $edit_error ) ) : ?>
+	<div id="message" class="notice notice-error">
+		<p><?php _e( 'There was an error while trying to update the file. You may need to fix something and try updating again.' ); ?></p>
+		<pre><?php echo esc_html( $edit_error->get_error_message() ? $edit_error->get_error_message() : $edit_error->get_error_code() ); ?></pre>
+	</div>
+<?php endif; ?>
+
 <div class="fileedit-sub">
 <div class="alignleft">
 <h2>
@@ -283,7 +207,7 @@ if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) {
 </h2>
 </div>
 <div class="alignright">
-	<form action="plugin-editor.php" method="post">
+	<form action="plugin-editor.php" method="get">
 		<strong><label for="plugin"><?php _e('Select plugin to edit:'); ?> </label></strong>
 		<select name="plugin" id="plugin">
 <?php
@@ -308,66 +232,53 @@ if ( isset( $_REQUEST['action'] ) && 'update' === $_REQUEST['action'] ) {
 <div id="templateside">
 	<h2><?php _e( 'Plugin Files' ); ?></h2>
 
-	<ul>
-<?php
-foreach ( $plugin_files as $plugin_file ) :
-	// Get the extension of the file
-	if ( preg_match('/\.([^.]+)$/', $plugin_file, $matches) ) {
-		$ext = strtolower($matches[1]);
-		// If extension is not in the acceptable list, skip it
-		if ( !in_array( $ext, $editable_extensions ) )
-			continue;
-	} else {
-		// No extension found
-		continue;
+	<?php
+	$plugin_editable_files = array();
+	foreach ( $plugin_files as $plugin_file ) {
+		if ( preg_match('/\.([^.]+)$/', $plugin_file, $matches ) && in_array( $matches[1], $editable_extensions ) ) {
+			$plugin_editable_files[] = $plugin_file;
+		}
 	}
 	?>
-	<li class="<?php echo esc_attr( $file === $plugin_file ? 'notice notice-info' : '' ); ?>"><a href="plugin-editor.php?file=<?php echo urlencode( $plugin_file ); ?>&amp;plugin=<?php echo urlencode( $plugin ); ?>"><?php echo esc_html( $plugin_file ); ?></a></li>
-<?php endforeach; ?>
+	<ul>
+	<?php foreach ( $plugin_editable_files as $plugin_file ) : ?>
+		<li class="<?php echo esc_attr( $file === $plugin_file ? 'notice notice-info' : '' ); ?>">
+			<a href="plugin-editor.php?file=<?php echo urlencode( $plugin_file ); ?>&amp;plugin=<?php echo urlencode( $plugin ); ?>"><?php echo esc_html( preg_replace( '#^.+?/#', '', $plugin_file ) ); ?></a>
+		</li>
+	<?php endforeach; ?>
 	</ul>
 </div>
 <form name="template" id="template" action="plugin-editor.php" method="post">
-	<?php wp_nonce_field('edit-plugin_' . $file) ?>
+	<?php wp_nonce_field( 'edit-plugin_' . $file, 'nonce' ); ?>
 		<div>
 			<label for="newcontent" id="theme-plugin-editor-label"><?php _e( 'Selected file content:' ); ?></label>
 			<textarea cols="70" rows="25" name="newcontent" id="newcontent" aria-describedby="editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4"><?php echo $content; ?></textarea>
 			<input type="hidden" name="action" value="update" />
 			<input type="hidden" name="file" value="<?php echo esc_attr( $file ); ?>" />
 			<input type="hidden" name="plugin" value="<?php echo esc_attr( $plugin ); ?>" />
-			<input type="hidden" name="scrollto" id="scrollto" value="<?php echo esc_attr( $scrollto ); ?>" />
 		</div>
 		<?php if ( !empty( $docs_select ) ) : ?>
 		<div id="documentation" class="hide-if-no-js"><label for="docs-list"><?php _e('Documentation:') ?></label> <?php echo $docs_select ?> <input type="button" class="button" value="<?php esc_attr_e( 'Look Up' ) ?> " onclick="if ( '' != jQuery('#docs-list').val() ) { window.open( 'https://api.wordpress.org/core/handbook/1.0/?function=' + escape( jQuery( '#docs-list' ).val() ) + '&amp;locale=<?php echo urlencode( get_user_locale() ) ?>&amp;version=<?php echo urlencode( get_bloginfo( 'version' ) ) ?>&amp;redirect=true'); }" /></div>
 		<?php endif; ?>
 <?php if ( is_writeable($real_file) ) : ?>
-	<?php if ( in_array( $plugin, (array) get_option( 'active_plugins', array() ) ) ) { ?>
-		<div class="notice notice-warning inline active-plugin-edit-warning">
-			<p><?php _e('<strong>Warning:</strong> Making changes to active plugins is not recommended. If your changes cause a fatal error, the plugin will be automatically deactivated.'); ?></p>
+	<div class="editor-notices">
+		<?php if ( in_array( $plugin, (array) get_option( 'active_plugins', array() ) ) ) { ?>
+			<div class="notice notice-warning inline active-plugin-edit-warning">
+			<p><?php _e('<strong>Warning:</strong> Making changes to active plugins is not recommended.'); ?></p>
 		</div>
-	<?php } ?>
+		<?php } ?>
+	</div>
 	<p class="submit">
-	<?php
-		if ( isset($_GET['phperror']) ) {
-			echo "<input type='hidden' name='phperror' value='1' />";
-			submit_button( __( 'Update File and Attempt to Reactivate' ), 'primary', 'submit', false );
-		} else {
-			submit_button( __( 'Update File' ), 'primary', 'submit', false );
-		}
-	?>
+		<?php submit_button( __( 'Update File' ), 'primary', 'submit', false ); ?>
+		<span class="spinner"></span>
 	</p>
 <?php else : ?>
 	<p><em><?php _e('You need to make this file writable before you can save your changes. See <a href="https://codex.wordpress.org/Changing_File_Permissions">the Codex</a> for more information.'); ?></em></p>
 <?php endif; ?>
+<?php wp_print_file_editor_templates(); ?>
 </form>
 <br class="clear" />
 </div>
-<script type="text/javascript">
-jQuery(document).ready(function($){
-	$('#template').submit(function(){ $('#scrollto').val( $('#newcontent').scrollTop() ); });
-	$('#newcontent').scrollTop( $('#scrollto').val() );
-});
-</script>
 <?php
-}
 
 include(ABSPATH . "wp-admin/admin-footer.php");
diff --git src/wp-admin/theme-editor.php src/wp-admin/theme-editor.php
index 2a593dee64..b49013ffd8 100644
--- src/wp-admin/theme-editor.php
+++ src/wp-admin/theme-editor.php
@@ -69,53 +69,8 @@ if ( $theme->errors() && 'theme_no_stylesheet' == $theme->errors()->get_error_co
 
 $allowed_files = $style_files = array();
 $has_templates = false;
-$default_types = array(
-	'bash',
-	'conf',
-	'css',
-	'diff',
-	'htm',
-	'html',
-	'http',
-	'inc',
-	'include',
-	'js',
-	'json',
-	'jsx',
-	'less',
-	'md',
-	'patch',
-	'php',
-	'php3',
-	'php4',
-	'php5',
-	'php7',
-	'phps',
-	'phtml',
-	'sass',
-	'scss',
-	'sh',
-	'sql',
-	'svg',
-	'text',
-	'txt',
-	'xml',
-	'yaml',
-	'yml',
-);
 
-/**
- * Filters the list of file types allowed for editing in the Theme editor.
- *
- * @since 4.4.0
- *
- * @param array    $default_types List of file types. Default types include 'php' and 'css'.
- * @param WP_Theme $theme         The current Theme object.
- */
-$file_types = apply_filters( 'wp_theme_editor_filetypes', $default_types, $theme );
-
-// Ensure that default types are still there.
-$file_types = array_unique( array_merge( $file_types, $default_types ) );
+$file_types = wp_get_theme_file_editable_extensions( $theme );
 
 foreach ( $file_types as $type ) {
 	switch ( $type ) {
@@ -143,33 +98,35 @@ if ( empty( $file ) ) {
 }
 
 validate_file_to_edit( $file, $allowed_files );
-$scrollto = isset( $_REQUEST['scrollto'] ) ? (int) $_REQUEST['scrollto'] : 0;
-
-switch( $action ) {
-case 'update':
-	check_admin_referer( 'edit-theme_' . $file . $stylesheet );
-	$newcontent = wp_unslash( $_POST['newcontent'] );
-	$location = 'theme-editor.php?file=' . urlencode( $relative_file ) . '&theme=' . urlencode( $stylesheet ) . '&scrollto=' . $scrollto;
-	if ( is_writeable( $file ) ) {
-		// is_writable() not always reliable, check return value. see comments @ https://secure.php.net/is_writable
-		$f = fopen( $file, 'w+' );
-		if ( $f !== false ) {
-			fwrite( $f, $newcontent );
-			fclose( $f );
-			$location .= '&updated=true';
-			$theme->cache_delete();
+
+// Handle fallback editing of file when JavaScript is not available.
+$edit_error = null;
+$posted_content = null;
+if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) {
+	$r = wp_edit_theme_plugin_file( wp_unslash( $_POST ) );
+	if ( is_wp_error( $r ) ) {
+		$edit_error = $r;
+		if ( check_ajax_referer( 'edit-theme_' . $file . $stylesheet, 'nonce', false ) && isset( $_POST['newcontent'] ) ) {
+			$posted_content = wp_unslash( $_POST['newcontent'] );
 		}
+	} else {
+		wp_redirect( add_query_arg(
+			array(
+				'a' => 1, // This means "success" for some reason.
+				'theme' => $stylesheet,
+				'file' => $relative_file,
+			),
+			admin_url( 'theme-editor.php' )
+		) );
+		exit;
 	}
-	wp_redirect( $location );
-	exit;
-
-default:
+}
 
-	$settings = wp_enqueue_code_editor( compact( 'file' ) );
-	if ( ! empty( $settings ) ) {
-		wp_enqueue_script( 'wp-theme-plugin-editor' );
-		wp_add_inline_script( 'wp-theme-plugin-editor', sprintf( 'jQuery( function() { wp.themePluginEditor.init( %s ); } )', wp_json_encode( $settings ) ) );
-	}
+	$settings = array(
+		'codeEditor' => wp_enqueue_code_editor( compact( 'file' ) ),
+	);
+	wp_enqueue_script( 'wp-theme-plugin-editor' );
+	wp_add_inline_script( 'wp-theme-plugin-editor', sprintf( 'jQuery( function( $ ) { wp.themePluginEditor.init( $( "#template" ), %s ); } )', wp_json_encode( $settings ) ) );
 
 	require_once( ABSPATH . 'wp-admin/admin-header.php' );
 
@@ -179,7 +136,9 @@ default:
 		$error = true;
 
 	$content = '';
-	if ( ! $error && filesize( $file ) > 0 ) {
+	if ( ! empty( $posted_content ) ) {
+		$content = $posted_content;
+	} elseif ( ! $error && filesize( $file ) > 0 ) {
 		$f = fopen($file, 'r');
 		$content = fread($f, filesize($file));
 
@@ -197,10 +156,6 @@ default:
 		$content = esc_textarea( $content );
 	}
 
-	if ( isset( $_GET['updated'] ) ) : ?>
- <div id="message" class="updated notice is-dismissible"><p><?php _e( 'File edited successfully.' ) ?></p></div>
-<?php endif;
-
 $file_description = get_file_description( $relative_file );
 $file_show = array_search( $file, array_filter( $allowed_files ) );
 $description = esc_html( $file_description );
@@ -211,12 +166,23 @@ if ( $file_description != $file_show ) {
 <div class="wrap">
 <h1><?php echo esc_html( $title ); ?></h1>
 
+<?php if ( isset( $_GET['a'] ) ) : ?>
+	<div id="message" class="updated notice is-dismissible">
+		<p><?php _e( 'File edited successfully.' ); ?></p>
+	</div>
+<?php elseif ( is_wp_error( $edit_error ) ) : ?>
+	<div id="message" class="notice notice-error">
+		<p><?php _e( 'There was an error while trying to update the file. You may need to fix something and try updating again.' ); ?></p>
+		<pre><?php echo esc_html( $edit_error->get_error_message() ? $edit_error->get_error_message() : $edit_error->get_error_code() ); ?></pre>
+	</div>
+<?php endif; ?>
+
 <div class="fileedit-sub">
 <div class="alignleft">
 <h2><?php echo $theme->display( 'Name' ); if ( $description ) echo ': ' . $description; ?></h2>
 </div>
 <div class="alignright">
-	<form action="theme-editor.php" method="post">
+	<form action="theme-editor.php" method="get">
 		<strong><label for="theme"><?php _e('Select theme to edit:'); ?> </label></strong>
 		<select name="theme" id="theme">
 <?php
@@ -299,14 +265,13 @@ if ( $allowed_files ) :
 	echo '<div class="error"><p>' . __('Oops, no such file exists! Double check the name and try again, merci.') . '</p></div>';
 else : ?>
 	<form name="template" id="template" action="theme-editor.php" method="post">
-	<?php wp_nonce_field( 'edit-theme_' . $file . $stylesheet ); ?>
+		<?php wp_nonce_field( 'edit-theme_' . $file . $stylesheet, 'nonce' ); ?>
 		<div>
 			<label for="newcontent" id="theme-plugin-editor-label"><?php _e( 'Selected file content:' ); ?></label>
 			<textarea cols="70" rows="30" name="newcontent" id="newcontent" aria-describedby="editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4"><?php echo $content; ?></textarea>
 			<input type="hidden" name="action" value="update" />
 			<input type="hidden" name="file" value="<?php echo esc_attr( $relative_file ); ?>" />
 			<input type="hidden" name="theme" value="<?php echo esc_attr( $theme->get_stylesheet() ); ?>" />
-			<input type="hidden" name="scrollto" id="scrollto" value="<?php echo esc_attr( $scrollto ); ?>" />
 		</div>
 	<?php if ( ! empty( $functions ) ) : ?>
 		<div id="documentation" class="hide-if-no-js">
@@ -316,32 +281,33 @@ else : ?>
 		</div>
 	<?php endif; ?>
 
-		<div>
-		<?php if ( is_child_theme() && $theme->get_stylesheet() == get_template() ) : ?>
-			<p><?php if ( is_writeable( $file ) ) { ?><strong><?php _e( 'Caution:' ); ?></strong><?php } ?>
-			<?php _e( 'This is a file in your current parent theme.' ); ?></p>
-		<?php endif; ?>
-<?php
-	if ( is_writeable( $file ) ) :
-		submit_button( __( 'Update File' ), 'primary', 'submit', true );
-	else : ?>
-<p><em><?php _e('You need to make this file writable before you can save your changes. See <a href="https://codex.wordpress.org/Changing_File_Permissions">the Codex</a> for more information.'); ?></em></p>
-<?php endif; ?>
+	<div>
+		<div class="editor-notices">
+			<?php if ( is_child_theme() && $theme->get_stylesheet() == get_template() ) : ?>
+				<div class="notice notice-warning inline">
+					<p>
+						<?php if ( is_writeable( $file ) ) { ?><strong><?php _e( 'Caution:' ); ?></strong><?php } ?>
+						<?php _e( 'This is a file in your current parent theme.' ); ?>
+					</p>
+				</div>
+			<?php endif; ?>
 		</div>
+	<?php if ( is_writeable( $file ) ) : ?>
+		<p class="submit">
+			<?php submit_button( __( 'Update File' ), 'primary', 'submit', false ); ?>
+			<span class="spinner"></span>
+		</p>
+	<?php else : ?>
+		<p><em><?php _e('You need to make this file writable before you can save your changes. See <a href="https://codex.wordpress.org/Changing_File_Permissions">the Codex</a> for more information.'); ?></em></p>
+	<?php endif; ?>
+	</div>
+	<?php wp_print_file_editor_templates(); ?>
 	</form>
 <?php
 endif; // $error
 ?>
 <br class="clear" />
 </div>
-<script type="text/javascript">
-jQuery(document).ready(function($){
-	$('#template').submit(function(){ $('#scrollto').val( $('#newcontent').scrollTop() ); });
-	$('#newcontent').scrollTop( $('#scrollto').val() );
-});
-</script>
 <?php
-break;
-}
 
 include(ABSPATH . 'wp-admin/admin-footer.php' );
diff --git src/wp-includes/js/wp-a11y.js src/wp-includes/js/wp-a11y.js
index 8639650a8f..18d6db579f 100644
--- src/wp-includes/js/wp-a11y.js
+++ src/wp-includes/js/wp-a11y.js
@@ -14,9 +14,10 @@ window.wp = window.wp || {};
 	 * @since 4.2.0
 	 * @since 4.3.0 Introduced the 'ariaLive' argument.
 	 *
-	 * @param {String} message  The message to be announced by Assistive Technologies.
-	 * @param {String} ariaLive Optional. The politeness level for aria-live. Possible values:
-	 *                          polite or assertive. Default polite.
+	 * @param {String} message    The message to be announced by Assistive Technologies.
+	 * @param {String} [ariaLive] The politeness level for aria-live. Possible values:
+	 *                            polite or assertive. Default polite.
+	 * @returns {void}
 	 */
 	function speak( message, ariaLive ) {
 		// Clear previous messages to allow repeated strings being read out.
diff --git src/wp-includes/load.php src/wp-includes/load.php
index 63a4b0f64c..0dcf31cbe5 100644
--- src/wp-includes/load.php
+++ src/wp-includes/load.php
@@ -1112,3 +1112,46 @@ function wp_is_file_mod_allowed( $context ) {
 	 */
 	return apply_filters( 'file_mod_allowed', ! defined( 'DISALLOW_FILE_MODS' ) || ! DISALLOW_FILE_MODS, $context );
 }
+
+/**
+ * Start scraping edited file errors.
+ *
+ * @since 4.9.0
+ */
+function wp_start_scraping_edited_file_errors() {
+	if ( ! isset( $_REQUEST['wp_scrape_key'] ) || ! isset( $_REQUEST['wp_scrape_nonce'] ) ) {
+		return;
+	}
+	$key = substr( sanitize_key( wp_unslash( $_REQUEST['wp_scrape_key'] ) ), 0, 32 );
+	$nonce = wp_unslash( $_REQUEST['wp_scrape_nonce'] );
+
+	if ( get_transient( 'scrape_key_' . $key ) !== $nonce ) {
+		echo "###### begin_scraped_error:$key ######";
+		echo wp_json_encode( array(
+			'code' => 'scrape_nonce_failure',
+			'message' => __( 'Scrape nonce check failed. Please try again.' ),
+		) );
+		die();
+	}
+	register_shutdown_function( 'wp_finalize_scraping_edited_file_errors', $key );
+}
+
+/**
+ * Finalize scraping for edited file errors.
+ *
+ * @since 4.9.0
+ *
+ * @param string $scrape_key Scrape key.
+ */
+function wp_finalize_scraping_edited_file_errors( $scrape_key ) {
+	$error = error_get_last();
+	if ( empty( $error ) ) {
+		return;
+	}
+	if ( ! in_array( $error['type'], array( E_CORE_ERROR, E_COMPILE_ERROR, E_ERROR, E_PARSE, E_USER_ERROR, E_RECOVERABLE_ERROR ), true ) ) {
+		return;
+	}
+	$error = str_replace( ABSPATH, '', $error );
+	echo "###### begin_scraped_error:$scrape_key ######";
+	echo wp_json_encode( $error );
+}
diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
index 250089dc25..7f0e75dfe5 100644
--- src/wp-includes/script-loader.php
+++ src/wp-includes/script-loader.php
@@ -471,11 +471,14 @@ function wp_default_scripts( &$scripts ) {
 	$scripts->add( 'htmlhint', '/wp-includes/js/codemirror/htmlhint.js', array(), '0.9.14-xwp' );
 	$scripts->add( 'htmlhint-kses', '/wp-includes/js/codemirror/htmlhint-kses.js', array( 'htmlhint' ) );
 	$scripts->add( 'code-editor', "/wp-admin/js/code-editor$suffix.js", array( 'jquery', 'wp-codemirror' ) );
-	$scripts->add( 'wp-theme-plugin-editor', "/wp-admin/js/theme-plugin-editor$suffix.js", array( 'code-editor', 'jquery', 'jquery-ui-core', 'wp-a11y', 'underscore' ) );
-	did_action( 'init' ) && $scripts->add_inline_script( 'wp-theme-plugin-editor', sprintf( 'wp.themePluginEditor.l10n = %s;', wp_json_encode( wp_array_slice_assoc(
-		/* translators: %d: error count */
-		_n_noop( 'There is %d error which must be fixed before you can save.', 'There are %d errors which must be fixed before you can save.' ),
-		array( 'singular', 'plural' )
+	$scripts->add( 'wp-theme-plugin-editor', "/wp-admin/js/theme-plugin-editor$suffix.js", array( 'wp-util', 'jquery', 'jquery-ui-core', 'wp-a11y', 'underscore' ) );
+	did_action( 'init' ) && $scripts->add_inline_script( 'wp-theme-plugin-editor', sprintf( 'wp.themePluginEditor.l10n = %s;', wp_json_encode( array(
+		'saveAlert' => __( 'The changes you made will be lost if you navigate away from this page.' ),
+		'lintError' => wp_array_slice_assoc(
+			/* translators: %d: error count */
+			_n_noop( 'There is %d error which must be fixed before you can update this file.', 'There are %d errors which must be fixed before you can update this file.' ),
+			array( 'singular', 'plural' )
+		),
 	) ) ) );
 
 	$scripts->add( 'wp-playlist', "/wp-includes/js/mediaelement/wp-playlist$suffix.js", array( 'wp-util', 'backbone', 'mediaelement' ), false, 1 );
diff --git src/wp-settings.php src/wp-settings.php
index 3d4c210338..bacf4cfddd 100644
--- src/wp-settings.php
+++ src/wp-settings.php
@@ -294,6 +294,8 @@ require( ABSPATH . WPINC . '/vars.php' );
 create_initial_taxonomies();
 create_initial_post_types();
 
+wp_start_scraping_edited_file_errors();
+
 // Register the default theme directory root
 register_theme_directory( get_theme_root() );
 
