+ * As the text is modified, the memory usage will finally go up to the same usage of full copy when all lines are edited. + *
+ * This function is experimental, and is disabled by default.
+ */
+ @Experimental
+ public static void setUseShallowCopyByDefault(boolean useShallowCopy) {
+ useShallowCopyByDefault = useShallowCopy;
+ }
+
+ /**
+ * @see #setUseShallowCopyByDefault(boolean)
+ */
+ public static boolean isUseShallowCopyByDefault() {
+ return useShallowCopyByDefault;
+ }
+
+ public AsyncIncrementalAnalyzeManager() {
+ this(isUseShallowCopyByDefault());
+ }
+
+ public AsyncIncrementalAnalyzeManager(boolean useShallowCopy) {
+ this.useShallowCopy = useShallowCopy;
+ }
private synchronized static int nextThreadId() {
sThreadId++;
@@ -107,7 +138,7 @@ public void rerun() {
}
var ref = getContentRef();
if (ref != null) {
- final var text = ref.getReference().copyText(false);
+ final var text = ref.getReference().copyText(false, useShallowCopy);
text.setUndoEnabled(false);
thread = new LooperThread();
thread.setName("AsyncAnalyzer-" + nextThreadId());
@@ -588,6 +619,10 @@ public void run() {
}
} catch (InterruptedException e) {
// ignored
+ } finally {
+ if (useShallowCopy && shadowed != null) {
+ shadowed.release();
+ }
}
}
}
diff --git a/editor/src/main/java/io/github/rosemoe/sora/lang/completion/snippet/CodeSnippet.java b/editor/src/main/java/io/github/rosemoe/sora/lang/completion/snippet/CodeSnippet.java
index 565664e2e..9584d4f93 100644
--- a/editor/src/main/java/io/github/rosemoe/sora/lang/completion/snippet/CodeSnippet.java
+++ b/editor/src/main/java/io/github/rosemoe/sora/lang/completion/snippet/CodeSnippet.java
@@ -25,8 +25,9 @@
import androidx.annotation.NonNull;
+import androidx.collection.IntObjectMap;
+import androidx.collection.MutableIntObjectMap;
-import io.github.rosemoe.sora.text.TextUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -78,11 +79,11 @@ public List
* If you use {@link BufferedWriter}, make sure you set an appropriate buffer size. We recommend using the default size (8192) or larger.
*
- * @param text Text to be written
- * @param writer Output writer
- * @param closeOnSucceed If true, the stream will be closed when operation is successfully
+ * @param text Text to be written
+ * @param writer Output writer
+ * @param closeOnSucceed If true, the stream will be closed when operation is successful
*/
public static void writeTo(@NonNull Content text, @NonNull Writer writer, boolean closeOnSucceed) throws IOException {
// Use buffered writer to avoid frequently IO when there are a lot of short lines
- final var buffered = (writer instanceof BufferedWriter) ? (BufferedWriter)writer : new BufferedWriter(writer, BUFFER_SIZE);
+ final var buffered = (writer instanceof BufferedWriter) ? (BufferedWriter) writer : new BufferedWriter(writer, BUFFER_SIZE);
+ char[] buf = new char[CHAR_BUFFER_SIZE];
try {
text.runReadActionsOnLines(0, text.getLineCount() - 1, (Content.ContentLineConsumer2) (index, line, flag) -> {
try {
// Write line content
- buffered.write(line.getBackingCharArray(), 0, line.length());
+ for (int i = 0; i < line.length(); ) {
+ int end = Math.min(i + buf.length, line.length());
+ line.getChars(i, end, buf, 0);
+ buffered.write(buf, 0, end - i);
+ i = end;
+ }
// Write line feed (the last line has empty line feed)
buffered.write(line.getLineSeparator().getChars());
} catch (IOException e) {
diff --git a/editor/src/main/java/io/github/rosemoe/sora/text/ContentLine.java b/editor/src/main/java/io/github/rosemoe/sora/text/ContentLine.java
index 6a2f2d0f3..e9831c80a 100644
--- a/editor/src/main/java/io/github/rosemoe/sora/text/ContentLine.java
+++ b/editor/src/main/java/io/github/rosemoe/sora/text/ContentLine.java
@@ -33,16 +33,22 @@
import io.github.rosemoe.sora.annotations.UnsupportedUserUsage;
import io.github.rosemoe.sora.text.bidi.BidiRequirementChecker;
import io.github.rosemoe.sora.text.bidi.TextBidi;
+import io.github.rosemoe.sora.text.string.StringLatin1;
+import io.github.rosemoe.sora.text.string.StringUTF16;
import io.github.rosemoe.sora.util.ShareableData;
public class ContentLine implements CharSequence, GetChars, BidiRequirementChecker, ShareableData
@@ -172,12 +232,14 @@ public ContentLine insert(int dstOffset, @Nullable CharSequence s,
"start " + start + ", end " + end + ", s.length() "
+ s.length());
int len = end - start;
+ if (coder == LATIN1 && requiresUTF16(s, start, end)) {
+ toUTF16IfNeeded(length + len);
+ }
ensureCapacity(length + len);
- System.arraycopy(value, dstOffset, value, dstOffset + len,
- length - dstOffset);
+ copyCharsInternal(value, dstOffset, value, dstOffset + len, length - dstOffset);
for (int i = start; i < end; i++) {
var ch = s.charAt(i);
- value[dstOffset++] = ch;
+ putCharInternal(dstOffset++, ch);
if (TextBidi.couldAffectRtl(ch)) {
rtlAffectingCount++;
}
@@ -188,14 +250,17 @@ public ContentLine insert(int dstOffset, @Nullable CharSequence s,
@NonNull
public ContentLine insert(int offset, char c) {
+ if (coder == LATIN1 && !StringLatin1.canEncode(c)) {
+ toUTF16IfNeeded(length + 1);
+ }
ensureCapacity(length + 1);
if (offset < length) {
- System.arraycopy(value, offset, value, offset + 1, length - offset);
+ copyCharsInternal(value, offset, value, offset + 1, length - offset);
}
if (TextBidi.couldAffectRtl(c)) {
rtlAffectingCount++;
}
- value[offset] = c;
+ putCharInternal(offset, c);
length += 1;
return this;
}
@@ -225,11 +290,11 @@ public ContentLine delete(int start, int end) {
int len = end - start;
if (len > 0) {
for (int i = start; i < end; i++) {
- if (TextBidi.couldAffectRtl(value[i])) {
+ if (TextBidi.couldAffectRtl(charAtInternal(i))) {
rtlAffectingCount--;
}
}
- System.arraycopy(value, start + len, value, start, length - end);
+ copyCharsInternal(value, start + len, value, start, length - end);
length -= len;
}
return this;
@@ -269,7 +334,7 @@ public char charAt(int index) {
var separator = getLineSeparator();
return separator.getLength() > 0 ? getLineSeparator().getContent().charAt(index - length) : '\n';
}
- return value[index];
+ return charAtInternal(index);
}
@Override
@@ -280,16 +345,21 @@ public ContentLine subSequence(int start, int end) {
if (end < start) {
throw new StringIndexOutOfBoundsException("start is greater than end");
}
- char[] newValue = new char[end - start + 16];
- System.arraycopy(value, start, newValue, 0, end - start);
var res = new ContentLine(false);
- res.value = newValue;
+ res.coder = coder;
+ if (res.coder == LATIN1) {
+ res.value = new byte[end - start + 16];
+ StringLatin1.copyChars(value, start, res.value, 0, end - start);
+ } else {
+ res.value = new byte[StringUTF16.bytesForChars(end - start + 16)];
+ StringUTF16.copyChars(value, start, res.value, 0, end - start);
+ }
res.length = end - start;
// Compute new value when required
if (rtlAffectingCount > 0) {
for (int i = 0; i < res.length; i++) {
- if (TextBidi.couldAffectRtl(newValue[i])) {
+ if (TextBidi.couldAffectRtl(res.charAt(i))) {
res.rtlAffectingCount++;
}
}
@@ -301,13 +371,17 @@ public ContentLine subSequence(int start, int end) {
* A convenient method to append text to a StringBuilder
*/
public void appendTo(@NonNull StringBuilder sb) {
- sb.append(value, 0, length);
+ if (coder == LATIN1) {
+ StringLatin1.appendTo(value, length, sb);
+ } else {
+ StringUTF16.appendTo(value, length, sb);
+ }
}
@Override
@NonNull
public String toString() {
- return new String(value, 0, length);
+ return coder == LATIN1 ? StringLatin1.newString(value, length) : StringUTF16.newString(value, length);
}
/**
@@ -316,22 +390,14 @@ public String toString() {
*/
@NonNull
public String toStringWithNewline() {
- if (value.length == length) {
+ if (charCapacity() == length) {
ensureCapacity(length + 1);
}
- value[length] = '\n';
- return new String(value, 0, length + 1);
- }
-
- /**
- * Get the backing char array of this object.
- * The result array should not be modified.
- */
- @NonNull
- public char[] getBackingCharArray() {
- return value;
+ putCharInternal(length, '\n');
+ return coder == LATIN1 ? StringLatin1.newString(value, length + 1) : StringUTF16.newString(value, length + 1);
}
+ @Override
public void getChars(int srcBegin, int srcEnd, @NonNull char[] dst, int dstBegin) {
if (srcBegin < 0)
throw new StringIndexOutOfBoundsException(srcBegin);
@@ -339,7 +405,11 @@ public void getChars(int srcBegin, int srcEnd, @NonNull char[] dst, int dstBegin
throw new StringIndexOutOfBoundsException(srcEnd);
if (srcBegin > srcEnd)
throw new StringIndexOutOfBoundsException("srcBegin > srcEnd");
- System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
+ if (coder == LATIN1) {
+ StringLatin1.getChars(value, srcBegin, srcEnd, dst, dstBegin);
+ } else {
+ StringUTF16.getChars(value, srcBegin, srcEnd, dst, dstBegin);
+ }
}
public void setLineSeparator(@Nullable LineSeparator separator) {
@@ -362,8 +432,9 @@ public LineSeparator getLineSeparator() {
public ContentLine copy() {
var clone = new ContentLine(false);
clone.length = length;
- clone.value = new char[value.length];
- System.arraycopy(value, 0, clone.value, 0, length);
+ clone.coder = coder;
+ clone.value = new byte[value.length];
+ System.arraycopy(value, 0, clone.value, 0, value.length);
clone.rtlAffectingCount = rtlAffectingCount;
clone.lineSeparator = lineSeparator;
return clone;
diff --git a/editor/src/main/java/io/github/rosemoe/sora/text/TextUtils.java b/editor/src/main/java/io/github/rosemoe/sora/text/TextUtils.java
index 3e8a2852f..e64f55d25 100644
--- a/editor/src/main/java/io/github/rosemoe/sora/text/TextUtils.java
+++ b/editor/src/main/java/io/github/rosemoe/sora/text/TextUtils.java
@@ -168,7 +168,7 @@ public static String padStart(String src, char padChar, int length) {
*
* @param line The line to search
*/
- public static long findLeadingAndTrailingWhitespacePos(ContentLine line) {
+ public static long findLeadingAndTrailingWhitespacePos(CharSequence line) {
return findLeadingAndTrailingWhitespacePos(line, 0, line.length());
}
@@ -179,16 +179,15 @@ public static long findLeadingAndTrailingWhitespacePos(ContentLine line) {
* @param start Range start (inclusive)
* @param end Range end (exclusive)
*/
- public static long findLeadingAndTrailingWhitespacePos(ContentLine line, int start, int end) {
- var buffer = line.getBackingCharArray();
+ public static long findLeadingAndTrailingWhitespacePos(CharSequence line, int start, int end) {
int leading = start;
int trailing = end;
- while (leading < end && isWhitespace(buffer[leading])) {
+ while (leading < end && isWhitespace(line.charAt(leading))) {
leading++;
}
// Skip for space-filled line
if (leading != end) {
- while (trailing > 0 && isWhitespace(buffer[trailing - 1])) {
+ while (trailing > 0 && isWhitespace(line.charAt(trailing - 1))) {
trailing--;
}
}
diff --git a/editor/src/main/java/io/github/rosemoe/sora/text/breaker/WordBreakerIcu.java b/editor/src/main/java/io/github/rosemoe/sora/text/breaker/WordBreakerIcu.java
index 19f45499f..9a7b21e04 100644
--- a/editor/src/main/java/io/github/rosemoe/sora/text/breaker/WordBreakerIcu.java
+++ b/editor/src/main/java/io/github/rosemoe/sora/text/breaker/WordBreakerIcu.java
@@ -34,10 +34,10 @@ public class WordBreakerIcu implements WordBreaker {
protected final BreakIterator wrappingIterator;
- protected final char[] chars;
+ protected final ContentLine text;
public WordBreakerIcu(@NonNull ContentLine text) {
- this.chars = text.getBackingCharArray();
+ this.text = text;
var textIterator = new CharSequenceIterator(text);
wrappingIterator = BreakIterator.getLineInstance();
wrappingIterator.setText(textIterator);
@@ -45,7 +45,7 @@ public WordBreakerIcu(@NonNull ContentLine text) {
public int getOptimizedBreakPoint(int start, int end) {
// Merging trailing whitespaces is not supported by editor, so force to break here
- if (end > 0 && !Character.isWhitespace(chars[end - 1]) && !wrappingIterator.isBoundary(end)) {
+ if (end > 0 && !Character.isWhitespace(text.charAt(end - 1)) && !wrappingIterator.isBoundary(end)) {
// Break text at last boundary
int lastBoundary = wrappingIterator.preceding(end);
if (lastBoundary != BreakIterator.DONE) {
diff --git a/editor/src/main/java/io/github/rosemoe/sora/text/breaker/WordBreakerProgram.java b/editor/src/main/java/io/github/rosemoe/sora/text/breaker/WordBreakerProgram.java
index 704e51318..bbb0cb289 100644
--- a/editor/src/main/java/io/github/rosemoe/sora/text/breaker/WordBreakerProgram.java
+++ b/editor/src/main/java/io/github/rosemoe/sora/text/breaker/WordBreakerProgram.java
@@ -39,7 +39,7 @@ public WordBreakerProgram(@NonNull ContentLine text) {
@Override
public int getOptimizedBreakPoint(int start, int end) {
int icuResult = super.getOptimizedBreakPoint(start, end);
- if (icuResult != end || end <= start || /* end > start */ Character.isWhitespace(chars[end - 1])) {
+ if (icuResult != end || end <= start || /* end > start */ Character.isWhitespace(text.charAt(end - 1))) {
return icuResult;
}
// The content can be placed on a single row
@@ -49,7 +49,7 @@ public int getOptimizedBreakPoint(int start, int end) {
// Add extra opportunities for dots
int index = end - 1;
while (index > start) {
- if (chars[index] == '.' && index - 1 >= start && !Character.isDigit(chars[index - 1])) {
+ if (text.charAt(index) == '.' && index - 1 >= start && !Character.isDigit(text.charAt(index - 1))) {
// Break after this dot
return index + 1;
}
diff --git a/editor/src/main/java/io/github/rosemoe/sora/text/string/StringLatin1.java b/editor/src/main/java/io/github/rosemoe/sora/text/string/StringLatin1.java
new file mode 100644
index 000000000..49c49f417
--- /dev/null
+++ b/editor/src/main/java/io/github/rosemoe/sora/text/string/StringLatin1.java
@@ -0,0 +1,72 @@
+/*
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2026 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ */
+package io.github.rosemoe.sora.text.string;
+
+import java.nio.charset.StandardCharsets;
+
+public final class StringLatin1 {
+
+ private StringLatin1() {
+ }
+
+ public static boolean canEncode(char c) {
+ return c <= 0x00FF;
+ }
+
+ public static char getChar(byte[] value, int index) {
+ return (char) (value[index] & 0xFF);
+ }
+
+ public static void putChar(byte[] value, int index, char c) {
+ value[index] = (byte) c;
+ }
+
+ public static void copyChars(byte[] src, int srcBegin, byte[] dst, int dstBegin, int len) {
+ System.arraycopy(src, srcBegin, dst, dstBegin, len);
+ }
+
+ public static void getChars(byte[] value, int srcBegin, int srcEnd, char[] dst, int dstBegin) {
+ for (int i = srcBegin; i < srcEnd; i++) {
+ dst[dstBegin++] = getChar(value, i);
+ }
+ }
+
+ public static void appendTo(byte[] value, int length, StringBuilder sb) {
+ for (int i = 0; i < length; i++) {
+ sb.append(getChar(value, i));
+ }
+ }
+
+ public static String newString(byte[] value, int length) {
+ return new String(value, 0, length, StandardCharsets.ISO_8859_1);
+ }
+
+ public static byte[] inflateToUTF16(byte[] value, int length, int newCapacity) {
+ var utf16 = new byte[StringUTF16.bytesForChars(newCapacity)];
+ for (int i = 0; i < length; i++) {
+ StringUTF16.putChar(utf16, i, getChar(value, i));
+ }
+ return utf16;
+ }
+}
diff --git a/editor/src/main/java/io/github/rosemoe/sora/text/string/StringUTF16.java b/editor/src/main/java/io/github/rosemoe/sora/text/string/StringUTF16.java
new file mode 100644
index 000000000..a125594a0
--- /dev/null
+++ b/editor/src/main/java/io/github/rosemoe/sora/text/string/StringUTF16.java
@@ -0,0 +1,67 @@
+/*
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2026 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ */
+package io.github.rosemoe.sora.text.string;
+
+public final class StringUTF16 {
+
+ private StringUTF16() {
+ }
+
+ public static int bytesForChars(int chars) {
+ return chars << 1;
+ }
+
+ public static char getChar(byte[] value, int index) {
+ int i = index << 1;
+ return (char) (((value[i] & 0xFF) << 8) | (value[i + 1] & 0xFF));
+ }
+
+ public static void putChar(byte[] value, int index, char c) {
+ int i = index << 1;
+ value[i] = (byte) (c >>> 8);
+ value[i + 1] = (byte) c;
+ }
+
+ public static void copyChars(byte[] src, int srcBegin, byte[] dst, int dstBegin, int len) {
+ System.arraycopy(src, srcBegin << 1, dst, dstBegin << 1, len << 1);
+ }
+
+ public static void getChars(byte[] value, int srcBegin, int srcEnd, char[] dst, int dstBegin) {
+ for (int i = srcBegin; i < srcEnd; i++) {
+ dst[dstBegin++] = getChar(value, i);
+ }
+ }
+
+ public static void appendTo(byte[] value, int length, StringBuilder sb) {
+ for (int i = 0; i < length; i++) {
+ sb.append(getChar(value, i));
+ }
+ }
+
+ public static String newString(byte[] value, int length) {
+ var chars = new char[length];
+ getChars(value, 0, length, chars, 0);
+ return new String(chars);
+ }
+}
diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/CodeEditor.java b/editor/src/main/java/io/github/rosemoe/sora/widget/CodeEditor.java
index b4a12de6e..38a0ad695 100644
--- a/editor/src/main/java/io/github/rosemoe/sora/widget/CodeEditor.java
+++ b/editor/src/main/java/io/github/rosemoe/sora/widget/CodeEditor.java
@@ -1440,16 +1440,15 @@ public void setHardwareAcceleratedDrawAllowed(boolean acceleratedDraw) {
* @param line The line to search
*/
protected long findLeadingAndTrailingWhitespacePos(ContentLine line) {
- var buffer = line.getBackingCharArray();
int column = line.length();
int leading = 0;
int trailing = column;
- while (leading < column && isWhitespace(buffer[leading])) {
+ while (leading < column && isWhitespace(line.charAt(leading))) {
leading++;
}
// Only when this action is needed
if (leading != column && (nonPrintableOptions & (FLAG_DRAW_WHITESPACE_INNER | FLAG_DRAW_WHITESPACE_TRAILING)) != 0) {
- while (trailing > 0 && isWhitespace(buffer[trailing - 1])) {
+ while (trailing > 0 && isWhitespace(line.charAt(trailing - 1))) {
trailing--;
}
}
@@ -1948,10 +1947,10 @@ public void deleteText() {
int line = cur.getLeftLine();
if (props.deleteEmptyLineFast || (props.deleteMultiSpaces != 1 && col > 0 && text.charAt(line, col - 1) == ' ')) {
// Check whether selection is in leading spaces
- var text = this.text.getLine(cur.getLeftLine()).getBackingCharArray();
+ var text = this.text.getLine(cur.getLeftLine());
var inLeading = true;
for (int i = col - 1; i >= 0; i--) {
- char ch = text[i];
+ char ch = text.charAt(i);
if (ch != ' ' && ch != '\t') {
inLeading = false;
break;
@@ -1963,7 +1962,7 @@ public void deleteText() {
var emptyLine = true;
var max = this.text.getColumnCount(line);
for (int i = col; i < max; i++) {
- char ch = text[i];
+ char ch = text.charAt(i);
if (ch != ' ' && ch != '\t') {
emptyLine = false;
break;
diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/EditorRenderer.java b/editor/src/main/java/io/github/rosemoe/sora/widget/EditorRenderer.java
index 8a58894ee..878d7880a 100644
--- a/editor/src/main/java/io/github/rosemoe/sora/widget/EditorRenderer.java
+++ b/editor/src/main/java/io/github/rosemoe/sora/widget/EditorRenderer.java
@@ -40,6 +40,7 @@
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.SystemClock;
+import android.text.GetChars;
import android.util.Log;
import android.util.SparseArray;
@@ -48,7 +49,6 @@
import androidx.annotation.RequiresApi;
import androidx.collection.MutableIntList;
import androidx.collection.MutableLongLongMap;
-import androidx.collection.MutableLongObjectMap;
import java.util.ArrayList;
import java.util.Collections;
@@ -1447,14 +1447,14 @@ protected void drawRows(Canvas canvas, float offset, LongArrayList postDrawLineN
canvas.save();
canvas.translate(paintingOffset, editor.getRowTopOfText(row) - editor.getOffsetY());
bufferedDrawPoints.setOffsets(paintingOffset, editor.getRowTopOfText(row) - editor.getOffsetY());
- float beginOffset = Math.max(0, paintingOffset);
+ float beginOffset = Math.max(0, offsetCopy);
float endOffset = beginOffset + editor.getWidth();
final var wsLeadingEnd = leadingWhitespaceEnd;
final var wsTrailingStart = trailingWhitespaceStart;
paintOther.setColor(editor.getColorScheme().getColor(EditorColorScheme.NON_PRINTABLE_CHAR));
tr.iterateDrawTextRegions(rowInf.startColumn, rowInf.endColumn, canvas, beginOffset, endOffset, false,
- (Canvas _canvas, char[] text, int index, int count, int contextIndex, int contextCount, boolean isRtl,
+ (Canvas _canvas, GetChars text, int index, int count, int contextIndex, int contextCount, boolean isRtl,
float horizontalOffset, float width, TextRowParams params, Span span) -> {
if ((nonPrintableFlags & CodeEditor.FLAG_DRAW_WHITESPACE_LEADING) != 0) {
drawWhitespaces(_canvas, tr, text, index, count, contextIndex, contextCount, isRtl, horizontalOffset, width, 0, wsLeadingEnd);
@@ -1714,7 +1714,7 @@ protected void drawDiagnosticIndicators(Canvas canvas, float offset) {
/**
* Draw non-printable characters
*/
- private void drawWhitespaces(Canvas canvas, TextRow tr, char[] chars, int index, int count, int contextIndex, int contextCount, boolean isRtl, float horizontalOffset, float width, int min, int max) {
+ private void drawWhitespaces(Canvas canvas, TextRow tr, GetChars chars, int index, int count, int contextIndex, int contextCount, boolean isRtl, float horizontalOffset, float width, int min, int max) {
int paintStart = Math.max(index, Math.min(index + count, min));
int paintEnd = Math.max(index, Math.min(index + count, max));
@@ -1723,7 +1723,7 @@ private void drawWhitespaces(Canvas canvas, TextRow tr, char[] chars, int index,
float rowCenter = (editor.getRowHeightOfText() / 2f + editor.getRowTopOfText(0));
float offset = isRtl ? horizontalOffset + width : horizontalOffset;
while (paintStart < paintEnd) {
- char ch = chars[paintStart];
+ char ch = chars.charAt(paintStart);
int paintCount = 0;
boolean paintLine = false;
if (ch == ' ' || ch == '\t') {
@@ -2302,7 +2302,7 @@ protected void patchTextRegionWithColor(Canvas canvas, float textOffset, int sta
paintGeneral.setStyle(useBoldStyle ? Paint.Style.FILL_AND_STROKE : Paint.Style.FILL);
paintGeneral.setFakeBoldText(useBoldStyle);
- patchTextRegions(canvas, textOffset, start, end, (Canvas canvasLocal, char[] text, int index, int count, int contextIndex, int contextCount, boolean isRtl,
+ patchTextRegions(canvas, textOffset, start, end, (Canvas canvasLocal, GetChars text, int index, int count, int contextIndex, int contextCount, boolean isRtl,
float horizontalOffset, float width, TextRowParams params, Span span) -> {
if (span == null) {
return;
diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/layout/ViewMeasureHelper.java b/editor/src/main/java/io/github/rosemoe/sora/widget/layout/ViewMeasureHelper.java
index babe7f02f..3137c0c3b 100644
--- a/editor/src/main/java/io/github/rosemoe/sora/widget/layout/ViewMeasureHelper.java
+++ b/editor/src/main/java/io/github/rosemoe/sora/widget/layout/ViewMeasureHelper.java
@@ -60,7 +60,7 @@ public static long getDesiredSize(int widthMeasureSpec, int heightMeasureSpec, f
var lines = heightMode != View.MeasureSpec.EXACTLY ? new int[text.getLineCount()] : null;
var lineMaxSize = new MutableInt(0);
text.runReadActionsOnLines(0, text.getLineCount() - 1, (Content.ContentLineConsumer) (index, line, directions) -> {
- int measured = (int) Math.ceil(measurer.measureText(line.getBackingCharArray(), 0, line.length(), paint));
+ int measured = (int) Math.ceil(measurer.measureText(line, 0, line.length(), paint));
if (measured > lineMaxSize.value) {
lineMaxSize.value = measured;
}
@@ -91,7 +91,7 @@ public static long getDesiredSize(int widthMeasureSpec, int heightMeasureSpec, f
rowCount.value = text.length();
} else {
text.runReadActionsOnLines(0, text.getLineCount() - 1, (Content.ContentLineConsumer) (index, line, directions) -> {
- int measured = (int) Math.ceil(measurer.measureText(line.getBackingCharArray(), 0, line.length(), paint));
+ int measured = (int) Math.ceil(measurer.measureText(line, 0, line.length(), paint));
rowCount.value += Math.max(1, Math.ceil(1.0 * measured / availableSize));
});
}
@@ -103,7 +103,7 @@ public static long getDesiredSize(int widthMeasureSpec, int heightMeasureSpec, f
if (widthMode != View.MeasureSpec.EXACTLY) {
var lineMaxSize = new MutableInt(0);
text.runReadActionsOnLines(0, text.getLineCount() - 1, (Content.ContentLineConsumer) (index, line, directions) -> {
- int measured = (int) Math.ceil(measurer.measureText(line.getBackingCharArray(), 0, line.length(), paint));
+ int measured = (int) Math.ceil(measurer.measureText(line, 0, line.length(), paint));
if (measured > lineMaxSize.value) {
lineMaxSize.value = measured;
}
diff --git a/editor/src/test/java/io/github/rosemoe/sora/text/ContentLineTest.kt b/editor/src/test/java/io/github/rosemoe/sora/text/ContentLineTest.kt
new file mode 100644
index 000000000..c709321a2
--- /dev/null
+++ b/editor/src/test/java/io/github/rosemoe/sora/text/ContentLineTest.kt
@@ -0,0 +1,83 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2026 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.text
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class ContentLineTest {
+
+ @Test
+ fun `latin1 path should preserve content and char operations`() {
+ val line = ContentLine()
+ line.insert(0, "ab")
+ line.insert(2, '\u00FF')
+ line.insert(3, "cd")
+
+ assertThat(line.length).isEqualTo(5)
+ assertThat(line.toString()).isEqualTo("ab\u00FFcd")
+ assertThat(line[2]).isEqualTo('\u00FF')
+
+ val out = CharArray(line.length)
+ line.getChars(0, line.length, out, 0)
+ assertThat(String(out)).isEqualTo("ab\u00FFcd")
+
+ val sb = StringBuilder()
+ line.appendTo(sb)
+ assertThat(sb.toString()).isEqualTo("ab\u00FFcd")
+ }
+
+ @Test
+ fun `utf16 upgrade should happen when inserting non latin1 char`() {
+ val line = ContentLine("hello")
+ line.insert(5, '中')
+ line.insert(0, "前")
+
+ assertThat(line.toString()).isEqualTo("前hello中")
+ assertThat(line[0]).isEqualTo('前')
+ assertThat(line[6]).isEqualTo('中')
+
+ val out = CharArray(line.length)
+ line.getChars(0, line.length, out, 0)
+ assertThat(String(out)).isEqualTo("前hello中")
+
+ line.delete(1, 6)
+ assertThat(line.toString()).isEqualTo("前中")
+ }
+
+ @Test
+ fun `subSequence and copy should work after utf16 upgrade`() {
+ val line = ContentLine("a中b文c")
+
+ val sub = line.subSequence(1, 4)
+ assertThat(sub.toString()).isEqualTo("中b文")
+
+ val copied = line.copy()
+ copied.insert(copied.length, '末')
+ assertThat(copied.toString()).isEqualTo("a中b文c末")
+ assertThat(line.toString()).isEqualTo("a中b文c")
+ }
+
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ce3af9e48..95ad91f8a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,7 +2,7 @@
agp = "9.0.1"
kotlin = "2.3.10"
tsBinding = "4.3.2"
-lsp4j = "0.24.0"
+lsp4j = "1.0.0"
androidxAnnotation = "1.9.1"
[libraries]
diff --git a/language-monarch/src/main/java/io/github/rosemoe/sora/langs/monarch/MonarchAnalyzer.kt b/language-monarch/src/main/java/io/github/rosemoe/sora/langs/monarch/MonarchAnalyzer.kt
index d49fce70e..1fd1b58d4 100644
--- a/language-monarch/src/main/java/io/github/rosemoe/sora/langs/monarch/MonarchAnalyzer.kt
+++ b/language-monarch/src/main/java/io/github/rosemoe/sora/langs/monarch/MonarchAnalyzer.kt
@@ -184,7 +184,7 @@ class MonarchAnalyzer(
// It's safe here to use raw data because the Content is only held by this thread
val length = model.getColumnCount(foldingStartLine)
- val chars = model.getLine(foldingStartLine).backingCharArray
+ val chars = model.getLine(foldingStartLine)
codeBlock.startColumn =
IndentRange.computeStartColumn(
@@ -294,7 +294,7 @@ class MonarchAnalyzer(
line, 0
),
IndentRange.computeIndentLevel(
- (lineC as ContentLine).backingCharArray, line.length - 1, language.tabSize
+ lineC, line.length - 1, language.tabSize
),
identifiers
), null, tokens
diff --git a/language-monarch/src/main/java/io/github/rosemoe/sora/langs/monarch/folding/IndentRange.kt b/language-monarch/src/main/java/io/github/rosemoe/sora/langs/monarch/folding/IndentRange.kt
index 09bdb7732..402b3ac55 100644
--- a/language-monarch/src/main/java/io/github/rosemoe/sora/langs/monarch/folding/IndentRange.kt
+++ b/language-monarch/src/main/java/io/github/rosemoe/sora/langs/monarch/folding/IndentRange.kt
@@ -35,7 +35,7 @@ object IndentRange {
// START sora-editor note
// Change String to char[] and int
// END sora-editor note
- fun computeStartColumn(line: CharArray, len: Int, tabSize: Int): Int {
+ fun computeStartColumn(line: CharSequence, len: Int, tabSize: Int): Int {
var column = 0
var i = 0
@@ -64,7 +64,7 @@ object IndentRange {
* - -1 => the line consists of whitespace
* - otherwise => the indent level is returned value
*/
- fun computeIndentLevel(line: CharArray, len: Int, tabSize: Int): Int {
+ fun computeIndentLevel(line: CharSequence, len: Int, tabSize: Int): Int {
var indent = 0
var i = 0
diff --git a/language-textmate/src/main/java/io/github/rosemoe/sora/langs/textmate/TextMateAnalyzer.java b/language-textmate/src/main/java/io/github/rosemoe/sora/langs/textmate/TextMateAnalyzer.java
index ef5832429..76adfc5d6 100644
--- a/language-textmate/src/main/java/io/github/rosemoe/sora/langs/textmate/TextMateAnalyzer.java
+++ b/language-textmate/src/main/java/io/github/rosemoe/sora/langs/textmate/TextMateAnalyzer.java
@@ -190,7 +190,7 @@ public void analyzeCodeBlocks(Content model, ArrayList