Index: src/wp-includes/class-wp-editor.php
===================================================================
--- src/wp-includes/class-wp-editor.php	(revision 32634)
+++ src/wp-includes/class-wp-editor.php	(working copy)
@@ -367,7 +367,8 @@
 						'wpgallery',
 						'wplink',
 						'wpdialogs',
-						'wpview',
+						'wptextpattern',
+						'wpview'
 					);
 
 					if ( ! self::$has_medialib ) {
Index: src/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js
===================================================================
--- src/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js	(revision 0)
+++ src/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js	(working copy)
@@ -0,0 +1,100 @@
+( function( tinymce, setTimeout ) {
+	tinymce.PluginManager.add( 'wptextpattern', function( editor ) {
+		var $$ = editor.$,
+			patterns = [],
+			canUndo = false;
+
+		function add( regExp, callback ) {
+			patterns.push( {
+				regExp: regExp,
+				callback: callback
+			} );
+		}
+
+		add( /^[*-]\s/, function() {
+			this.execCommand( 'InsertUnorderedList' );
+		} );
+
+		add( /^1[.)]\s/, function() {
+			this.execCommand( 'InsertOrderedList' );
+		} );
+
+		editor.on( 'selectionchange', function() {
+			canUndo = false;
+		} );
+
+		editor.on( 'keydown', function( event ) {
+			if ( canUndo && event.keyCode === tinymce.util.VK.BACKSPACE ) {
+				editor.undoManager.undo();
+				event.preventDefault();
+			}
+		} );
+
+		editor.on( 'keyup', function( event ) {
+			var rng, node, text, parent, child;
+
+			if ( event.keyCode !== tinymce.util.VK.SPACEBAR ) {
+				return;
+			}
+
+			rng = editor.selection.getRng();
+			node = rng.startContainer;
+			text = node.nodeValue;
+
+			if ( node.nodeType !== 3 ) {
+				return;
+			}
+
+			parent = editor.dom.getParent( node, 'p' );
+
+			if ( ! parent ) {
+				return;
+			}
+
+			while ( child = parent.firstChild ) {
+				if ( child.nodeType !== 3 ) {
+					parent = child;
+				} else {
+					break;
+				}
+			}
+
+			if ( child !== node ) {
+				return;
+			}
+
+			tinymce.each( patterns, function( pattern ) {
+				var replace = text.replace( pattern.regExp, '' );
+
+				if ( text === replace ) {
+					return;
+				}
+
+				if ( rng.startOffset !== text.length - replace.length ) {
+					return;
+				}
+
+				editor.undoManager.add();
+
+				editor.undoManager.transact( function() {
+					editor.selection.setCursorLocation( node, 0 );
+
+					if ( replace ) {
+						$$( node ).replaceWith( document.createTextNode( replace ) );
+					} else  {
+						$$( node.parentNode ).empty().append( '<br>' );
+					}
+
+					pattern.callback.apply( editor );
+				} );
+
+				// We need to wait for native events to be triggered.
+				setTimeout( function() {
+					canUndo = true;
+				} );
+
+				return false;
+			} );
+		} );
+	} );
+} )( window.tinymce, window.setTimeout );
Index: tests/qunit/editor/js/utils.js
===================================================================
--- tests/qunit/editor/js/utils.js	(revision 32634)
+++ tests/qunit/editor/js/utils.js	(working copy)
@@ -131,7 +131,18 @@
 
 	// TODO: Replace this with the new event logic in 3.5
 	function type(chr) {
-		var editor = tinymce.activeEditor, keyCode, charCode, evt, startElm, rng;
+		var editor = tinymce.activeEditor, keyCode, charCode, evt, startElm, rng, startContainer, startOffset, textNode;
+
+		function charCodeToKeyCode(charCode) {
+			var lookup = {
+				'0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55, '8': 56, '9': 57,'a': 65, 'b': 66, 'c': 67,
+				'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73, 'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81,
+				'r': 82, 's': 83, 't': 84, 'u': 85,	'v': 86, 'w': 87, 'x': 88, 'y': 89, ' ': 32, ',': 188, '-': 189, '.': 190, '/': 191, '\\': 220,
+				'[': 219, ']': 221, '\'': 222, ';': 186, '=': 187, ')': 41
+			};
+
+			return lookup[String.fromCharCode(charCode)];
+		}
 
 		function fakeEvent(target, type, evt) {
 			editor.dom.fire(target, type, evt);
@@ -139,7 +150,8 @@
 
 		// Numeric keyCode
 		if (typeof(chr) == "number") {
-			charCode = keyCode = chr;
+			charCode = chr;
+			keyCode = charCodeToKeyCode(charCode);
 		} else if (typeof(chr) == "string") {
 			// String value
 			if (chr == '\b') {
@@ -150,10 +162,18 @@
 				charCode = chr.charCodeAt(0);
 			} else {
 				charCode = chr.charCodeAt(0);
-				keyCode = charCode;
+				keyCode = charCodeToKeyCode(charCode);
 			}
 		} else {
 			evt = chr;
+
+			if (evt.charCode) {
+				chr = String.fromCharCode(evt.charCode);
+			}
+
+			if (evt.keyCode) {
+				keyCode = evt.keyCode;
+			}
 		}
 
 		evt = evt || {keyCode: keyCode, charCode: charCode};
@@ -175,17 +195,19 @@
 					rng.execCommand('Delete', false, null);
 				} else {
 					rng = editor.selection.getRng();
+					startContainer = rng.startContainer;
 
-					if (rng.startContainer.nodeType == 1 && rng.collapsed) {
-						var nodes = rng.startContainer.childNodes, lastNode = nodes[nodes.length - 1];
+					if (startContainer.nodeType == 1 && rng.collapsed) {
+						var nodes = rng.startContainer.childNodes;
+						startContainer = nodes[nodes.length - 1];
+					}
 
-						// If caret is at <p>abc|</p> and after the abc text node then move it to the end of the text node
-						// Expand the range to include the last char <p>ab[c]</p> since IE 11 doesn't delete otherwise
-						if (rng.startOffset >= nodes.length - 1 && lastNode && lastNode.nodeType == 3 && lastNode.data.length > 0) {
-							rng.setStart(lastNode, lastNode.data.length - 1);
-							rng.setEnd(lastNode, lastNode.data.length);
-							editor.selection.setRng(rng);
-						}
+					// If caret is at <p>abc|</p> and after the abc text node then move it to the end of the text node
+					// Expand the range to include the last char <p>ab[c]</p> since IE 11 doesn't delete otherwise
+					if ( rng.collapsed && startContainer && startContainer.nodeType == 3 && startContainer.data.length > 0) {
+						rng.setStart(startContainer, startContainer.data.length - 1);
+						rng.setEnd(startContainer, startContainer.data.length);
+						editor.selection.setRng(rng);
 					}
 
 					editor.getDoc().execCommand('Delete', false, null);
@@ -194,13 +216,19 @@
 				rng = editor.selection.getRng(true);
 
 				if (rng.startContainer.nodeType == 3 && rng.collapsed) {
-					rng.startContainer.insertData(rng.startOffset, chr);
-					rng.setStart(rng.startContainer, rng.startOffset + 1);
-					rng.collapse(true);
-					editor.selection.setRng(rng);
+					// `insertData` may alter the range.
+					startContainer = rng.startContainer;
+					startOffset = rng.startOffset;
+					rng.startContainer.insertData( rng.startOffset, chr );
+					rng.setStart( startContainer, startOffset + 1 );
 				} else {
-					rng.insertNode(editor.getDoc().createTextNode(chr));
+					textNode = editor.getDoc().createTextNode(chr);
+					rng.insertNode(textNode);
+					rng.setStart(textNode, 1);
 				}
+
+				rng.collapse(true);
+				editor.selection.setRng(rng);
 			}
 		}
 
Index: tests/qunit/index.html
===================================================================
--- tests/qunit/index.html	(revision 32634)
+++ tests/qunit/index.html	(working copy)
@@ -39,5 +39,14 @@
 		<script src="wp-includes/js/shortcode.js"></script>
 		<script src="wp-admin/js/customize-controls.js"></script>
 		<script src="wp-admin/js/customize-controls-utils.js"></script>
+
+		<!-- TinyMCE -->
+
+		<script src="../../src/wp-includes/js/tinymce/tinymce.js"></script>
+
+		<script src="editor/js/utils.js"></script>
+
+		<script src="wp-includes/js/tinymce/plugins/wptextpattern/plugin.js"></script>
+
 	</body>
 </html>
Index: tests/qunit/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js
===================================================================
--- tests/qunit/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js	(revision 0)
+++ tests/qunit/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js	(working copy)
@@ -0,0 +1,120 @@
+( function( $, QUnit, tinymce, _type, setTimeout ) {
+	var editor;
+
+	function type() {
+		var args = arguments;
+
+		setTimeout( function() {
+			if ( typeof args[0] === 'string' ) {
+				args[0] = args[0].split( '' );
+			}
+
+			if ( typeof args[0] === 'function' ) {
+				args[0]();
+			} else {
+				_type( args[0].shift() );
+			}
+
+			if ( ! args[0].length ) {
+				[].shift.call( args );
+			}
+
+			if ( args.length ) {
+				type.apply( null, args );
+			}
+		} );
+	}
+
+	QUnit.module( 'tinymce.plugins.wptextpattern', {
+		beforeEach: function( assert ) {
+			var done = assert.async();
+
+			$( '#qunit-fixture' ).append( '<textarea id="editor">' );
+
+			tinymce.init( {
+				selector: '#editor',
+				plugins: 'wptextpattern',
+				init_instance_callback: function() {
+					editor = arguments[0];
+					editor.focus();
+					editor.selection.setCursorLocation();
+					setTimeout( done );
+				}
+			} );
+		},
+		afterEach: function() {
+			editor.remove();
+		}
+	} );
+
+	QUnit.test( 'Unordered list.', function( assert ) {
+		type( '* test', function() {
+			assert.equal( editor.getContent(), '<ul>\n<li>test</li>\n</ul>' );
+		}, assert.async() );
+	} );
+
+	QUnit.test( 'Ordered list.', function( assert ) {
+		type( '1. test', function() {
+			assert.equal( editor.getContent(), '<ol>\n<li>test</li>\n</ol>' );
+		}, assert.async() );
+	} );
+
+	QUnit.test( 'Ordered list with content.', function( assert ) {
+		editor.setContent( '<p><strong>test</strong></p>' );
+		editor.selection.setCursorLocation();
+
+		type( '* ', function() {
+			assert.equal( editor.getContent(), '<ul>\n<li><strong>test</strong></li>\n</ul>' );
+		}, assert.async() );
+	} );
+
+	QUnit.test( 'Only transform inside a P tag.', function( assert ) {
+		editor.setContent( '<h1>test</h1>' );
+		editor.selection.setCursorLocation();
+
+		type( '* ', function() {
+			assert.equal( editor.getContent(), '<h1>* test</h1>' );
+		}, assert.async() );
+	} );
+
+	QUnit.test( 'Only transform at the start of a P tag.', function( assert ) {
+		editor.setContent( '<p>test <strong>test</strong></p>' );
+		editor.selection.setCursorLocation( editor.$( 'strong' )[0].firstChild, 0 );
+
+		type( '* ', function() {
+			assert.equal( editor.getContent(), '<p>test <strong>* test</strong></p>' );
+		}, assert.async() );
+	} );
+
+	QUnit.test( 'Only transform when at the cursor is at the start.', function( assert ) {
+		editor.setContent( '<p>* test</p>' );
+		editor.selection.setCursorLocation( editor.$( 'p' )[0].firstChild, 6 );
+
+		type( ' test', function() {
+			assert.equal( editor.getContent(), '<p>* test test</p>' );
+		}, assert.async() );
+	} );
+
+	QUnit.test( 'Backspace should undo the transformation.', function( assert ) {
+		editor.setContent( '<p>test</p>' );
+		editor.selection.setCursorLocation();
+
+		type( '* \b', function() {
+			assert.equal( editor.getContent(), '<p>* test</p>' );
+			assert.equal( editor.selection.getRng().startOffset, 2 );
+		}, assert.async() );
+	} );
+
+	QUnit.test( 'Backspace should undo the transformation only right after it happened.', function( assert ) {
+		editor.setContent( '<p>test</p>' );
+		editor.selection.setCursorLocation();
+
+		type( '* ', function() {
+			editor.selection.setCursorLocation( editor.$( 'li' )[0].firstChild, 4 );
+			// Gecko.
+			editor.fire( 'click' );
+		}, '\b', function() {
+			assert.equal( editor.getContent(), '<ul>\n<li>tes</li>\n</ul>' );
+		}, assert.async() );
+	} );
+} )( window.jQuery, window.QUnit, window.tinymce, window.Utils.type, window.setTimeout );
