浏览代码

Created the IndentCodeBlockCommand.

Aleksander Nowodzinski 6 年之前
父节点
当前提交
881b953c95

+ 315 - 0
packages/ckeditor5-code-block/src/indentcodeblockcommand.js

@@ -0,0 +1,315 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module code-block/indentcodeblockcommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+import first from '@ckeditor/ckeditor5-utils/src/first';
+import { getLeadingWhiteSpaces } from './utils';
+
+/**
+ * The code block indentation command plugin.
+ *
+ * @extends module:core/command~Command
+ */
+export default class IndentCodeBlockCommand extends Command {
+	constructor( editor, indentDirection ) {
+		super( editor );
+
+		/**
+		 * Determines whether this command indents (`'forward'`) or outdents (`'backward'`).
+		 *
+		 * @readonly
+		 * @private
+		 * @member {String}
+		 */
+		this._indentDirection = indentDirection;
+
+		/**
+		 * A sequence of characters added (or removed) to the line when indenting (or outdenting).
+		 *
+		 * @readonly
+		 * @private
+		 * @member {String}
+		 */
+		this._indentSequence = editor.config.get( 'codeBlock.indentSequence' );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		this.isEnabled = this._checkEnabled();
+	}
+
+	/**
+	 * Executes the command. When the command {@link #isEnabled is enabled}, the code line(s) the selection
+	 * is anchored to will be indented or outdented.
+	 *
+	 * @fires execute
+	 */
+	execute() {
+		const editor = this.editor;
+		const model = editor.model;
+
+		model.change( writer => {
+			const positions = getIndentOutdentPositions( model );
+
+			if ( this._indentDirection === 'forward' ) {
+				this._indentPositions( writer, positions );
+			} else {
+				this._outdentPositions( writer, positions );
+			}
+		} );
+	}
+
+	/**
+	 * Checks whether the command can be enabled in the current context.
+	 *
+	 * @private
+	 * @returns {Boolean} Whether the command should be enabled.
+	 */
+	_checkEnabled() {
+		if ( !this._indentSequence ) {
+			return false;
+		}
+
+		const selection = this.editor.model.document.selection;
+		const firstBlock = first( selection.getSelectedBlocks() );
+
+		if ( !firstBlock || !firstBlock.is( 'codeBlock' ) ) {
+			return false;
+		}
+
+		const model = this.editor.model;
+
+		// Indent (forward) command is always enabled when there's any code block in the selection
+		// because you can always indent code lines. Outdent (backward) command on the other hand,
+		// can execute only when there is an indent character sequence in some of the lines.
+		if ( this._indentDirection === 'backward' ) {
+			return getIndentOutdentPositions( model ).some( position => {
+				return getLastOutdentableSequenceRange( model, position, this._indentSequence );
+			} );
+		}
+
+		return true;
+	}
+
+	/**
+	 * Indent all positions, for instance assuming the indent sequence is 4x space ("    "):
+	 *
+	 *		<codeBlock>^foo</codeBlock>        ->       <codeBlock>    foo</codeBlock>
+	 *
+	 *		<codeBlock>foo^bar</codeBlock>     ->       <codeBlock>foo    bar</codeBlock>
+	 *
+	 * Also, when there is more than one position:
+	 *
+	 *		<codeBlock>
+	 *			^foobar
+	 *			<softBreak></softBreak>
+	 *			^bazqux
+	 *		</codeBlock>
+	 *
+	 *		->
+	 *
+	 *		<codeBlock>
+	 *			    foobar
+	 *			<softBreak></softBreak>
+	 *			    bazqux
+	 *		</codeBlock>
+	 *
+	 * @private
+	 * @param {module:engine/model/writer~Writer} writer
+	 * @param {Array.<module:engine/model/position~Position>} positions
+	 */
+	_indentPositions( writer, positions ) {
+		for ( const position of positions ) {
+			writer.insertText( this._indentSequence, position );
+		}
+	}
+
+	/**
+	 * Outdent all positions, for instance assuming the indent sequence is 4x space ("    "):
+	 *
+	 *		<codeBlock>^foo</codeBlock>         ->       <codeBlock>foo</codeBlock>
+	 *
+	 *		<codeBlock>    ^bar</codeBlock>     ->       <codeBlock>bar</codeBlock>
+	 *
+	 * Also, when there is more than one position:
+	 *
+	 *		<codeBlock>
+	 *			    ^foobar
+	 *			<softBreak></softBreak>
+	 *			    ^bazqux
+	 *		</codeBlock>
+	 *
+	 *		->
+	 *
+	 *		<codeBlock>
+	 *			foobar
+	 *			<softBreak></softBreak>
+	 *			bazqux
+	 *		</codeBlock>
+	 *
+	 * @private
+	 * @param {module:engine/model/writer~Writer} writer
+	 * @param {Array.<module:engine/model/position~Position>} positions
+	 */
+	_outdentPositions( writer, positions ) {
+		for ( const position of positions ) {
+			const range = getLastOutdentableSequenceRange( this.editor.model, position, this._indentSequence );
+
+			if ( range ) {
+				writer.remove( range );
+			}
+		}
+	}
+}
+
+// Returns an array of all model positions within the selection that represent code block lines.
+//
+// If the selection is collapsed, it returns the exact selection anchor position:
+//
+//		<codeBlock>[]foo</codeBlock>        ->     <codeBlock>^foo</codeBlock>
+//		<codeBlock>foo[]bar</codeBlock>     ->     <codeBlock>foo^bar</codeBlock>
+//
+// Otherwise, it returns positions **before** each text node belonging to all code blocks contained by the selection:
+//
+//		<codeBlock>                                <codeBlock>
+//		    foo[bar                                   ^foobar
+//		    <softBreak></softBreak>         ->        <softBreak></softBreak>
+//		    baz]qux                                   ^bazqux
+//		</codeBlock>                               </codeBlock>
+//
+// it also works across other non–code blocks:
+//
+//		<codeBlock>                                <codeBlock>
+//		    foo[bar                                   ^foobar
+//		</codeBlock>                               </codeBlock>
+//		<paragraph>text</paragraph>         ->     <paragraph>text</paragraph>
+//		<codeBlock>                                <codeBlock>
+//		    baz]qux                                   ^bazqux
+//		</codeBlock>                               </codeBlock>
+//
+// **Note:** The positions are in the reverse order so they do not get outdated when iterating over them and
+// the writer inserts or removes things.
+//
+// **Note:** The position is situated after the leading white spaces in the text node:
+//
+// @param {<module:engine/model/model~Model>} model
+// @returns {Array.<module:engine/model/position~Position>}
+function getIndentOutdentPositions( model ) {
+	const selection = model.document.selection;
+	const positions = [];
+
+	// When the selection is collapsed, there's only one position we can indent or outdent.
+	if ( selection.isCollapsed ) {
+		positions.push( selection.anchor );
+	}
+
+	// When the selection is NOT collapsed, collect all positions starting before text nodes
+	// (code lines) in any <codeBlock> within the selection.
+	else {
+		// Walk backward so positions we're about to collect here do not get outdated when
+		// inserting or deleting using the writer.
+		const walker = selection.getFirstRange().getWalker( {
+			ignoreElementEnd: true,
+			direction: 'backward'
+		} );
+
+		for ( const { item } of walker ) {
+			if ( item.is( 'textProxy' ) && item.parent.is( 'codeBlock' ) ) {
+				const leadingWhiteSpaces = getLeadingWhiteSpaces( item.textNode );
+				const { parent, startOffset } = item.textNode;
+
+				// Make sure the position is after all leading whitespaces in the text node.
+				const position = model.createPositionAt( parent, startOffset + leadingWhiteSpaces.length );
+
+				positions.push( position );
+			}
+		}
+	}
+
+	return positions;
+}
+
+// For a position coming from `getIndentOutdentPositions()`, it returns the range representing
+// the last occurrence of the indent sequence among the leading whitespaces of the code line the
+// position represents.
+//
+// For instance, assuming the indent sequence is 4x space ("    "):
+//
+//		<codeBlock>foo^</codeBlock>                                 ->          null
+//		<codeBlock>foo^<softBreak></softBreak>bar</codeBlock>       ->          null
+//		<codeBlock>  ^foo</codeBlock>                               ->          null
+//		<codeBlock>        ^foo</codeBlock>                         ->          <codeBlock>    [    ]foo</codeBlock>
+//		<codeBlock>    ^foo    bar</codeBlock>                      ->          <codeBlock>[    ]foo    bar</codeBlock>
+//
+// @param {<module:engine/model/model~Model>} model
+// @param {<module:engine/model/position~Position>} position
+// @param {String} sequence
+// @returns {<module:engine/model/range~Range>|null}
+function getLastOutdentableSequenceRange( model, position, sequence ) {
+	// Positions start before each text node (code line). Get the node corresponding to the position.
+	const nodeAtPosition = getCodeLineTextNodeAtPosition( position );
+
+	if ( !nodeAtPosition ) {
+		return null;
+	}
+
+	const leadingWhiteSpaces = getLeadingWhiteSpaces( nodeAtPosition );
+	const lastIndexOfSequence = leadingWhiteSpaces.lastIndexOf( sequence );
+
+	// For instance, assuming the indent sequence is 4x space ("    "):
+	//
+	//		<codeBlock>    	^foo</codeBlock>           ->             null
+	//
+	if ( lastIndexOfSequence + sequence.length !== leadingWhiteSpaces.length ) {
+		return null;
+	}
+
+	// For instance, assuming the indent sequence is 4x space ("    "):
+	//
+	//		<codeBlock>  ^foo</codeBlock>           ->             null
+	//
+	if ( lastIndexOfSequence === -1 ) {
+		return null;
+	}
+
+	const { parent, startOffset } = nodeAtPosition;
+
+	// Create a range that contains the **last** indent sequence among the leading whitespaces
+	// of the line.
+	//
+	// For instance, assuming the indent sequence is 4x space ("    "):
+	//
+	//		<codeBlock>        ^foo</codeBlock>      ->     <codeBlock>    [    ]foo</codeBlock>
+	//
+	return model.createRange(
+		model.createPositionAt( parent, startOffset + lastIndexOfSequence ),
+		model.createPositionAt( parent, startOffset + lastIndexOfSequence + sequence.length )
+	);
+}
+
+function getCodeLineTextNodeAtPosition( position ) {
+	// Positions start before each text node (code line). Get the node corresponding to the position.
+	let nodeAtPosition = position.parent.getChild( position.index );
+
+	// <codeBlock>foo^</codeBlock>
+	// <codeBlock>foo^<softBreak></softBreak>bar</codeBlock>
+	if ( !nodeAtPosition || nodeAtPosition.is( 'softBreak' ) ) {
+		nodeAtPosition = position.nodeBefore;
+	}
+
+	// <codeBlock>^</codeBlock>
+	// <codeBlock>foo^<softBreak></softBreak>bar</codeBlock>
+	if ( !nodeAtPosition || nodeAtPosition.is( 'softBreak' ) ) {
+		return null;
+	}
+
+	return nodeAtPosition;
+}

