From 89793a83ea3de703dbd02f7e2362ba4fcb161179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Thu, 18 Apr 2024 16:16:22 +0200 Subject: [PATCH 01/14] Fix cursor position after undoing previously entered text --- src/web/InputHistory.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/web/InputHistory.ts b/src/web/InputHistory.ts index 86713abf..607c5c65 100644 --- a/src/web/InputHistory.ts +++ b/src/web/InputHistory.ts @@ -42,17 +42,22 @@ export default class InputHistory { } debouncedAdd(text: string, cursorPosition: number): void { - this.currentText = text; - if (this.timeout) { clearTimeout(this.timeout); } + if (this.currentText === null) { + this.add(text, cursorPosition); + } else { + this.history[this.historyIndex] = {text, cursorPosition}; + } + this.currentText = text; + this.timeout = setTimeout(() => { if (this.currentText == null) { return; } - this.add(this.currentText, cursorPosition); + this.currentText = null; }, this.debounceTime); } @@ -79,10 +84,21 @@ export default class InputHistory { } undo(): HistoryItem | null { + const currentHistoryItem = this.history[this.historyIndex]; + const previousHistoryItem = this.history[this.historyIndex - 1]; + + const undoCursorPosition = Math.min( + (currentHistoryItem?.cursorPosition ?? 0) - ((currentHistoryItem?.text ?? '').replaceAll('\n', '').length - (previousHistoryItem?.text ?? '').replaceAll('\n', '').length), + (previousHistoryItem?.text ?? '').length, + ); + + const undoItem = previousHistoryItem ? {text: previousHistoryItem.text, cursorPosition: undoCursorPosition} : null; + if (this.currentText !== null && this.timeout) { clearTimeout(this.timeout); this.timeout = null; - return this.history[this.historyIndex] || null; + + return undoItem; } if (this.history.length === 0 || this.historyIndex - 1 < 0) { @@ -92,7 +108,8 @@ export default class InputHistory { if (this.historyIndex > 0) { this.historyIndex -= 1; } - return this.history[this.historyIndex] || null; + + return undoItem; } redo(): HistoryItem | null { From 5384df573a72ad5343410716f58baf7c70d2a3e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Tue, 7 May 2024 11:12:17 +0200 Subject: [PATCH 02/14] Fix cursor position setting --- src/web/InputHistory.ts | 42 +++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/web/InputHistory.ts b/src/web/InputHistory.ts index 607c5c65..6a58bc7a 100644 --- a/src/web/InputHistory.ts +++ b/src/web/InputHistory.ts @@ -47,21 +47,29 @@ export default class InputHistory { } if (this.currentText === null) { + this.timeout = null; this.add(text, cursorPosition); + if (this.history.length === 1) { + return; + } } else { this.history[this.historyIndex] = {text, cursorPosition}; } this.currentText = text; this.timeout = setTimeout(() => { - if (this.currentText == null) { - return; - } - this.currentText = null; }, this.debounceTime); } + stopTimeout(): void { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; + } + this.currentText = null; + } + add(text: string, cursorPosition: number): void { if (this.history.length > 0) { const lastItem = this.history[this.history.length - 1]; @@ -84,22 +92,20 @@ export default class InputHistory { } undo(): HistoryItem | null { + this.stopTimeout(); + const currentHistoryItem = this.history[this.historyIndex]; const previousHistoryItem = this.history[this.historyIndex - 1]; - const undoCursorPosition = Math.min( - (currentHistoryItem?.cursorPosition ?? 0) - ((currentHistoryItem?.text ?? '').replaceAll('\n', '').length - (previousHistoryItem?.text ?? '').replaceAll('\n', '').length), - (previousHistoryItem?.text ?? '').length, - ); - - const undoItem = previousHistoryItem ? {text: previousHistoryItem.text, cursorPosition: undoCursorPosition} : null; - - if (this.currentText !== null && this.timeout) { - clearTimeout(this.timeout); - this.timeout = null; - - return undoItem; - } + const undoItem = previousHistoryItem + ? { + text: previousHistoryItem.text, + cursorPosition: Math.min( + (currentHistoryItem?.cursorPosition ?? 0) - ((currentHistoryItem?.text ?? '').replaceAll('\n', '').length - (previousHistoryItem?.text ?? '').replaceAll('\n', '').length), + (previousHistoryItem?.text ?? '').length, + ), + } + : null; if (this.history.length === 0 || this.historyIndex - 1 < 0) { return null; @@ -114,7 +120,7 @@ export default class InputHistory { redo(): HistoryItem | null { if (this.currentText !== null && this.timeout) { - clearTimeout(this.timeout); + this.stopTimeout(); return this.history[this.history.length - 1] || null; } From cf85a1be4e62df9c3b946db1b58e9beff2ce08a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Tue, 7 May 2024 14:48:28 +0200 Subject: [PATCH 03/14] Fix adding first element --- src/MarkdownTextInput.web.tsx | 6 +++++- src/__tests__/webInputHistory.test.tsx | 21 ++++++++++++--------- src/web/InputHistory.ts | 3 --- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index 4ded68c5..256383f6 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -203,7 +203,11 @@ const MarkdownTextInput = React.forwardRef( } const parsedText = ParseUtils.parseText(target, text, cursorPosition, customMarkdownStyles, !multiline); if (history.current && shouldAddToHistory) { - history.current.debouncedAdd(parsedText.text, parsedText.cursorPosition); + if (history.current.history.length === 0) { + history.current.add(parsedText.text, parsedText.cursorPosition); + } else { + history.current.debouncedAdd(parsedText.text, parsedText.cursorPosition); + } } return parsedText; diff --git a/src/__tests__/webInputHistory.test.tsx b/src/__tests__/webInputHistory.test.tsx index d5279e6e..6d3274a9 100644 --- a/src/__tests__/webInputHistory.test.tsx +++ b/src/__tests__/webInputHistory.test.tsx @@ -7,6 +7,7 @@ const testingHistory = [ {text: 'Hello _*world*_!', cursorPosition: 16}, ]; const depth = testingHistory.length; +const debounceTime = 150; test('add history action', () => { const history = new InputHistory(depth); @@ -48,35 +49,37 @@ describe('debounce add history action', () => { }); test('should debounce', () => { - const history = new InputHistory(depth, 300); + const history = new InputHistory(depth, debounceTime); history.debouncedAdd(newItem.text, newItem.cursorPosition); - expect(history.history).toEqual([]); - jest.advanceTimersByTime(300); expect(history.history).toEqual([newItem]); + history.debouncedAdd(newItem2.text, newItem2.cursorPosition); + expect(history.history).toEqual([newItem2]); + jest.advanceTimersByTime(debounceTime); + expect(history.history).toEqual([newItem2]); }); test('should cancel previous invocation', () => { - const history = new InputHistory(depth, 300); + const history = new InputHistory(depth, debounceTime); history.debouncedAdd(newItem.text, newItem.cursorPosition); jest.advanceTimersByTime(100); history.debouncedAdd(newItem2.text, newItem2.cursorPosition); - jest.advanceTimersByTime(300); + jest.advanceTimersByTime(debounceTime); expect(history.history).toEqual([newItem2]); }); test('undo before debounce invokes the function', () => { - const history = new InputHistory(depth, 300); + const history = new InputHistory(depth, debounceTime); history.debouncedAdd(newItem.text, newItem.cursorPosition); expect(history.undo()).toEqual(null); - jest.advanceTimersByTime(300); + jest.advanceTimersByTime(debounceTime); expect(history.history).toEqual([]); }); test('redo before debounce invokes the function', () => { - const history = new InputHistory(depth, 300); + const history = new InputHistory(depth, debounceTime); history.debouncedAdd(newItem.text, newItem.cursorPosition); expect(history.redo()).toEqual(null); - jest.advanceTimersByTime(300); + jest.advanceTimersByTime(debounceTime); expect(history.history).toEqual([]); }); }); diff --git a/src/web/InputHistory.ts b/src/web/InputHistory.ts index 6a58bc7a..be0bed13 100644 --- a/src/web/InputHistory.ts +++ b/src/web/InputHistory.ts @@ -49,9 +49,6 @@ export default class InputHistory { if (this.currentText === null) { this.timeout = null; this.add(text, cursorPosition); - if (this.history.length === 1) { - return; - } } else { this.history[this.historyIndex] = {text, cursorPosition}; } From 0fd1bff38f5b9a6daac96c47910b9a14ac0e37d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Tue, 7 May 2024 15:22:09 +0200 Subject: [PATCH 04/14] Add default item when creating InputHistory --- src/MarkdownTextInput.web.tsx | 10 +---- src/__tests__/webInputHistory.test.tsx | 18 ++++----- src/web/InputHistory.ts | 53 +++++++++++++------------- 3 files changed, 37 insertions(+), 44 deletions(-) diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index 256383f6..0e4a113b 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -174,7 +174,7 @@ const MarkdownTextInput = React.forwardRef( const dimensions = React.useRef(null); if (!history.current) { - history.current = new InputHistory(100); + history.current = new InputHistory(100, 150, value || ''); } const flattenedStyle = useMemo(() => StyleSheet.flatten(style), [style]); @@ -203,7 +203,7 @@ const MarkdownTextInput = React.forwardRef( } const parsedText = ParseUtils.parseText(target, text, cursorPosition, customMarkdownStyles, !multiline); if (history.current && shouldAddToHistory) { - if (history.current.history.length === 0) { + if (history.current.items.length === 0) { history.current.add(parsedText.text, parsedText.cursorPosition); } else { history.current.debouncedAdd(parsedText.text, parsedText.cursorPosition); @@ -592,12 +592,6 @@ const MarkdownTextInput = React.forwardRef( }, [selection, updateRefSelectionVariables]); useEffect(() => { - if (history.current?.history.length !== 0) { - return; - } - const currentValue = value ?? ''; - history.current.add(currentValue, currentValue.length); - handleContentSizeChange(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/__tests__/webInputHistory.test.tsx b/src/__tests__/webInputHistory.test.tsx index 6d3274a9..e1624077 100644 --- a/src/__tests__/webInputHistory.test.tsx +++ b/src/__tests__/webInputHistory.test.tsx @@ -15,7 +15,7 @@ test('add history action', () => { history.add(item.text, item.cursorPosition); }); - expect(history.history).toEqual(testingHistory); + expect(history.items).toEqual(testingHistory); expect(history.getCurrentItem()).toEqual(testingHistory[testingHistory.length - 1]); }); @@ -29,7 +29,7 @@ test('history depth', () => { const newItem = {text, cursorPosition: text.length}; const currentHistory = [...testingHistory.slice(1), newItem]; - expect(history.history).toEqual(currentHistory); + expect(history.items).toEqual(currentHistory); expect(history.getCurrentItem()).toEqual(newItem); }); @@ -51,11 +51,9 @@ describe('debounce add history action', () => { test('should debounce', () => { const history = new InputHistory(depth, debounceTime); history.debouncedAdd(newItem.text, newItem.cursorPosition); - expect(history.history).toEqual([newItem]); - history.debouncedAdd(newItem2.text, newItem2.cursorPosition); - expect(history.history).toEqual([newItem2]); + expect(history.items).toEqual([]); jest.advanceTimersByTime(debounceTime); - expect(history.history).toEqual([newItem2]); + expect(history.items).toEqual([newItem]); }); test('should cancel previous invocation', () => { @@ -64,7 +62,7 @@ describe('debounce add history action', () => { jest.advanceTimersByTime(100); history.debouncedAdd(newItem2.text, newItem2.cursorPosition); jest.advanceTimersByTime(debounceTime); - expect(history.history).toEqual([newItem2]); + expect(history.items).toEqual([newItem2]); }); test('undo before debounce invokes the function', () => { @@ -72,7 +70,7 @@ describe('debounce add history action', () => { history.debouncedAdd(newItem.text, newItem.cursorPosition); expect(history.undo()).toEqual(null); jest.advanceTimersByTime(debounceTime); - expect(history.history).toEqual([]); + expect(history.items).toEqual([]); }); test('redo before debounce invokes the function', () => { @@ -80,7 +78,7 @@ describe('debounce add history action', () => { history.debouncedAdd(newItem.text, newItem.cursorPosition); expect(history.redo()).toEqual(null); jest.advanceTimersByTime(debounceTime); - expect(history.history).toEqual([]); + expect(history.items).toEqual([]); }); }); @@ -113,6 +111,6 @@ test('clearing history after adding new text after undo', () => { history.add(newItem.text, newItem.cursorPosition); - expect(history.history).toEqual([testingHistory[0], newItem]); + expect(history.items).toEqual([testingHistory[0], newItem]); expect(history.getCurrentItem()).toEqual(newItem); }); diff --git a/src/web/InputHistory.ts b/src/web/InputHistory.ts index be0bed13..31e249c7 100644 --- a/src/web/InputHistory.ts +++ b/src/web/InputHistory.ts @@ -6,7 +6,7 @@ type HistoryItem = { export default class InputHistory { depth: number; - history: HistoryItem[]; + items: HistoryItem[]; historyIndex: number; @@ -16,19 +16,20 @@ export default class InputHistory { debounceTime: number; - constructor(depth: number, debounceTime = 150) { + constructor(depth: number, debounceTime = 150, startingText = '') { this.depth = depth; - this.history = []; + this.items = []; this.historyIndex = 0; this.debounceTime = debounceTime; + this.add(startingText, startingText.length); } getCurrentItem(): HistoryItem | null { - return this.history[this.historyIndex] || null; + return this.items[this.historyIndex] || null; } setHistory(newHistory: HistoryItem[]): void { - this.history = newHistory.slice(newHistory.length - this.depth); + this.items = newHistory.slice(newHistory.length - this.depth); this.historyIndex = newHistory.length - 1; } @@ -37,7 +38,7 @@ export default class InputHistory { } clear(): void { - this.history = []; + this.items = []; this.historyIndex = 0; } @@ -50,7 +51,7 @@ export default class InputHistory { this.timeout = null; this.add(text, cursorPosition); } else { - this.history[this.historyIndex] = {text, cursorPosition}; + this.items[this.historyIndex] = {text, cursorPosition}; } this.currentText = text; @@ -68,31 +69,35 @@ export default class InputHistory { } add(text: string, cursorPosition: number): void { - if (this.history.length > 0) { - const lastItem = this.history[this.history.length - 1]; + if (this.items.length > 0) { + const lastItem = this.items[this.items.length - 1]; if (lastItem && text === lastItem.text) { - this.historyIndex = this.history.length - 1; + this.historyIndex = this.items.length - 1; return; } } - if (this.historyIndex < this.history.length - 1) { - this.history.splice(this.historyIndex + 1); + if (this.historyIndex < this.items.length - 1) { + this.items.splice(this.historyIndex + 1); } - this.history.push({text, cursorPosition}); - if (this.history.length > this.depth) { - this.history.shift(); + this.items.push({text, cursorPosition}); + if (this.items.length > this.depth) { + this.items.shift(); } - this.historyIndex = this.history.length - 1; + this.historyIndex = this.items.length - 1; } undo(): HistoryItem | null { this.stopTimeout(); - const currentHistoryItem = this.history[this.historyIndex]; - const previousHistoryItem = this.history[this.historyIndex - 1]; + if (this.items.length === 0 || this.historyIndex - 1 < 0) { + return null; + } + + const currentHistoryItem = this.items[this.historyIndex]; + const previousHistoryItem = this.items[this.historyIndex - 1]; const undoItem = previousHistoryItem ? { @@ -104,10 +109,6 @@ export default class InputHistory { } : null; - if (this.history.length === 0 || this.historyIndex - 1 < 0) { - return null; - } - if (this.historyIndex > 0) { this.historyIndex -= 1; } @@ -118,19 +119,19 @@ export default class InputHistory { redo(): HistoryItem | null { if (this.currentText !== null && this.timeout) { this.stopTimeout(); - return this.history[this.history.length - 1] || null; + return this.items[this.items.length - 1] || null; } - if (this.history.length === 0 || this.historyIndex + 1 > this.history.length) { + if (this.items.length === 0 || this.historyIndex + 1 > this.items.length) { return null; } - if (this.historyIndex < this.history.length - 1) { + if (this.historyIndex < this.items.length - 1) { this.historyIndex += 1; } else { return null; } - return this.history[this.historyIndex] || null; + return this.items[this.historyIndex] || null; } } From 8393e370a3b5d9c495d593a63077024e7bb81a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 8 May 2024 09:16:10 +0200 Subject: [PATCH 05/14] Update test cases --- src/__tests__/webInputHistory.test.tsx | 117 +++++++++++++++---------- src/web/InputHistory.ts | 1 - 2 files changed, 71 insertions(+), 47 deletions(-) diff --git a/src/__tests__/webInputHistory.test.tsx b/src/__tests__/webInputHistory.test.tsx index e1624077..ebe8fd1a 100644 --- a/src/__tests__/webInputHistory.test.tsx +++ b/src/__tests__/webInputHistory.test.tsx @@ -1,6 +1,9 @@ import {expect} from '@jest/globals'; import InputHistory from '../web/InputHistory'; +const defaultItemText = ''; +const defaultItem = {text: defaultItemText, cursorPosition: defaultItemText.length}; + const testingHistory = [ {text: 'Hello world!', cursorPosition: 12}, {text: 'Hello *world*!', cursorPosition: 14}, @@ -9,6 +12,12 @@ const testingHistory = [ const depth = testingHistory.length; const debounceTime = 150; +test('history default item', () => { + const history = new InputHistory(depth, debounceTime, defaultItemText); + expect(history.getCurrentItem()).toEqual(defaultItem); + expect(history.items).toEqual([defaultItem]); +}); + test('add history action', () => { const history = new InputHistory(depth); testingHistory.forEach((item) => { @@ -33,6 +42,43 @@ test('history depth', () => { expect(history.getCurrentItem()).toEqual(newItem); }); +test('undo history action', () => { + const history = new InputHistory(depth); + history.setHistory(testingHistory); + + expect(history.undo()).toEqual(testingHistory[1]); + expect(history.getCurrentItem()).toEqual(testingHistory[1]); + + history.setHistoryIndex(0); + expect(history.undo()).toEqual(null); + expect(history.getCurrentItem()).toEqual(testingHistory[0]); +}); + +test('redo history action', () => { + const history = new InputHistory(depth); + history.setHistory(testingHistory); + expect(history.redo()).toEqual(null); + expect(history.getCurrentItem()).toEqual(testingHistory[testingHistory.length - 1]); + + history.setHistoryIndex(1); + expect(history.redo()).toEqual(testingHistory[2]); + expect(history.getCurrentItem()).toEqual(testingHistory[2]); +}); + +test('clearing history after adding new text after undo', () => { + const history = new InputHistory(depth); + history.setHistory(testingHistory); + history.setHistoryIndex(0); + + const text = '> Hello _*world*_!'; + const newItem = {text, cursorPosition: text.length}; + + history.add(newItem.text, newItem.cursorPosition); + + expect(history.items).toEqual([testingHistory[0], newItem]); + expect(history.getCurrentItem()).toEqual(newItem); +}); + describe('debounce add history action', () => { const text = 'Hello world!'; const newItem = {text, cursorPosition: text.length}; @@ -49,68 +95,47 @@ describe('debounce add history action', () => { }); test('should debounce', () => { - const history = new InputHistory(depth, debounceTime); + const history = new InputHistory(depth, debounceTime, defaultItemText); history.debouncedAdd(newItem.text, newItem.cursorPosition); - expect(history.items).toEqual([]); + expect(history.items).toEqual([defaultItem, newItem]); + history.debouncedAdd(newItem2.text, newItem2.cursorPosition); + expect(history.items).toEqual([defaultItem, newItem2]); + jest.advanceTimersByTime(debounceTime); - expect(history.items).toEqual([newItem]); + history.debouncedAdd(newItem.text, newItem.cursorPosition); + expect(history.items).toEqual([defaultItem, newItem2, newItem]); }); test('should cancel previous invocation', () => { const history = new InputHistory(depth, debounceTime); history.debouncedAdd(newItem.text, newItem.cursorPosition); - jest.advanceTimersByTime(100); + jest.advanceTimersByTime(debounceTime / 2); history.debouncedAdd(newItem2.text, newItem2.cursorPosition); jest.advanceTimersByTime(debounceTime); - expect(history.items).toEqual([newItem2]); + expect(history.items).toEqual([defaultItem, newItem2]); }); - test('undo before debounce invokes the function', () => { + test('undo before debounce ends', () => { const history = new InputHistory(depth, debounceTime); history.debouncedAdd(newItem.text, newItem.cursorPosition); - expect(history.undo()).toEqual(null); - jest.advanceTimersByTime(debounceTime); - expect(history.items).toEqual([]); + expect(history.undo()).toEqual(defaultItem); + expect(history.getCurrentItem()).toEqual(defaultItem); + history.debouncedAdd(newItem2.text, newItem2.cursorPosition); + expect(history.items).toEqual([defaultItem, newItem2]); + expect(history.getCurrentItem()).toEqual(newItem2); }); - test('redo before debounce invokes the function', () => { + test('redo before debounce ends', () => { + const text3 = 'Hello world 3!'; + const newItem3 = {text: text3, cursorPosition: text3.length}; + const history = new InputHistory(depth, debounceTime); - history.debouncedAdd(newItem.text, newItem.cursorPosition); + history.setHistory(testingHistory); + history.setHistoryIndex(1); + + history.debouncedAdd(newItem3.text, newItem3.cursorPosition); expect(history.redo()).toEqual(null); - jest.advanceTimersByTime(debounceTime); - expect(history.items).toEqual([]); + expect(history.getCurrentItem()).toEqual(newItem3); + expect(history.items).toEqual([testingHistory[0], testingHistory[1], newItem3]); }); }); - -test('undo history action', () => { - const history = new InputHistory(depth); - history.setHistory(testingHistory); - - expect(history.undo()).toEqual(testingHistory[1]); - - history.setHistoryIndex(0); - expect(history.undo()).toEqual(null); -}); - -test('redo history action', () => { - const history = new InputHistory(depth); - history.setHistory(testingHistory); - expect(history.redo()).toEqual(null); - - history.setHistoryIndex(1); - expect(history.redo()).toEqual(testingHistory[2]); -}); - -test('clearing history after adding new text after undo', () => { - const history = new InputHistory(depth); - history.setHistory(testingHistory); - history.setHistoryIndex(0); - - const text = '> Hello _*world*_!'; - const newItem = {text, cursorPosition: text.length}; - - history.add(newItem.text, newItem.cursorPosition); - - expect(history.items).toEqual([testingHistory[0], newItem]); - expect(history.getCurrentItem()).toEqual(newItem); -}); diff --git a/src/web/InputHistory.ts b/src/web/InputHistory.ts index 31e249c7..eb215275 100644 --- a/src/web/InputHistory.ts +++ b/src/web/InputHistory.ts @@ -119,7 +119,6 @@ export default class InputHistory { redo(): HistoryItem | null { if (this.currentText !== null && this.timeout) { this.stopTimeout(); - return this.items[this.items.length - 1] || null; } if (this.items.length === 0 || this.historyIndex + 1 > this.items.length) { From b3b62c2dce96b7c7b7b2dd7e88046a4b3d959d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 8 May 2024 09:23:04 +0200 Subject: [PATCH 06/14] Fix redo before debounce ends test --- src/__tests__/webInputHistory.test.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/__tests__/webInputHistory.test.tsx b/src/__tests__/webInputHistory.test.tsx index ebe8fd1a..51c4f028 100644 --- a/src/__tests__/webInputHistory.test.tsx +++ b/src/__tests__/webInputHistory.test.tsx @@ -126,16 +126,13 @@ describe('debounce add history action', () => { }); test('redo before debounce ends', () => { - const text3 = 'Hello world 3!'; - const newItem3 = {text: text3, cursorPosition: text3.length}; - const history = new InputHistory(depth, debounceTime); history.setHistory(testingHistory); history.setHistoryIndex(1); - history.debouncedAdd(newItem3.text, newItem3.cursorPosition); + history.debouncedAdd(newItem2.text, newItem2.cursorPosition); expect(history.redo()).toEqual(null); - expect(history.getCurrentItem()).toEqual(newItem3); - expect(history.items).toEqual([testingHistory[0], testingHistory[1], newItem3]); + expect(history.getCurrentItem()).toEqual(newItem2); + expect(history.items).toEqual([testingHistory[0], testingHistory[1], newItem2]); }); }); From a024db048ac72a47959ed2d5a807e9c6af881ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 8 May 2024 09:38:52 +0200 Subject: [PATCH 07/14] Change function name --- src/MarkdownTextInput.web.tsx | 6 +----- src/__tests__/webInputHistory.test.tsx | 16 ++++++++-------- src/web/InputHistory.ts | 2 +- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index 0e4a113b..a13eeac9 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -203,11 +203,7 @@ const MarkdownTextInput = React.forwardRef( } const parsedText = ParseUtils.parseText(target, text, cursorPosition, customMarkdownStyles, !multiline); if (history.current && shouldAddToHistory) { - if (history.current.items.length === 0) { - history.current.add(parsedText.text, parsedText.cursorPosition); - } else { - history.current.debouncedAdd(parsedText.text, parsedText.cursorPosition); - } + history.current.throttledAdd(parsedText.text, parsedText.cursorPosition); } return parsedText; diff --git a/src/__tests__/webInputHistory.test.tsx b/src/__tests__/webInputHistory.test.tsx index 51c4f028..63f57d8b 100644 --- a/src/__tests__/webInputHistory.test.tsx +++ b/src/__tests__/webInputHistory.test.tsx @@ -96,31 +96,31 @@ describe('debounce add history action', () => { test('should debounce', () => { const history = new InputHistory(depth, debounceTime, defaultItemText); - history.debouncedAdd(newItem.text, newItem.cursorPosition); + history.throttledAdd(newItem.text, newItem.cursorPosition); expect(history.items).toEqual([defaultItem, newItem]); - history.debouncedAdd(newItem2.text, newItem2.cursorPosition); + history.throttledAdd(newItem2.text, newItem2.cursorPosition); expect(history.items).toEqual([defaultItem, newItem2]); jest.advanceTimersByTime(debounceTime); - history.debouncedAdd(newItem.text, newItem.cursorPosition); + history.throttledAdd(newItem.text, newItem.cursorPosition); expect(history.items).toEqual([defaultItem, newItem2, newItem]); }); test('should cancel previous invocation', () => { const history = new InputHistory(depth, debounceTime); - history.debouncedAdd(newItem.text, newItem.cursorPosition); + history.throttledAdd(newItem.text, newItem.cursorPosition); jest.advanceTimersByTime(debounceTime / 2); - history.debouncedAdd(newItem2.text, newItem2.cursorPosition); + history.throttledAdd(newItem2.text, newItem2.cursorPosition); jest.advanceTimersByTime(debounceTime); expect(history.items).toEqual([defaultItem, newItem2]); }); test('undo before debounce ends', () => { const history = new InputHistory(depth, debounceTime); - history.debouncedAdd(newItem.text, newItem.cursorPosition); + history.throttledAdd(newItem.text, newItem.cursorPosition); expect(history.undo()).toEqual(defaultItem); expect(history.getCurrentItem()).toEqual(defaultItem); - history.debouncedAdd(newItem2.text, newItem2.cursorPosition); + history.throttledAdd(newItem2.text, newItem2.cursorPosition); expect(history.items).toEqual([defaultItem, newItem2]); expect(history.getCurrentItem()).toEqual(newItem2); }); @@ -130,7 +130,7 @@ describe('debounce add history action', () => { history.setHistory(testingHistory); history.setHistoryIndex(1); - history.debouncedAdd(newItem2.text, newItem2.cursorPosition); + history.throttledAdd(newItem2.text, newItem2.cursorPosition); expect(history.redo()).toEqual(null); expect(history.getCurrentItem()).toEqual(newItem2); expect(history.items).toEqual([testingHistory[0], testingHistory[1], newItem2]); diff --git a/src/web/InputHistory.ts b/src/web/InputHistory.ts index eb215275..d94d30f9 100644 --- a/src/web/InputHistory.ts +++ b/src/web/InputHistory.ts @@ -42,7 +42,7 @@ export default class InputHistory { this.historyIndex = 0; } - debouncedAdd(text: string, cursorPosition: number): void { + throttledAdd(text: string, cursorPosition: number): void { if (this.timeout) { clearTimeout(this.timeout); } From 82844c1b9666e59c73e8ddd728b662925b53c252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 8 May 2024 13:13:35 +0200 Subject: [PATCH 08/14] Fix redo after pasing text bug --- src/MarkdownTextInput.web.tsx | 15 ++++++++++++--- src/web/InputHistory.ts | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index a13eeac9..acdbadc8 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -328,9 +328,10 @@ const MarkdownTextInput = React.forwardRef( if (!divRef.current || !(e.target instanceof HTMLElement)) { return; } + const changedText = e.target.innerText; if (compositionRef.current) { - updateTextColor(divRef.current, e.target.innerText); + updateTextColor(divRef.current, changedText); compositionRef.current = false; return; } @@ -344,14 +345,22 @@ const MarkdownTextInput = React.forwardRef( case 'historyRedo': text = redo(divRef.current); break; + case 'insertFromPaste': + // if there is no newline at the end of the copied text, contentEditable adds invisible
tag at the end of the text, so we need to normalize it + if (changedText.length > 2 && changedText[changedText.length - 2] !== '\n' && changedText[changedText.length - 1] === '\n') { + text = parseText(divRef.current, normalizeValue(changedText), processedMarkdownStyle).text; + break; + } + text = parseText(divRef.current, changedText, processedMarkdownStyle).text; + break; default: - text = parseText(divRef.current, e.target.innerText, processedMarkdownStyle).text; + text = parseText(divRef.current, changedText, processedMarkdownStyle).text; } if (pasteRef?.current) { pasteRef.current = false; updateSelection(e); } - updateTextColor(divRef.current, e.target.innerText); + updateTextColor(divRef.current, changedText); if (onChange) { const event = e as unknown as NativeSyntheticEvent; diff --git a/src/web/InputHistory.ts b/src/web/InputHistory.ts index d94d30f9..b9b55dc1 100644 --- a/src/web/InputHistory.ts +++ b/src/web/InputHistory.ts @@ -103,7 +103,7 @@ export default class InputHistory { ? { text: previousHistoryItem.text, cursorPosition: Math.min( - (currentHistoryItem?.cursorPosition ?? 0) - ((currentHistoryItem?.text ?? '').replaceAll('\n', '').length - (previousHistoryItem?.text ?? '').replaceAll('\n', '').length), + (currentHistoryItem?.cursorPosition ?? 0) - ((currentHistoryItem?.text ?? '').length - (previousHistoryItem?.text ?? '').length), (previousHistoryItem?.text ?? '').length, ), } From e42a8f1f176b6847fc20b11a8412a2114dae913f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 8 May 2024 13:20:26 +0200 Subject: [PATCH 09/14] Remove duplicated code --- src/MarkdownTextInput.web.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index acdbadc8..2c367589 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -596,11 +596,6 @@ const MarkdownTextInput = React.forwardRef( CursorUtils.setCursorPosition(divRef.current, newSelection.start, newSelection.end); }, [selection, updateRefSelectionVariables]); - useEffect(() => { - handleContentSizeChange(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
Date: Fri, 10 May 2024 13:53:58 +0200 Subject: [PATCH 10/14] Fix history undo cycle when adding the same element --- src/web/InputHistory.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/web/InputHistory.ts b/src/web/InputHistory.ts index b9b55dc1..1ee6fa6d 100644 --- a/src/web/InputHistory.ts +++ b/src/web/InputHistory.ts @@ -69,9 +69,9 @@ export default class InputHistory { } add(text: string, cursorPosition: number): void { - if (this.items.length > 0) { - const lastItem = this.items[this.items.length - 1]; - if (lastItem && text === lastItem.text) { + if (this.historyIndex + 1 < this.items.length) { + const nextItem = this.items[this.historyIndex + 1]; + if (nextItem && text === nextItem.text) { this.historyIndex = this.items.length - 1; return; } From f8b127230c73b71c1fcbe02a2d109e465397da76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Tue, 14 May 2024 11:26:54 +0200 Subject: [PATCH 11/14] Fix text coloring after redo --- src/MarkdownTextInput.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index 2c367589..d586bef6 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -360,7 +360,7 @@ const MarkdownTextInput = React.forwardRef( pasteRef.current = false; updateSelection(e); } - updateTextColor(divRef.current, changedText); + updateTextColor(divRef.current, text); if (onChange) { const event = e as unknown as NativeSyntheticEvent; From e6ec16e9883df177bec32eda8ca0578c6dbd8fed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Tue, 14 May 2024 12:07:11 +0200 Subject: [PATCH 12/14] Fix hisotry element adding --- src/web/InputHistory.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/web/InputHistory.ts b/src/web/InputHistory.ts index 1ee6fa6d..fd8b9770 100644 --- a/src/web/InputHistory.ts +++ b/src/web/InputHistory.ts @@ -18,10 +18,9 @@ export default class InputHistory { constructor(depth: number, debounceTime = 150, startingText = '') { this.depth = depth; - this.items = []; + this.items = [{text: startingText, cursorPosition: startingText.length}]; this.historyIndex = 0; this.debounceTime = debounceTime; - this.add(startingText, startingText.length); } getCurrentItem(): HistoryItem | null { @@ -69,24 +68,24 @@ export default class InputHistory { } add(text: string, cursorPosition: number): void { - if (this.historyIndex + 1 < this.items.length) { - const nextItem = this.items[this.historyIndex + 1]; - if (nextItem && text === nextItem.text) { - this.historyIndex = this.items.length - 1; + if (this.items.length > 0) { + const currentItem = this.items[this.historyIndex]; + if (currentItem && text === currentItem.text) { return; } } if (this.historyIndex < this.items.length - 1) { this.items.splice(this.historyIndex + 1); + this.historyIndex = this.items.length - 1; } this.items.push({text, cursorPosition}); if (this.items.length > this.depth) { this.items.shift(); + } else { + this.historyIndex += 1; } - - this.historyIndex = this.items.length - 1; } undo(): HistoryItem | null { From 5b133f29a4281b076c7bf4b71cb8418b3adbf95c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Tue, 14 May 2024 12:13:32 +0200 Subject: [PATCH 13/14] Add history index test --- src/__tests__/webInputHistory.test.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/__tests__/webInputHistory.test.tsx b/src/__tests__/webInputHistory.test.tsx index 63f57d8b..e56f268b 100644 --- a/src/__tests__/webInputHistory.test.tsx +++ b/src/__tests__/webInputHistory.test.tsx @@ -32,7 +32,12 @@ test('history depth', () => { const history = new InputHistory(depth); const text = '> Hello _*world*_!'; - history.setHistory(testingHistory); + const nextHistoryIndexes = [1, 2, 2]; + testingHistory.forEach((item, index) => { + history.add(item.text, item.cursorPosition); + expect(history.historyIndex).toEqual(nextHistoryIndexes[index]); + }); + history.add(text, text.length); const newItem = {text, cursorPosition: text.length}; From 1e94c9fe93156e72c2afeb676a7ccefd76e50801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Tue, 14 May 2024 15:49:54 +0200 Subject: [PATCH 14/14] Fix cursor positioning when undoing deleted text --- src/MarkdownTextInput.web.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index d586bef6..322282ef 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -80,6 +80,10 @@ let focusTimeout: NodeJS.Timeout | null = null; function normalizeValue(value: string) { return value.replace(/\n$/, ''); } +// Adds one '\n' at the end of the string if it's missing +function denormalizeValue(value: string) { + return value.endsWith('\n') ? `${value}\n` : value; +} // If an Input Method Editor is processing key input, the 'keyCode' is 229. // https://www.w3.org/TR/uievents/#determine-keydown-keyup-keyCode @@ -203,7 +207,8 @@ const MarkdownTextInput = React.forwardRef( } const parsedText = ParseUtils.parseText(target, text, cursorPosition, customMarkdownStyles, !multiline); if (history.current && shouldAddToHistory) { - history.current.throttledAdd(parsedText.text, parsedText.cursorPosition); + // We need to normalize the value before saving it to the history to prevent situations when additional new lines break the cursor position calculation logic + history.current.throttledAdd(normalizeValue(parsedText.text), parsedText.cursorPosition); } return parsedText; @@ -236,7 +241,8 @@ const MarkdownTextInput = React.forwardRef( (target: HTMLDivElement) => { if (!history.current) return ''; const item = history.current.undo(); - return parseText(target, item ? item.text : null, processedMarkdownStyle, item ? item.cursorPosition : null, false).text; + const undoValue = item ? denormalizeValue(item.text) : null; + return parseText(target, undoValue, processedMarkdownStyle, item ? item.cursorPosition : null, false).text; }, [parseText, processedMarkdownStyle], ); @@ -245,7 +251,8 @@ const MarkdownTextInput = React.forwardRef( (target: HTMLDivElement) => { if (!history.current) return ''; const item = history.current.redo(); - return parseText(target, item ? item.text : null, processedMarkdownStyle, item ? item.cursorPosition : null, false).text; + const redoValue = item ? denormalizeValue(item.text) : null; + return parseText(target, redoValue, processedMarkdownStyle, item ? item.cursorPosition : null, false).text; }, [parseText, processedMarkdownStyle], );