From 832da062cccc814fe5748e60a9c71f03290a6eff Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 22 Jan 2020 13:37:27 +0000
Subject: [PATCH] Improve trailing spurious breaks + tests

---
 src/editor/deserialize.js      |  2 +-
 src/editor/operations.js       | 16 +++++---
 test/editor/operations-test.js | 67 ++++++++++++++++++++++++++--------
 3 files changed, 63 insertions(+), 22 deletions(-)

diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js
index 7ba4c3eda3..190963f357 100644
--- a/src/editor/deserialize.js
+++ b/src/editor/deserialize.js
@@ -250,7 +250,7 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) {
 }
 
 export function parsePlainTextMessage(body, partCreator, isQuotedMessage) {
-    const lines = body.split(/\r\n|\r|\n/g); // split on any new-line combination not just \n
+    const lines = body.split(/\r\n|\r|\n/g); // split on any new-line combination not just \n, collapses \r\n
     const parts = lines.reduce((parts, line, i) => {
         if (isQuotedMessage) {
             parts.push(partCreator.plain(QUOTE_LINE_PREFIX));
diff --git a/src/editor/operations.js b/src/editor/operations.js
index 6bae60e6b8..d0115d9ca7 100644
--- a/src/editor/operations.js
+++ b/src/editor/operations.js
@@ -100,6 +100,10 @@ export function formatRangeAsCode(range) {
     replaceRangeAndExpandSelection(range, parts);
 }
 
+// parts helper methods
+const isBlank = part => !part.text || !/\S/.test(part.text);
+const isNL = part => part.type === "newline";
+
 export function toggleInlineFormat(range, prefix, suffix = prefix) {
     const {model, parts} = range;
     const {partCreator} = model;
@@ -113,14 +117,12 @@ export function toggleInlineFormat(range, prefix, suffix = prefix) {
         // - 2 newline parts in sequence
         // - newline part, plain(<empty or just spaces>), newline part
 
-        const isBlank = part => !part.text || !/\S/.test(part.text);
-        const isNL = part => part.type === "newline";
-
         // bump startIndex onto the first non-blank after the paragraph ending
         if (isBlank(parts[i - 2]) && isNL(parts[i - 1]) && !isNL(parts[i]) && !isBlank(parts[i])) {
             startIndex = i;
         }
 
+        // if at a paragraph break, store the indexes of the paragraph
         if (isNL(parts[i - 1]) && isNL(parts[i])) {
             paragraphIndexes.push([startIndex, i - 1]);
             startIndex = i + 1;
@@ -129,9 +131,11 @@ export function toggleInlineFormat(range, prefix, suffix = prefix) {
             startIndex = i + 1;
         }
     }
-    if (startIndex < parts.length) {
-        // TODO don't use parts.length here to clean up any trailing cruft
-        paragraphIndexes.push([startIndex, parts.length]);
+
+    const lastNonEmptyPart = parts.map(isBlank).lastIndexOf(false);
+    // If we have not yet included the final paragraph then add it now
+    if (startIndex <= lastNonEmptyPart) {
+        paragraphIndexes.push([startIndex, lastNonEmptyPart + 1]);
     }
 
     // keep track of how many things we have inserted as an offset:=0
diff --git a/test/editor/operations-test.js b/test/editor/operations-test.js
index 872cc78bdb..90a9812306 100644
--- a/test/editor/operations-test.js
+++ b/test/editor/operations-test.js
@@ -18,6 +18,8 @@ import EditorModel from "../../src/editor/model";
 import {createPartCreator, createRenderer} from "./mock";
 import {toggleInlineFormat} from "../../src/editor/operations";
 
+const SERIALIZED_NEWLINE = {"text": "\n", "type": "newline"};
+
 describe('editor/operations: formatting operations', () => {
     describe('toggleInlineFormat', () => {
         it('works for words', () => {
@@ -93,17 +95,54 @@ describe('editor/operations: formatting operations', () => {
             expect(range.parts.map(p => p.text).join("")).toBe("world,\nhow");
             expect(model.serializeParts()).toEqual([
                 {"text": "hello world,", "type": "plain"},
-                {"text": "\n", "type": "newline"},
+                SERIALIZED_NEWLINE,
                 {"text": "how are you doing?", "type": "plain"},
             ]);
             toggleInlineFormat(range, "**");
             expect(model.serializeParts()).toEqual([
                 {"text": "hello **world,", "type": "plain"},
-                {"text": "\n", "type": "newline"},
+                SERIALIZED_NEWLINE,
                 {"text": "how** are you doing?", "type": "plain"},
             ]);
         });
 
+        it('works for a paragraph with spurious breaks around it in selected range', () => {
+            const renderer = createRenderer();
+            const pc = createPartCreator();
+            const model = new EditorModel([
+                pc.newline(),
+                pc.newline(),
+                pc.plain("hello world,"),
+                pc.newline(),
+                pc.plain("how are you doing?"),
+                pc.newline(),
+                pc.newline(),
+            ], pc, renderer);
+
+            const range = model.startRange(model.positionForOffset(0, false), model.getPositionAtEnd());  // select-all
+
+            expect(range.parts.map(p => p.text).join("")).toBe("\n\nhello world,\nhow are you doing?\n\n");
+            expect(model.serializeParts()).toEqual([
+                SERIALIZED_NEWLINE,
+                SERIALIZED_NEWLINE,
+                {"text": "hello world,", "type": "plain"},
+                SERIALIZED_NEWLINE,
+                {"text": "how are you doing?", "type": "plain"},
+                SERIALIZED_NEWLINE,
+                SERIALIZED_NEWLINE,
+            ]);
+            toggleInlineFormat(range, "**");
+            expect(model.serializeParts()).toEqual([
+                SERIALIZED_NEWLINE,
+                SERIALIZED_NEWLINE,
+                {"text": "**hello world,", "type": "plain"},
+                SERIALIZED_NEWLINE,
+                {"text": "how are you doing?**", "type": "plain"},
+                SERIALIZED_NEWLINE,
+                SERIALIZED_NEWLINE,
+            ]);
+        });
+
         it('works for multiple paragraph', () => {
             const renderer = createRenderer();
             const pc = createPartCreator();
@@ -116,36 +155,34 @@ describe('editor/operations: formatting operations', () => {
                 pc.plain("new paragraph"),
             ], pc, renderer);
 
-            let range = model.startRange(model.positionForOffset(0, true),
-                model.getPositionAtEnd()); // select-all
+            let range = model.startRange(model.positionForOffset(0, true), model.getPositionAtEnd()); // select-all
 
             expect(model.serializeParts()).toEqual([
                 {"text": "hello world,", "type": "plain"},
-                {"text": "\n", "type": "newline"},
+                SERIALIZED_NEWLINE,
                 {"text": "how are you doing?", "type": "plain"},
-                {"text": "\n", "type": "newline"},
-                {"text": "\n", "type": "newline"},
+                SERIALIZED_NEWLINE,
+                SERIALIZED_NEWLINE,
                 {"text": "new paragraph", "type": "plain"},
             ]);
             toggleInlineFormat(range, "__");
             expect(model.serializeParts()).toEqual([
                 {"text": "__hello world,", "type": "plain"},
-                {"text": "\n", "type": "newline"},
+                SERIALIZED_NEWLINE,
                 {"text": "how are you doing?__", "type": "plain"},
-                {"text": "\n", "type": "newline"},
-                {"text": "\n", "type": "newline"},
+                SERIALIZED_NEWLINE,
+                SERIALIZED_NEWLINE,
                 {"text": "__new paragraph__", "type": "plain"},
             ]);
-            range = model.startRange(model.positionForOffset(0, true),
-                model.getPositionAtEnd()); // select-all
+            range = model.startRange(model.positionForOffset(0, true), model.getPositionAtEnd()); // select-all
             console.log("RANGE", range.parts);
             toggleInlineFormat(range, "__");
             expect(model.serializeParts()).toEqual([
                 {"text": "hello world,", "type": "plain"},
-                {"text": "\n", "type": "newline"},
+                SERIALIZED_NEWLINE,
                 {"text": "how are you doing?", "type": "plain"},
-                {"text": "\n", "type": "newline"},
-                {"text": "\n", "type": "newline"},
+                SERIALIZED_NEWLINE,
+                SERIALIZED_NEWLINE,
                 {"text": "new paragraph", "type": "plain"},
             ]);
         });