+ 452 - 0
packages/ckeditor5-code-block/tests/indentcodeblockcommand.js

@@ -0,0 +1,452 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import CodeBlockEditing from '../src/codeblockediting';
+import IndentCodeBlockCommand from '../src/indentcodeblockcommand';
+
+import AlignmentEditing from '@ckeditor/ckeditor5-alignment/src/alignmentediting';
+import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting';
+
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+describe.only( 'IndentCodeBlockCommand', () => {
+	let editor, model, indentCommand, outdentCommand;
+
+	beforeEach( () => {
+		return ModelTestEditor
+			.create( {
+				plugins: [ CodeBlockEditing, Paragraph, BlockQuoteEditing, AlignmentEditing, BoldEditing ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				indentCommand = new IndentCodeBlockCommand( editor, 'forward' );
+				outdentCommand = new IndentCodeBlockCommand( editor, 'backward' );
+			} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	describe( 'Forward (indent) command', () => {
+		describe( '#isEnabled', () => {
+			it( 'should be true when the first selected block is a codeBlock #1', () => {
+				setModelData( model, '<codeBlock language="foo">f[]oo</codeBlock>' );
+
+				expect( indentCommand.isEnabled ).to.be.true;
+			} );
+
+			it( 'should be true when the first selected block is a codeBlock #2', () => {
+				setModelData( model, '<codeBlock language="foo">f[oo</codeBlock><paragraph>ba]r</paragraph>' );
+
+				expect( indentCommand.isEnabled ).to.be.true;
+			} );
+
+			it( 'should be false when there is no code block in the selection', () => {
+				setModelData( model, '<paragraph>foo[]</paragraph>' );
+
+				expect( indentCommand.isEnabled ).to.be.false;
+			} );
+
+			it( 'should be false when the selection is not anchored in the code block', () => {
+				setModelData( model, '<paragraph>f[oo</paragraph><codeBlock language="foo">bar</codeBlock><paragraph>ba]z</paragraph>' );
+
+				expect( indentCommand.isEnabled ).to.be.false;
+			} );
+
+			describe( 'config.codeBlock.indentSequence', () => {
+				it( 'should disable the command when the config is not set', () => {
+					return ModelTestEditor
+						.create( {
+							plugins: [ CodeBlockEditing, Paragraph, BlockQuoteEditing, AlignmentEditing, BoldEditing ],
+							codeBlock: {
+								indentSequence: false
+							}
+						} )
+						.then( newEditor => {
+							const editor = newEditor;
+							const model = editor.model;
+							const indentCommand = new IndentCodeBlockCommand( editor, 'forward' );
+
+							setModelData( model, '<codeBlock language="foo">[]foo</codeBlock>' );
+
+							expect( indentCommand.isEnabled ).to.be.false;
+
+							return editor.destroy();
+						} );
+				} );
+			} );
+		} );
+
+		describe( 'execute()', () => {
+			it( 'should indent when a selection is collapsed in an empty code block', () => {
+				setModelData( model, '<codeBlock language="foo">[]</codeBlock>' );
+
+				indentCommand.execute();
+
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="foo">	[]</codeBlock>' );
+			} );
+
+			it( 'should indent when a selection is collapsed', () => {
+				setModelData( model, '<codeBlock language="foo">f[]oo</codeBlock>' );
+
+				indentCommand.execute();
+
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="foo">f	[]oo</codeBlock>' );
+			} );
+
+			it( 'should indent a whole line when a selection is expanded', () => {
+				setModelData( model, '<codeBlock language="foo">f[o]o</codeBlock>' );
+
+				indentCommand.execute();
+
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="foo">	f[o]o</codeBlock>' );
+			} );
+
+			it( 'should indent multiple lines when a selection is expanded', () => {
+				setModelData( model, '<codeBlock language="foo">f[oo<softBreak></softBreak>b]ar</codeBlock>' );
+
+				indentCommand.execute();
+
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="foo">	f[oo<softBreak></softBreak>	b]ar</codeBlock>' );
+			} );
+
+			it( 'should append the indentation to the line\'s leading white spaces (#1)', () => {
+				setModelData( model, '<codeBlock language="foo">[]foo</codeBlock>' );
+
+				// <codeBlock language="foo">    []foo</codeBlock>
+				model.change( writer => {
+					writer.insertText( '    ', model.document.getRoot().getChild( 0 ) );
+				} );
+
+				indentCommand.execute();
+
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="foo">    	[]foo</codeBlock>' );
+			} );
+
+			it( 'should append the indentation to the line\'s leading white spaces (#2)', () => {
+				setModelData( model, '<codeBlock language="foo">f[oo<softBreak></softBreak>b]ar</codeBlock>' );
+
+				// <codeBlock language="foo">    f[oo<softBreak></softBreak>    b]ar</codeBlock>
+				model.change( writer => {
+					writer.insertText( '    ', model.document.getRoot().getChild( 0 ), 4 );
+					writer.insertText( '    ', model.document.getRoot().getChild( 0 ), 0 );
+				} );
+
+				indentCommand.execute();
+
+				expect( getModelData( model ) ).to.equal(
+					'<codeBlock language="foo">    	f[oo<softBreak></softBreak>    	b]ar</codeBlock>' );
+			} );
+
+			describe( 'config.codeBlock.indentSequence', () => {
+				it( 'should be used when indenting', () => {
+					return ModelTestEditor
+						.create( {
+							plugins: [ CodeBlockEditing, Paragraph, BlockQuoteEditing, AlignmentEditing, BoldEditing ],
+							codeBlock: {
+								indentSequence: '  '
+							}
+						} )
+						.then( newEditor => {
+							const editor = newEditor;
+							const model = editor.model;
+							const indentCommand = new IndentCodeBlockCommand( editor, 'forward' );
+
+							setModelData( model, '<codeBlock language="foo">f[o]o</codeBlock>' );
+
+							indentCommand.execute();
+
+							expect( getModelData( model ) ).to.equal( '<codeBlock language="foo">  f[o]o</codeBlock>' );
+
+							return editor.destroy();
+						} );
+				} );
+			} );
+		} );
+	} );
+
+	describe( 'Backward (outdent) command', () => {
+		describe( '#isEnabled', () => {
+			it( 'should be true when the selection is in a line containing the indent sequence', () => {
+				setModelData( model, '<codeBlock language="foo">f[]oo</codeBlock>' );
+
+				// <codeBlock language="foo">	f[]oo</codeBlock>
+				model.change( writer => {
+					writer.insertText( '	', model.document.getRoot().getChild( 0 ) );
+				} );
+
+				expect( outdentCommand.isEnabled ).to.be.true;
+			} );
+
+			it( 'should be true when any line in the selection contains more than the indent sequence', () => {
+				setModelData( model, '<codeBlock language="foo">f[oo</codeBlock><paragraph>ba]r</paragraph>' );
+
+				// <codeBlock language="foo">	f[oo</codeBlock><paragraph>ba]r</paragraph>
+				model.change( writer => {
+					writer.insertText( '	', model.document.getRoot().getChild( 0 ) );
+				} );
+
+				expect( outdentCommand.isEnabled ).to.be.true;
+			} );
+
+			it( 'should be true when any line in the selection contains the indent sequence', () => {
+				setModelData( model, '<codeBlock language="foo">f[oo</codeBlock><codeBlock language="foo">ba]r</codeBlock>' );
+
+				// <codeBlock language="foo">f[oo</codeBlock><codeBlock language="foo">	ba]r</codeBlock>
+				model.change( writer => {
+					writer.insertText( '	', model.document.getRoot().getChild( 1 ) );
+				} );
+
+				expect( outdentCommand.isEnabled ).to.be.true;
+			} );
+
+			it( 'should be false when the indent sequence is in other element', () => {
+				setModelData( model, '<paragraph>foo[]</paragraph>' );
+
+				// <paragraph>	foo[]</paragraph>
+				model.change( writer => {
+					writer.insertText( '	', model.document.getRoot().getChild( 0 ) );
+				} );
+
+				expect( outdentCommand.isEnabled ).to.be.false;
+			} );
+
+			it( 'should be false when there is no indent sequence in the line (#1)', () => {
+				setModelData( model, '<codeBlock language="foo">f[]oo</codeBlock>' );
+
+				expect( outdentCommand.isEnabled ).to.be.false;
+			} );
+
+			it( 'should be false when there is no indent sequence in the line (#2)', () => {
+				setModelData( model, '<codeBlock language="foo">[]</codeBlock>' );
+
+				expect( outdentCommand.isEnabled ).to.be.false;
+			} );
+
+			it( 'should be false when there is no indent sequence in the line (#3)', () => {
+				setModelData( model, '<codeBlock language="foo">foo[]</codeBlock>' );
+
+				expect( outdentCommand.isEnabled ).to.be.false;
+			} );
+
+			it( 'should be false when there is no corrent sequence in the line', () => {
+				setModelData( model, '<codeBlock language="foo">foo[]</codeBlock>' );
+
+				// <codeBlock language="foo">    foo[]</codeBlock>
+				model.change( writer => {
+					writer.insertText( '    ', model.document.getRoot().getChild( 0 ) );
+				} );
+
+				expect( outdentCommand.isEnabled ).to.be.false;
+			} );
+
+			it( 'should be false when the sequence is not in leading characters of the line (#1)', () => {
+				setModelData( model, '<codeBlock language="foo">barfoo[]</codeBlock>' );
+
+				// <codeBlock language="foo">bar	foo[]</codeBlock>
+				model.change( writer => {
+					writer.insertText( '	', model.document.getRoot().getChild( 0 ), 3 );
+				} );
+
+				expect( outdentCommand.isEnabled ).to.be.false;
+			} );
+
+			it( 'should be false when the sequence is not in leading characters of the line (#2)', () => {
+				setModelData( model, '<codeBlock language="foo">foo[]</codeBlock>' );
+
+				// <codeBlock language="foo">foo[]</codeBlock>
+				model.change( writer => {
+					writer.insertText( '    	    ', model.document.getRoot().getChild( 0 ), 0 );
+				} );
+
+				expect( outdentCommand.isEnabled ).to.be.false;
+			} );
+
+			describe( 'config.codeBlock.indentSequence', () => {
+				it( 'should be respected (#1)', () => {
+					return ModelTestEditor
+						.create( {
+							plugins: [ CodeBlockEditing, Paragraph, BlockQuoteEditing, AlignmentEditing, BoldEditing ],
+							codeBlock: {
+								indentSequence: '  '
+							}
+						} )
+						.then( newEditor => {
+							const editor = newEditor;
+							const model = editor.model;
+							const outdentCommand = new IndentCodeBlockCommand( editor, 'backward' );
+
+							setModelData( model, '<codeBlock language="foo">foo[]</codeBlock>' );
+
+							// <codeBlock language="foo">    foo[]</codeBlock>
+							model.change( writer => {
+								writer.insertText( '    ', model.document.getRoot().getChild( 0 ) );
+							} );
+
+							expect( outdentCommand.isEnabled ).to.be.true;
+
+							return editor.destroy();
+						} );
+				} );
+
+				it( 'should be respected (#2)', () => {
+					return ModelTestEditor
+						.create( {
+							plugins: [ CodeBlockEditing, Paragraph, BlockQuoteEditing, AlignmentEditing, BoldEditing ],
+							codeBlock: {
+								indentSequence: '  '
+							}
+						} )
+						.then( newEditor => {
+							const editor = newEditor;
+							const model = editor.model;
+							const outdentCommand = new IndentCodeBlockCommand( editor, 'backward' );
+
+							setModelData( model, '<codeBlock language="foo"> foo[]</codeBlock>' );
+
+							// <codeBlock language="foo"> foo[]</codeBlock>
+							model.change( writer => {
+								writer.insertText( ' ', model.document.getRoot().getChild( 0 ) );
+							} );
+
+							expect( outdentCommand.isEnabled ).to.be.false;
+
+							return editor.destroy();
+						} );
+				} );
+
+				it( 'should disable the command when the config is not set', () => {
+					return ModelTestEditor
+						.create( {
+							plugins: [ CodeBlockEditing, Paragraph, BlockQuoteEditing, AlignmentEditing, BoldEditing ],
+							codeBlock: {
+								indentSequence: false
+							}
+						} )
+						.then( newEditor => {
+							const editor = newEditor;
+							const model = editor.model;
+							const outdentCommand = new IndentCodeBlockCommand( editor, 'backward' );
+
+							setModelData( model, '<codeBlock language="foo">foo[]</codeBlock>' );
+
+							// <codeBlock language="foo">	foo[]</codeBlock>
+							model.change( writer => {
+								writer.insertText( '	', model.document.getRoot().getChild( 0 ) );
+							} );
+
+							expect( outdentCommand.isEnabled ).to.be.false;
+
+							return editor.destroy();
+						} );
+				} );
+			} );
+		} );
+
+		describe( 'execute()', () => {
+			it( 'should outdent a single line', () => {
+				setModelData( model, '<codeBlock language="foo">f[]oo</codeBlock>' );
+
+				// <codeBlock language="foo">	f[]oo</codeBlock>
+				model.change( writer => {
+					writer.insertText( '	', model.document.getRoot().getChild( 0 ) );
+				} );
+
+				outdentCommand.execute();
+
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="foo">f[]oo</codeBlock>' );
+			} );
+
+			it( 'should outdent only one level in a single line', () => {
+				setModelData( model, '<codeBlock language="foo">f[]oo</codeBlock>' );
+
+				// <codeBlock language="foo">		f[]oo</codeBlock>
+				model.change( writer => {
+					writer.insertText( '		', model.document.getRoot().getChild( 0 ) );
+				} );
+
+				outdentCommand.execute();
+
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="foo">	f[]oo</codeBlock>' );
+			} );
+
+			it( 'should outdent multiple lines', () => {
+				setModelData( model, '<codeBlock language="foo">f[oo<softBreak></softBreak>ba]r</codeBlock>' );
+
+				// <codeBlock language="foo">	f[oo<softBreak></softBreak>	ba]r</codeBlock>
+				model.change( writer => {
+					writer.insertText( '	', model.document.getRoot().getChild( 0 ), 4 );
+					writer.insertText( '	', model.document.getRoot().getChild( 0 ), 0 );
+				} );
+
+				outdentCommand.execute();
+
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="foo">f[oo<softBreak></softBreak>ba]r</codeBlock>' );
+			} );
+
+			it( 'should outdent only one level across multiple lines', () => {
+				setModelData( model, '<codeBlock language="foo">f[oo<softBreak></softBreak>ba]r</codeBlock>' );
+
+				// <codeBlock language="foo">	f[oo<softBreak></softBreak>		ba]r</codeBlock>
+				model.change( writer => {
+					writer.insertText( '		', model.document.getRoot().getChild( 0 ), 4 );
+					writer.insertText( '	', model.document.getRoot().getChild( 0 ), 0 );
+				} );
+
+				outdentCommand.execute();
+
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="foo">f[oo<softBreak></softBreak>	ba]r</codeBlock>' );
+			} );
+
+			it( 'should outdent some lines', () => {
+				setModelData( model, '<codeBlock language="foo">f[oo<softBreak></softBreak>ba]r</codeBlock>' );
+
+				// <codeBlock language="foo">f[oo<softBreak></softBreak>	ba]r</codeBlock>
+				model.change( writer => {
+					writer.insertText( '	', model.document.getRoot().getChild( 0 ), 4 );
+				} );
+
+				outdentCommand.execute();
+
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="foo">f[oo<softBreak></softBreak>ba]r</codeBlock>' );
+			} );
+
+			describe( 'config.codeBlock.indentSequence', () => {
+				it( 'should be respected', () => {
+					return ModelTestEditor
+						.create( {
+							plugins: [ CodeBlockEditing, Paragraph, BlockQuoteEditing, AlignmentEditing, BoldEditing ],
+							codeBlock: {
+								indentSequence: '  '
+							}
+						} )
+						.then( newEditor => {
+							const editor = newEditor;
+							const model = editor.model;
+							const outdentCommand = new IndentCodeBlockCommand( editor, 'backward' );
+
+							setModelData( model, '<codeBlock language="foo">f[]oo</codeBlock>' );
+
+							// <codeBlock language="foo">  f[]oo</codeBlock>
+							model.change( writer => {
+								writer.insertText( '  ', model.document.getRoot().getChild( 0 ) );
+							} );
+
+							outdentCommand.execute();
+
+							expect( getModelData( model ) ).to.equal( '<codeBlock language="foo">f[]oo</codeBlock>' );
+
+							return editor.destroy();
+						} );
+				} );
+			} );
+		} );
+	} );
+} );