Przeglądaj źródła

Indentation retention when pressing enter.

Aleksander Nowodzinski 6 lat temu
rodzic
commit
4ff12c6974

+ 139 - 27
packages/ckeditor5-code-block/src/codeblockediting.js

@@ -127,36 +127,148 @@ export default class CodeBlockEditing extends Plugin {
 	afterInit() {
 		const editor = this.editor;
 		const view = editor.editing.view;
-		const viewDoc = view.document;
-
-		this.listenTo( viewDoc, 'enter', ( evt, data ) => {
-			const doc = editor.model.document;
-			const positionParent = doc.selection.getLastPosition().parent;
-
-			if ( positionParent.is( 'codeBlock' ) ) {
-				const lastPosition = doc.selection.getLastPosition();
-				const isSoftBreakBefore = lastPosition.nodeBefore && lastPosition.nodeBefore.is( 'softBreak' );
-				const isSoftEnter = data.isSoft;
-
-				if ( !isSoftEnter && doc.selection.isCollapsed && lastPosition.isAtEnd && isSoftBreakBefore ) {
-					editor.model.change( writer => {
-						writer.remove( lastPosition.nodeBefore );
-						editor.execute( 'enter' );
-
-						const newBlock = doc.selection.anchor.parent;
-
-						writer.rename( newBlock, DEFAULT_ELEMENT );
-						editor.model.schema.removeDisallowedAttributes( [ newBlock ], writer );
-						view.scrollToTheSelection();
-					} );
-				} else {
-					editor.execute( 'shiftEnter' );
-				}
+		const model = editor.model;
+		const modelDoc = model.document;
 
-				data.preventDefault();
-				evt.stop();
+		this.listenTo( view.document, 'enter', ( evt, data ) => {
+			const positionParent = modelDoc.selection.getLastPosition().parent;
+
+			if ( !positionParent.is( 'codeBlock' ) ) {
+				return;
 			}
+
+			// Upon enter key press we can either leave the block if it's "two enters" in a row
+			// or create a new code block line, preserving previous line's indentation.
+			leaveBlock( data.isSoft ) || breakLine();
+
+			data.preventDefault();
+			evt.stop();
 		} );
+
+		// Normally, when the enter (or shift+enter) key is pressed, a soft line break is to be added to the
+		// code block. Let's try to follow the indentation of the previous line when possible, for instance:
+		//
+		//		// Before pressing enter (or shift enter)
+		//		<codeBlock>
+		//		"    foo()"[]                   // Indent of 4 spaces.
+		//		</codeBlock>
+		//
+		//		// After pressing:
+		//		<codeBlock>
+		//			"    foo()"                 // Indent of 4 spaces.
+		//			<softBreak></softBreak>     // A new soft break created by pressing enter.
+		//			"    "[]                    // Retain the indent of 4 spaces.
+		//		</codeBlock>
+		function breakLine() {
+			const lastSelectionPosition = modelDoc.selection.getLastPosition();
+			const node = lastSelectionPosition.nodeBefore || lastSelectionPosition.textNode;
+			let leadingWhiteSpaces;
+
+			// Figure out the indentation (white space chars) at the beginning of the line.
+			if ( node && node.is( 'text' ) ) {
+				leadingWhiteSpaces = node.data.match( /^(\s*)/ )[ 0 ];
+			}
+
+			// Keeping everything in a change block for a single undo step.
+			editor.model.change( writer => {
+				editor.execute( 'shiftEnter' );
+
+				// If the line before being broken in two had some indentation, let's retain it
+				// in the new line.
+				if ( leadingWhiteSpaces ) {
+					writer.insertText( leadingWhiteSpaces, modelDoc.selection.anchor );
+				}
+			} );
+		}
+
+		// Leave the code block when enter (but NOT shift+enter) has been pressed twice at the end
+		// of the code block:
+		//
+		//		// Before:
+		//		<codeBlock>foo[]</codeBlock>
+		//
+		//		// After first press
+		//		<codeBlock>foo<softBreak></softBreak>[]</codeBlock>
+		//
+		//		// After second press
+		//		<codeBlock>foo</codeBlock><paragraph>[]</paragraph>
+		//
+		function leaveBlock( isSoftEnter ) {
+			const lastSelectionPosition = modelDoc.selection.getLastPosition();
+			const nodeBefore = lastSelectionPosition.nodeBefore;
+
+			let emptyLineRangeToRemoveOnDoubleEnter;
+
+			if ( isSoftEnter || !modelDoc.selection.isCollapsed || !lastSelectionPosition.isAtEnd || !nodeBefore ) {
+				return false;
+			}
+
+			// When the position is directly preceded by a soft break
+			//
+			//		<codeBlock>foo<softBreak></softBreak>[]</codeBlock>
+			//
+			// it creates the following range that will be cleaned up before leaving:
+			//
+			//		<codeBlock>foo[<softBreak></softBreak>]</codeBlock>
+			//
+			if ( nodeBefore.is( 'softBreak' ) ) {
+				emptyLineRangeToRemoveOnDoubleEnter = model.createRangeOn( nodeBefore );
+			}
+
+			// When there's some text before the position made purely of white–space characters
+			//
+			//		<codeBlock>foo<softBreak></softBreak>    []</codeBlock>
+			//
+			// but NOT when it's the first one of the kind
+			//
+			//		<codeBlock>    []</codeBlock>
+			//
+			// it creates the following range to clean up before leaving:
+			//
+			//		<codeBlock>foo[<softBreak></softBreak>    ]</codeBlock>
+			//
+			else if (
+				nodeBefore.is( 'text' ) &&
+				!nodeBefore.data.match( /\S/ ) &&
+				nodeBefore.previousSibling &&
+				nodeBefore.previousSibling.is( 'softBreak' )
+			) {
+				emptyLineRangeToRemoveOnDoubleEnter = model.createRange(
+					model.createPositionBefore( nodeBefore.previousSibling ), model.createPositionAfter( nodeBefore )
+				);
+			}
+
+			// Not leaving the block in the following cases:
+			//
+			//		<codeBlock>    []</codeBlock>
+			//		<codeBlock>  a []</codeBlock>
+			//		<codeBlock>foo<softBreak></softBreak>bar[]</codeBlock>
+			//		<codeBlock>foo<softBreak></softBreak> a []</codeBlock>
+			//
+			else {
+				return false;
+			}
+
+			// We're doing everything in a single change block to have a single undo step.
+			editor.model.change( writer => {
+				// Remove the last <softBreak> and all white space characters that followed it.
+				writer.remove( emptyLineRangeToRemoveOnDoubleEnter );
+
+				// "Clone" the <codeBlock> in the standard way.
+				editor.execute( 'enter' );
+
+				const newBlock = modelDoc.selection.anchor.parent;
+
+				// Make the cloned <codeBlock> a regular <paragraph> (with clean attributes, so no language).
+				writer.rename( newBlock, DEFAULT_ELEMENT );
+				editor.model.schema.removeDisallowedAttributes( [ newBlock ], writer );
+
+				// Eye candy.
+				view.scrollToTheSelection();
+			} );
+
+			return true;
+		}
 	}
 }
 

+ 179 - 58
packages/ckeditor5-code-block/tests/codeblockediting.js

@@ -190,9 +190,7 @@ describe( 'CodeBlockEditing', () => {
 
 			setModelData( model, '<codeBlock>foo[]bar</codeBlock>' );
 
-			viewDoc.fire( 'enter', new DomEventData( viewDoc, {
-				preventDefault: sinon.spy()
-			} ) );
+			viewDoc.fire( 'enter', getEvent() );
 
 			expect( getModelData( model ) ).to.equal( '<codeBlock>foo<softBreak></softBreak>[]bar</codeBlock>' );
 			sinon.assert.calledOnce( shiftEnterCommand.execute );
@@ -208,90 +206,213 @@ describe( 'CodeBlockEditing', () => {
 
 			setModelData( model, '<paragraph>foo[]bar</paragraph>' );
 
-			viewDoc.fire( 'enter', new DomEventData( viewDoc, {
-				preventDefault: sinon.spy()
-			} ) );
+			viewDoc.fire( 'enter', getEvent() );
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>[]bar</paragraph>' );
 			sinon.assert.calledOnce( enterCommand.execute );
 			sinon.assert.notCalled( shiftEnterCommand.execute );
 		} );
 
-		it( 'should leave the block when pressed twice at the end', () => {
-			const spy = sinon.spy( editor.editing.view, 'scrollToTheSelection' );
+		describe( 'indentation retention', () => {
+			it( 'should work when indentation is with spaces', () => {
+				setModelData( model, '<codeBlock language="css">foo[]</codeBlock>' );
 
-			setModelData( model, '<codeBlock language="css">foo[]</codeBlock>' );
+				model.change( writer => {
+					// <codeBlock language="css">  foo[]</codeBlock>
+					writer.insertText( '  ', model.document.getRoot().getChild( 0 ), 0 );
+				} );
 
-			viewDoc.fire( 'enter', new DomEventData( viewDoc, {
-				preventDefault: sinon.spy()
-			} ) );
+				viewDoc.fire( 'enter', getEvent() );
 
-			expect( getModelData( model ) ).to.equal( '<codeBlock language="css">foo<softBreak></softBreak>[]</codeBlock>' );
+				expect( getModelData( model ) ).to.equal(
+					'<codeBlock language="css">  foo<softBreak></softBreak>  []</codeBlock>' );
 
-			viewDoc.fire( 'enter', new DomEventData( viewDoc, {
-				preventDefault: sinon.spy()
-			} ) );
+				editor.execute( 'undo' );
 
-			expect( getModelData( model ) ).to.equal(
-				'<codeBlock language="css">foo</codeBlock>' +
-				'<paragraph>[]</paragraph>'
-			);
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="css">  foo[]</codeBlock>' );
+			} );
 
-			sinon.assert.calledOnce( spy );
+			it( 'should work when indentation is with tabs', () => {
+				setModelData( model, '<codeBlock language="css">foo[]</codeBlock>' );
 
-			editor.execute( 'undo' );
-			expect( getModelData( model ) ).to.equal( '<codeBlock language="css">foo<softBreak></softBreak>[]</codeBlock>' );
+				model.change( writer => {
+					// <codeBlock language="css">	foo[]</codeBlock>
+					writer.insertText( '	', model.document.getRoot().getChild( 0 ), 0 );
+				} );
 
-			editor.execute( 'undo' );
-			expect( getModelData( model ) ).to.equal( '<codeBlock language="css">foo[]</codeBlock>' );
-		} );
+				viewDoc.fire( 'enter', getEvent() );
 
-		it( 'should not leave the block when pressed twice when in the middle of the code', () => {
-			setModelData( model, '<codeBlock language="css">fo[]o</codeBlock>' );
+				expect( getModelData( model ) ).to.equal(
+					'<codeBlock language="css">	foo<softBreak></softBreak>	[]</codeBlock>' );
 
-			viewDoc.fire( 'enter', new DomEventData( viewDoc, {
-				preventDefault: sinon.spy()
-			} ) );
+				editor.execute( 'undo' );
 
-			expect( getModelData( model ) ).to.equal(
-				'<codeBlock language="css">fo<softBreak></softBreak>[]o</codeBlock>' );
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="css">	foo[]</codeBlock>' );
+			} );
 
-			viewDoc.fire( 'enter', new DomEventData( viewDoc, {
-				preventDefault: sinon.spy()
-			} ) );
+			it( 'should retain only the last line', () => {
+				setModelData( model, '<codeBlock language="css">foo<softBreak></softBreak>bar[]</codeBlock>' );
 
-			expect( getModelData( model ) ).to.equal(
-				'<codeBlock language="css">fo<softBreak></softBreak><softBreak></softBreak>[]o</codeBlock>' );
-		} );
+				model.change( writer => {
+					// <codeBlock language="css">  foo<softBreak></softBreak>	bar[]</codeBlock>
+					writer.insertText( '	', model.document.getRoot().getChild( 0 ), 4 );
+					writer.insertText( '  ', model.document.getRoot().getChild( 0 ), 0 );
+				} );
 
-		it( 'should not leave the block when pressed twice at the beginning of the code', () => {
-			setModelData( model, '<codeBlock language="css">[]foo</codeBlock>' );
+				viewDoc.fire( 'enter', getEvent() );
 
-			viewDoc.fire( 'enter', new DomEventData( viewDoc, {
-				preventDefault: sinon.spy()
-			} ) );
+				expect( getModelData( model ) ).to.equal(
+					'<codeBlock language="css">  foo<softBreak></softBreak>	bar<softBreak></softBreak>	[]</codeBlock>' );
 
-			expect( getModelData( model ) ).to.equal(
-				'<codeBlock language="css"><softBreak></softBreak>[]foo</codeBlock>' );
+				editor.execute( 'undo' );
 
-			viewDoc.fire( 'enter', new DomEventData( viewDoc, {
-				preventDefault: sinon.spy()
-			} ) );
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="css">  foo<softBreak></softBreak>	bar[]</codeBlock>' );
+			} );
 
-			expect( getModelData( model ) ).to.equal(
-				'<codeBlock language="css"><softBreak></softBreak><softBreak></softBreak>[]foo</codeBlock>' );
+			it( 'should retain when the selection is non–collapsed', () => {
+				setModelData( model, '<codeBlock language="css">f[o]o</codeBlock>' );
+
+				model.change( writer => {
+					// <codeBlock language="css">    f[o]o</codeBlock>
+					writer.insertText( '    ', model.document.getRoot().getChild( 0 ), 0 );
+				} );
+
+				viewDoc.fire( 'enter', getEvent() );
+
+				expect( getModelData( model ) ).to.equal(
+					'<codeBlock language="css">    f<softBreak></softBreak>    []o</codeBlock>' );
+
+				editor.execute( 'undo' );
+
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="css">    f[o]o</codeBlock>' );
+			} );
+
+			it( 'should consider only leading white-spaces', () => {
+				setModelData( model, '<codeBlock language="css">foo[]</codeBlock>' );
+
+				model.change( writer => {
+					// <codeBlock language="css">  foo []</codeBlock>
+					writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 3 );
+					writer.insertText( '  ', model.document.getRoot().getChild( 0 ), 0 );
+				} );
+
+				viewDoc.fire( 'enter', getEvent() );
+
+				expect( getModelData( model ) ).to.equal(
+					'<codeBlock language="css">  foo <softBreak></softBreak>  []</codeBlock>' );
+
+				editor.execute( 'undo' );
+
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="css">  foo []</codeBlock>' );
+			} );
+
+			it( 'should not work when there is some non-whitespace character', () => {
+				setModelData( model, '<codeBlock language="css">foo[]</codeBlock>' );
+
+				model.change( writer => {
+					// <codeBlock language="css">foo   []</codeBlock>
+					writer.insertText( '   ', model.document.getRoot().getChild( 0 ), 3 );
+				} );
+
+				viewDoc.fire( 'enter', getEvent() );
+
+				expect( getModelData( model ) ).to.equal(
+					'<codeBlock language="css">foo   <softBreak></softBreak>[]</codeBlock>' );
+			} );
 		} );
 
-		it( 'should not leave the block when pressed shift+enter twice at the end of the code', () => {
-			setModelData( model, '<codeBlock language="css">foo<softBreak></softBreak>[]</codeBlock>' );
+		describe( 'leaving block on double enter', () => {
+			it( 'should leave the block when pressed twice at the end', () => {
+				const spy = sinon.spy( editor.editing.view, 'scrollToTheSelection' );
 
-			viewDoc.fire( 'enter', new DomEventData( viewDoc, {
-				preventDefault: sinon.spy()
-			}, { isSoft: true } ) );
+				setModelData( model, '<codeBlock language="css">foo[]</codeBlock>' );
 
-			expect( getModelData( model ) ).to.equal(
-				'<codeBlock language="css">foo<softBreak></softBreak><softBreak></softBreak>[]</codeBlock>' );
+				viewDoc.fire( 'enter', getEvent() );
+
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="css">foo<softBreak></softBreak>[]</codeBlock>' );
+
+				viewDoc.fire( 'enter', getEvent() );
+
+				expect( getModelData( model ) ).to.equal(
+					'<codeBlock language="css">foo</codeBlock>' +
+					'<paragraph>[]</paragraph>'
+				);
+
+				sinon.assert.calledOnce( spy );
+
+				editor.execute( 'undo' );
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="css">foo<softBreak></softBreak>[]</codeBlock>' );
+
+				editor.execute( 'undo' );
+				expect( getModelData( model ) ).to.equal( '<codeBlock language="css">foo[]</codeBlock>' );
+			} );
+
+			it( 'should not leave the block when the selection is not collapsed', () => {
+				setModelData( model, '<codeBlock language="css">f[oo<softBreak></softBreak>]</codeBlock>' );
+
+				viewDoc.fire( 'enter', getEvent() );
+
+				expect( getModelData( model ) ).to.equal(
+					'<codeBlock language="css">f<softBreak></softBreak>[]</codeBlock>' );
+			} );
+
+			it( 'should not leave the block when pressed twice when in the middle of the code', () => {
+				setModelData( model, '<codeBlock language="css">fo[]o</codeBlock>' );
+
+				viewDoc.fire( 'enter', getEvent() );
+
+				expect( getModelData( model ) ).to.equal(
+					'<codeBlock language="css">fo<softBreak></softBreak>[]o</codeBlock>' );
+
+				viewDoc.fire( 'enter', getEvent() );
+
+				expect( getModelData( model ) ).to.equal(
+					'<codeBlock language="css">fo<softBreak></softBreak><softBreak></softBreak>[]o</codeBlock>' );
+			} );
+
+			it( 'should not leave the block when pressed twice at the beginning of the code', () => {
+				setModelData( model, '<codeBlock language="css">[]foo</codeBlock>' );
+
+				viewDoc.fire( 'enter', getEvent() );
+
+				expect( getModelData( model ) ).to.equal(
+					'<codeBlock language="css"><softBreak></softBreak>[]foo</codeBlock>' );
+
+				viewDoc.fire( 'enter', getEvent() );
+
+				expect( getModelData( model ) ).to.equal(
+					'<codeBlock language="css"><softBreak></softBreak><softBreak></softBreak>[]foo</codeBlock>' );
+			} );
+
+			it( 'should not leave the block when pressed shift+enter twice at the end of the code', () => {
+				setModelData( model, '<codeBlock language="css">foo<softBreak></softBreak>[]</codeBlock>' );
+
+				viewDoc.fire( 'enter', getEvent( { isSoft: true } ) );
+
+				expect( getModelData( model ) ).to.equal(
+					'<codeBlock language="css">foo<softBreak></softBreak><softBreak></softBreak>[]</codeBlock>' );
+			} );
+
+			it( 'should clean up the last line if has white–space characters only', () => {
+				setModelData( model, '<codeBlock language="css">foo<softBreak></softBreak>[]</codeBlock>' );
+
+				model.change( writer => {
+					// <codeBlock language="css">foo<softBreak></softBreak>  []</codeBlock>
+					writer.insertText( '  ', model.document.getRoot().getChild( 0 ), 4 );
+				} );
+
+				viewDoc.fire( 'enter', getEvent() );
+
+				expect( getModelData( model ) ).to.equal(
+					'<codeBlock language="css">foo</codeBlock><paragraph>[]</paragraph>' );
+			} );
 		} );
+
+		function getEvent( data = {} ) {
+			return new DomEventData( viewDoc, {
+				preventDefault: sinon.spy()
+			}, data );
+		}
 	} );
 
 	describe( 'editing pipeline m -> v', () => {

+ 7 - 2
packages/ckeditor5-code-block/tests/manual/codeblock.html

@@ -1,8 +1,13 @@
 <div id="editor">
 <pre><code class="plaintext">Plain text</code></pre>
 	<p>Getting used to an entirely different culture can be challenging. While it’s also nice to learn about cultures online or from books, nothing comes close to experiencing cultural diversity in person. You learn to appreciate each and every single one of the differences while you become more culturally fluid.</p>
-<pre><code class="javascript">foo()
-bar()</code></pre>
+<pre><code class="javascript">function foo() {
+	console.log( 'indented using 1 tab' );
+}
+
+function bar() {
+    console.log( 'indented using spaces' );
+}</code></pre>
 	<p>Getting used to an entirely different culture can be challenging. While it’s also nice to learn about cultures online or from books, nothing comes close to experiencing cultural diversity in person. You learn to appreciate each and every single one of the differences while you become more culturally fluid.</p>
 <pre><code class="css">body { color: red }</code></pre>
 <pre><code class="css">p { font-size: 10px; }</code></pre>