diff --git a/app/src/main/java/net/gsantner/markor/format/AutoFormatter.java b/app/src/main/java/net/gsantner/markor/format/AutoFormatter.java index 8ce8bedfa..c2108071e 100644 --- a/app/src/main/java/net/gsantner/markor/format/AutoFormatter.java +++ b/app/src/main/java/net/gsantner/markor/format/AutoFormatter.java @@ -11,6 +11,7 @@ import android.annotation.SuppressLint; import android.text.Editable; +import android.text.SpannableStringBuilder; import android.text.Spanned; import net.gsantner.opoc.util.StringUtils; @@ -236,7 +237,7 @@ public UnOrderedOrCheckListLine(CharSequence text, int position, PrefixPatterns * @param position Position within current line * @return OrderedListLine corresponding to top of current list */ - private static OrderedListLine getOrderedListStart(final Editable text, int position, final PrefixPatterns prefixPatterns) { + private static OrderedListLine getOrderedListStart(final CharSequence text, int position, final PrefixPatterns prefixPatterns) { position = Math.max(Math.min(position, text.length() - 1), 0); OrderedListLine listStart = new OrderedListLine(text, position, prefixPatterns); @@ -261,14 +262,18 @@ private static OrderedListLine getOrderedListStart(final Editable text, int posi *

* This is an unfortunately complex + complicated function. Tweak at your peril and test a *lot* :) */ - public static void renumberOrderedList(final Editable text, int cursorPosition, final PrefixPatterns prefixPatterns) { + public static void renumberOrderedList(final Editable edit, int cursorPosition, final PrefixPatterns prefixPatterns) { // Top of list - final OrderedListLine firstLine = getOrderedListStart(text, cursorPosition, prefixPatterns); + final OrderedListLine firstLine = getOrderedListStart(edit, cursorPosition, prefixPatterns); if (!firstLine.isOrderedList) { return; } + // Copy all the text if we are going to process + // SpannableStringBuilder makes the spans _appear_ to transition smoothly + final Editable text = new SpannableStringBuilder(edit); + // Stack represents each level in the list up from current final Stack levels = new Stack<>(); levels.push(firstLine); @@ -277,6 +282,7 @@ public static void renumberOrderedList(final Editable text, int cursorPosition, int position; try { + boolean madeChange = false; // Loop to end of list while (firstLine.isParentLevelOf(line) || firstLine.isMatchingList(line)) { @@ -312,7 +318,9 @@ else if (!line.isEmpty) { final OrderedListLine peek = levels.peek(); final String newValue = line.equals(peek) ? "1" : getNextOrderedValue(peek.value); if (!newValue.equals(line.value)) { + text.replace(line.numStart, line.numEnd, newValue); + madeChange = true; // Re-create line as it has changed line = new OrderedListLine(text, line.lineStart, prefixPatterns); @@ -329,6 +337,13 @@ else if (!line.isEmpty) { break; } } + + // Replace the text in Editable in one chunk + if (madeChange) { + final int[] diff = StringUtils.findDiff(edit, text); + edit.replace(diff[0], diff[1], text.subSequence(diff[0], diff[2])); + } + } catch (EmptyStackException ex) { // Usually means that indents and de-indents did not match up ex.printStackTrace(); diff --git a/app/src/main/java/net/gsantner/markor/ui/hleditor/HighlightingEditor.java b/app/src/main/java/net/gsantner/markor/ui/hleditor/HighlightingEditor.java index f03ed0a97..7b388c1ea 100644 --- a/app/src/main/java/net/gsantner/markor/ui/hleditor/HighlightingEditor.java +++ b/app/src/main/java/net/gsantner/markor/ui/hleditor/HighlightingEditor.java @@ -167,17 +167,28 @@ public void removeTextChangedListener(final TextWatcher listener) { _activeListeners.remove(listener); } + // Run some code with accessibility disabled + public void withAccessibilityDisabled(final Callback.a0 callback) { + try { + _accessibilityEnabled = false; + callback.callback(); + } finally { + _accessibilityEnabled = true; + } + } + // Run some code with auto formatters disabled + // Also disables accessibility public void withAutoFormatDisabled(final Callback.a0 callback) { if (getAutoFormatEnabled()) { try { setAutoFormatEnabled(false); - callback.callback(); + withAccessibilityDisabled(() -> callback.callback()); } finally { setAutoFormatEnabled(true); } } else { - callback.callback(); + withAccessibilityDisabled(() -> callback.callback()); } } @@ -213,16 +224,14 @@ private void highlightWithoutChange() { if (MainActivity.IS_DEBUG_ENABLED) { AppSettings.appendDebugLog("Start highlighting"); } - setAccessibilityEnabled(false); - _hl.run(getText()); + withAccessibilityDisabled(() ->_hl.run(getText())); } catch (Exception e) { // In no case ever let highlighting crash the editor e.printStackTrace(); } catch (Error e) { e.printStackTrace(); - } finally { - setAccessibilityEnabled(true); } + if (MainActivity.IS_DEBUG_ENABLED) { AppSettings.appendDebugLog(_hl._profiler.resetDebugText()); AppSettings.appendDebugLog("Finished highlighting"); @@ -323,12 +332,4 @@ protected void onSelectionChanged(int selStart, int selEnd) { AppSettings.appendDebugLog("Selection changed: " + selStart + "->" + selEnd); } } - - public void setAccessibilityEnabled(final boolean enabled) { - _accessibilityEnabled = enabled; - } - - public boolean getAccessibilityEnabled() { - return _accessibilityEnabled; - } } diff --git a/app/src/main/java/net/gsantner/markor/ui/hleditor/TextActions.java b/app/src/main/java/net/gsantner/markor/ui/hleditor/TextActions.java index 9f68b042f..01fa29d35 100644 --- a/app/src/main/java/net/gsantner/markor/ui/hleditor/TextActions.java +++ b/app/src/main/java/net/gsantner/markor/ui/hleditor/TextActions.java @@ -388,15 +388,10 @@ public static void runRegexReplaceAction(final EditText editor, final ReplacePat * @param matchAll Whether to stop matching subsequent ReplacePatterns after first match+replace */ public static void runRegexReplaceAction(final EditText editor, final List patterns, final boolean matchAll) { - try { - if (editor instanceof HighlightingEditor) { - ((HighlightingEditor) editor).setAccessibilityEnabled(false); - } + if (editor instanceof HighlightingEditor) { + ((HighlightingEditor) editor).withAutoFormatDisabled(() -> _runRegexReplaceAction(editor, patterns, matchAll)); + } else { _runRegexReplaceAction(editor, patterns, matchAll); - } finally { - if (editor instanceof HighlightingEditor) { - ((HighlightingEditor) editor).setAccessibilityEnabled(true); - } } } @@ -772,15 +767,7 @@ public final void runRenumberOrderedListIfRequired() { public final void runRenumberOrderedListIfRequired(final boolean force) { if (force || _hlEditor.getAutoFormatEnabled()) { - final boolean isAccessibilityEnabled = _hlEditor.getAccessibilityEnabled(); - try { - _hlEditor.setAccessibilityEnabled(false); - _hlEditor.withAutoFormatDisabled(() -> renumberOrderedList(StringUtils.getSelection(_hlEditor)[0])); - } finally { - if (isAccessibilityEnabled) { - _hlEditor.setAccessibilityEnabled(true); - } - } + _hlEditor.withAutoFormatDisabled(() -> renumberOrderedList(StringUtils.getSelection(_hlEditor)[0])); } } diff --git a/app/src/main/java/net/gsantner/opoc/util/StringUtils.java b/app/src/main/java/net/gsantner/opoc/util/StringUtils.java index d91d9fa0d..8a51d4267 100644 --- a/app/src/main/java/net/gsantner/opoc/util/StringUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/StringUtils.java @@ -452,4 +452,30 @@ public static String interpolateEscapedDateTime(final String snip) { interpolated.append(temp); // Remaining text return interpolated.toString(); } + + // Find the smallest single difference region { a, b, c } + // s.t. setting dest[a:b] = source[a:c] makes dest == source + public static int[] findDiff(final CharSequence dest, final CharSequence source) { + + final int dl = dest.length(), sl = source.length(); + final int minLength = Math.min(dl, sl); + + int start = 0; + while(start < minLength && source.charAt(start) == dest.charAt(start)) start++; + + // Handle several special cases + if (sl == dl && start == sl) { // Case where 2 sequences are same + return new int[] { sl, sl, sl }; + } else if (sl < dl && start == sl) { // Pure crop + return new int[] { sl, dl, sl }; + } else if (dl < sl && start == dl) { // Pure append + return new int[] { dl, dl, sl }; + } + + int end = 0; + final int maxEnd = minLength - start; + while(end < maxEnd && source.charAt(sl - end - 1) == dest.charAt(dl - end - 1)) end++; + + return new int[] { start, dl - end, sl - end }; + } } diff --git a/app/src/test/java/net/gsantner/markor/StringUtilsTest.java b/app/src/test/java/net/gsantner/markor/StringUtilsTest.java new file mode 100644 index 000000000..9d9d539a7 --- /dev/null +++ b/app/src/test/java/net/gsantner/markor/StringUtilsTest.java @@ -0,0 +1,29 @@ +package net.gsantner.markor; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.gsantner.opoc.util.StringUtils; + +import org.junit.Test; + +public class StringUtilsTest { + + @Test + public void findDiffTest() { + assertThat(StringUtils.findDiff("", "")).isEqualTo(new int[] {0, 0, 0}); + assertThat(StringUtils.findDiff("abcd", "abcd")).isEqualTo(new int[] {4, 4, 4}); + assertThat(StringUtils.findDiff("ab", "abcd")).isEqualTo(new int[] {2, 2, 4}); + assertThat(StringUtils.findDiff("abcd", "ab")).isEqualTo(new int[] {2, 4, 2}); + assertThat(StringUtils.findDiff("ab1d", "ab2d")).isEqualTo(new int[] {2, 3, 3}); + assertThat(StringUtils.findDiff("ab12d", "ab34d")).isEqualTo(new int[] {2, 4, 4}); + assertThat(StringUtils.findDiff("ab12d", "ab3d")).isEqualTo(new int[] {2, 4, 3}); + assertThat(StringUtils.findDiff("ab12d", "abd")).isEqualTo(new int[] {2, 4, 2}); + assertThat(StringUtils.findDiff("abd", "ab12d")).isEqualTo(new int[] {2, 2, 4}); + assertThat(StringUtils.findDiff("abcd", "")).isEqualTo(new int[] {0, 4, 0}); + assertThat(StringUtils.findDiff("", "abcd")).isEqualTo(new int[] {0, 0, 4}); + assertThat(StringUtils.findDiff("ab11d", "ab1d")).isEqualTo(new int[] {3, 4, 3}); + assertThat(StringUtils.findDiff("aaaaa", "aaa")).isEqualTo(new int[] {3, 5, 3}); + assertThat(StringUtils.findDiff("aaa", "aaaaa")).isEqualTo(new int[] {3, 3, 5}); + } +} +