From 9beb8563da09eadc3d9a2201d7047969183f6c26 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 04:54:47 +0000 Subject: [PATCH 01/22] chore(deps): update styfle/cancel-workflow-action action to v0.13.0 --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 7e8b8292..d45cb7a4 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.12.1 + uses: styfle/cancel-workflow-action@0.13.0 with: access_token: ${{ github.token }} From 9e6b8d86211ff219c0b4bf2517048866a101917e Mon Sep 17 00:00:00 2001 From: Rosemoe <2073412493@qq.com> Date: Sun, 18 Jan 2026 11:44:31 +0800 Subject: [PATCH 02/22] chore(build): remove kotlin plugin from version catalog --- gradle/libs.versions.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ccc09a6..fd5e4075 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,5 +41,4 @@ tests-robolectric = { module = "org.robolectric:robolectric", version = "4.16" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } -kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } publish = { id = "com.vanniktech.maven.publish.base", version = "0.35.0" } From 787c00058b08c61991ea346b3e50515267df5fc5 Mon Sep 17 00:00:00 2001 From: Rosemoe <2073412493@qq.com> Date: Sun, 18 Jan 2026 12:05:34 +0800 Subject: [PATCH 03/22] build(oniguruma): exclude test code in compilation --- oniguruma-native/src/main/cpp/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oniguruma-native/src/main/cpp/CMakeLists.txt b/oniguruma-native/src/main/cpp/CMakeLists.txt index 0067a6b9..f4e31448 100644 --- a/oniguruma-native/src/main/cpp/CMakeLists.txt +++ b/oniguruma-native/src/main/cpp/CMakeLists.txt @@ -5,7 +5,8 @@ project("oniguruma-binding") set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_FLAGS "-fvisibility=hidden") -add_subdirectory(oniguruma) +set(BUILD_TEST OFF CACHE BOOL "Disable oniguruma tests" FORCE) +add_subdirectory(oniguruma EXCLUDE_FROM_ALL) add_library("oniguruma-binding" SHARED binding.cpp) From f2f53019ea60e2f6f5af309815a8d1d0cc75a2aa Mon Sep 17 00:00:00 2001 From: Rosemoe <2073412493@qq.com> Date: Sun, 18 Jan 2026 12:06:29 +0800 Subject: [PATCH 04/22] fix(build): build-logic has no task named `clean`, preventing IDE `Clean Project` from working properly --- build-logic/build.gradle.kts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 build-logic/build.gradle.kts diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 00000000..40ca8985 --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,27 @@ +/******************************************************************************* + * 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 + ******************************************************************************/ + +tasks.register("clean").configure { + delete(rootProject.layout.buildDirectory) +} \ No newline at end of file From 37857709271e537a2bb8e93c0e03ef40d91b410c Mon Sep 17 00:00:00 2001 From: Rosemoe <2073412493@qq.com> Date: Sun, 18 Jan 2026 14:14:12 +0800 Subject: [PATCH 05/22] feat(editor): add click event for inlay hints --- .../github/rosemoe/sora/app/MainActivity.kt | 4 ++ app/src/main/res/values-zh/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../rosemoe/sora/event/InlayHintClickEvent.kt | 44 +++++++++++++++++ .../github/rosemoe/sora/graphics/TextRow.java | 49 +++++++++++++++++-- .../rosemoe/sora/widget/CodeEditor.java | 17 +++++++ .../sora/widget/EditorTouchEventHandler.java | 14 ++++-- .../rosemoe/sora/widget/layout/Layout.java | 32 ++++++++++-- .../sora/widget/layout/LineBreakLayout.java | 9 ++-- .../sora/widget/layout/WordwrapLayout.java | 19 +++---- 10 files changed, 166 insertions(+), 24 deletions(-) create mode 100644 editor/src/main/java/io/github/rosemoe/sora/event/InlayHintClickEvent.kt diff --git a/app/src/main/java/io/github/rosemoe/sora/app/MainActivity.kt b/app/src/main/java/io/github/rosemoe/sora/app/MainActivity.kt index 0a848579..9d4fcae4 100644 --- a/app/src/main/java/io/github/rosemoe/sora/app/MainActivity.kt +++ b/app/src/main/java/io/github/rosemoe/sora/app/MainActivity.kt @@ -52,6 +52,7 @@ import io.github.rosemoe.sora.app.lsp.LspTestJavaActivity import io.github.rosemoe.sora.app.tests.TestActivity import io.github.rosemoe.sora.event.ContentChangeEvent import io.github.rosemoe.sora.event.EditorKeyEvent +import io.github.rosemoe.sora.event.InlayHintClickEvent import io.github.rosemoe.sora.event.KeyBindingEvent import io.github.rosemoe.sora.event.PublishSearchResultEvent import io.github.rosemoe.sora.event.SelectionChangeEvent @@ -240,6 +241,9 @@ class MainActivity : AppCompatActivity() { subscribeAlways { toast(R.string.tip_side_icon) } + subscribeAlways { + toast(R.string.tip_inlay_hint) + } subscribeAlways { event -> Log.d( TAG, diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index a09ce540..5113048f 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -89,6 +89,7 @@ 物理键盘连接时隐藏软键盘 日志已删除。 点击了侧边按钮 + 点击了嵌入提示 选择LSP活动 是否要打开以Kotlin编写的LSP界面? diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 51e0e412..9ed76c3f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -89,6 +89,7 @@ Hide soft kbd if hard kbd available Log removed. Side icon clicked. + Inlay hint clicked. Select LSP Activity Do you want to open LspActivity written in Kotlin? Yes diff --git a/editor/src/main/java/io/github/rosemoe/sora/event/InlayHintClickEvent.kt b/editor/src/main/java/io/github/rosemoe/sora/event/InlayHintClickEvent.kt new file mode 100644 index 00000000..301f925e --- /dev/null +++ b/editor/src/main/java/io/github/rosemoe/sora/event/InlayHintClickEvent.kt @@ -0,0 +1,44 @@ +/******************************************************************************* + * 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.event + +import io.github.rosemoe.sora.lang.styling.inlayHint.InlayHint +import io.github.rosemoe.sora.text.CharPosition +import io.github.rosemoe.sora.widget.CodeEditor + + +/** + * Called when inlay hint is clicked. + * + * If you would like to avoid [ClickEvent] to be triggered, you are expected to intercept editor by + * calling [io.github.rosemoe.sora.event.InlayHintClickEvent.intercept] + */ +class InlayHintClickEvent( + editor: CodeEditor, + val inlayHint: InlayHint, + val textPosition: CharPosition +) : Event(editor) { + override fun canIntercept(): Boolean = true +} \ No newline at end of file diff --git a/editor/src/main/java/io/github/rosemoe/sora/graphics/TextRow.java b/editor/src/main/java/io/github/rosemoe/sora/graphics/TextRow.java index 17803b7e..839f72b7 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/graphics/TextRow.java +++ b/editor/src/main/java/io/github/rosemoe/sora/graphics/TextRow.java @@ -504,6 +504,8 @@ private static class IteratingContext { /* for horizontal char offset seeking */ public float targetHorizontalOffset = -1f; public int resultCharOffset = -1; + public RowElement resultElement; + public boolean isInElementBounds = false; /* for background region iterating / text patching */ public int startCharOffset; public int endCharOffset; @@ -1053,11 +1055,18 @@ private float handleMultiElementRun(List e, boolean isRtl, ListPoint var visualElements = isRtl ? new ReversedListView<>(e) : e; float localOffset = 0f; for (var element : visualElements) { + float offsetAdvance = 0f; if (element.type == RowElementTypes.TEXT) { - localOffset += handleSingleTextElement(element, pointers, canvas, offset + localOffset, ctx); + offsetAdvance = handleSingleTextElement(element, pointers, canvas, offset + localOffset, ctx); } else if (element.type == RowElementTypes.INLAY_HINT) { - localOffset += handleSingleInlineElement(element, canvas, offset + localOffset, ctx); + offsetAdvance = handleSingleInlineElement(element, canvas, offset + localOffset, ctx); } + if (ctx.targetHorizontalOffset != -1) { + ctx.resultElement = element; + float leftOffset = offset + localOffset; + ctx.isInElementBounds = leftOffset <= ctx.targetHorizontalOffset && ctx.targetHorizontalOffset <= leftOffset + offsetAdvance; + } + localOffset += offsetAdvance; if (offset + localOffset > ctx.maxOffset) { break; } @@ -1120,14 +1129,16 @@ public boolean accept(List e, boolean isRtl, ListPointers pointers) } /** - * Get text index from the given cursor horizontal offset + * Get text index and element (inline elements considered) from the given cursor horizontal offset */ - public int getIndexForCursorOffset(float offset) { + @NonNull + public ElementPosition getElementPositionForCursorOffset(float offset) { var ctx = new IteratingContext(); ctx.targetHorizontalOffset = offset; ctx.maxOffset = offset; iterateRuns(new MaxOffsetIterationConsumer(ctx), true); - return ctx.resultCharOffset == -1 ? textStart : ctx.resultCharOffset; + var charOffset = ctx.resultCharOffset == -1 ? textStart : ctx.resultCharOffset; + return new ElementPosition(ctx.resultElement, ctx.isInElementBounds, charOffset); } /** @@ -1398,4 +1409,32 @@ void drawText(Canvas canvas, char[] text, int index, int count, int contextIndex float horizontalOffset, float width, TextRowParams params, Span span); } + + public static class ElementPosition { + + /** + * The row element where the horizontal offset is in + */ + @Nullable + public RowElement element; + + /** + * Whether the requested offset is in the horizontal bounds of the element. + *

+ * Meaningless if element is null. + */ + public boolean isInElementBounds; + + /** + * Text offset in line text + */ + public int textOffset; + + public ElementPosition(@Nullable RowElement element, boolean isInElementBounds, int textOffset) { + this.element = element; + this.isInElementBounds = isInElementBounds; + this.textOffset = textOffset; + } + + } } 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 e6a0a2ec..c9149117 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 @@ -2387,6 +2387,23 @@ public long getPointPositionOnScreen(float x, float y) { return getPointPosition(x + getOffsetX(), y + getOffsetY()); } + public Layout.VisualLocation getPointVisualPosition(float xOffset, float yOffset) { + return layout.getVisualPositionForLayoutOffset(xOffset - measureTextRegionOffset(), yOffset); + } + + public Layout.VisualLocation getPointVisualPositionOnScreen(float x, float y) { + y = Math.max(0, y); + var stuckLines = renderer.lastStuckLines; + if (stuckLines != null) { + // position Y maybe negative + var index = (int) Math.max(0, (y / getRowHeight())); + if (y < stuckLines.size() * getRowHeight() && index < stuckLines.size()) { + return getPointVisualPosition(x, layout.getCharLayoutOffset(stuckLines.get(index).startLine, 0)[0] - getRowHeight() / 2f); + } + } + return getPointVisualPosition(x + getOffsetX(), y + getOffsetY()); + } + /** * Get max scroll y * diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/EditorTouchEventHandler.java b/editor/src/main/java/io/github/rosemoe/sora/widget/EditorTouchEventHandler.java index e9602e9a..1ba46662 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/widget/EditorTouchEventHandler.java +++ b/editor/src/main/java/io/github/rosemoe/sora/widget/EditorTouchEventHandler.java @@ -45,6 +45,7 @@ import io.github.rosemoe.sora.event.DragSelectStopEvent; import io.github.rosemoe.sora.event.EditorMotionEvent; import io.github.rosemoe.sora.event.HandleStateChangeEvent; +import io.github.rosemoe.sora.event.InlayHintClickEvent; import io.github.rosemoe.sora.event.InterceptTarget; import io.github.rosemoe.sora.event.LongPressEvent; import io.github.rosemoe.sora.event.ScrollEvent; @@ -59,6 +60,7 @@ import io.github.rosemoe.sora.util.IntPair; import io.github.rosemoe.sora.util.Numbers; import io.github.rosemoe.sora.widget.component.Magnifier; +import io.github.rosemoe.sora.widget.layout.RowElementTypes; import io.github.rosemoe.sora.widget.style.SelectionHandleStyle; import kotlin.jvm.functions.Function5; import kotlin.jvm.functions.Function7; @@ -845,9 +847,9 @@ public boolean onSingleTapUp(@NonNull MotionEvent e) { var resolved = RegionResolverKt.resolveTouchRegion(editor, e); var region = IntPair.getFirst(resolved); var regionBound = IntPair.getSecond(resolved); - long res = editor.getPointPositionOnScreen(e.getX(), e.getY()); - int line = IntPair.getFirst(res); - int column = IntPair.getSecond(res); + var visualPosition = editor.getPointVisualPositionOnScreen(e.getX(), e.getY()); + int line = visualPosition.line; + int column = visualPosition.column; editor.performClick(); if (region == RegionResolverKt.REGION_SIDE_ICON) { int row = (int) (e.getY() + editor.getOffsetX()) / editor.getRowHeight(); @@ -863,6 +865,12 @@ public boolean onSingleTapUp(@NonNull MotionEvent e) { } } var position = editor.getText().getIndexer().getCharPosition(line, column); + if (visualPosition.element != null && visualPosition.element.type == RowElementTypes.INLAY_HINT + && visualPosition.element.inlayHint != null && visualPosition.isInElementBounds) { + if ((editor.dispatchEvent(new InlayHintClickEvent(editor, visualPosition.element.inlayHint, position)) & InterceptTarget.TARGET_EDITOR) != 0) { + return true; + } + } if ((dispatchEditorMotionEvent(ClickEvent::new, position, e, region, regionBound) & InterceptTarget.TARGET_EDITOR) != 0) { return true; } diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/layout/Layout.java b/editor/src/main/java/io/github/rosemoe/sora/widget/layout/Layout.java index 3f58a749..e78ecd12 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/widget/layout/Layout.java +++ b/editor/src/main/java/io/github/rosemoe/sora/widget/layout/Layout.java @@ -32,6 +32,7 @@ import io.github.rosemoe.sora.lang.analysis.StyleUpdateRange; import io.github.rosemoe.sora.text.ContentLine; import io.github.rosemoe.sora.text.ContentListener; +import io.github.rosemoe.sora.util.IntPair; /** * Layout is a manager class for editor to display text @@ -107,12 +108,18 @@ default RowIterator obtainRowIterator(int initialRow) { /** * Get character line and column for offsets in layout * - * @param xOffset Horizontal offset on layout - * @param yOffset Vertical offset on layout + * @param offsetX Horizontal offset on layout + * @param offsetY Vertical offset on layout * @return Packed IntPair, first is line and second is column * @see io.github.rosemoe.sora.util.IntPair */ - long getCharPositionForLayoutOffset(float xOffset, float yOffset); + default long getCharPositionForLayoutOffset(float offsetX, float offsetY) { + var pos = getVisualPositionForLayoutOffset(offsetX, offsetY); + return IntPair.pack(pos.line, pos.column); + } + + @NonNull + VisualLocation getVisualPositionForLayoutOffset(float offsetX, float offsetY); /** * Get layout offset of a position in text @@ -167,4 +174,23 @@ default float[] getCharLayoutOffset(int line, int column) { */ void invalidateLines(StyleUpdateRange range); + class VisualLocation { + + public final int line; + + public final int column; + + @Nullable + public final RowElement element; + + public boolean isInElementBounds; + + public VisualLocation(int line, int column, @Nullable RowElement element, boolean isInElementBounds) { + this.line = line; + this.column = column; + this.element = element; + this.isInElementBounds = isInElementBounds; + } + } + } diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/layout/LineBreakLayout.java b/editor/src/main/java/io/github/rosemoe/sora/widget/layout/LineBreakLayout.java index 2dbb1a1d..91b65a10 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/widget/layout/LineBreakLayout.java +++ b/editor/src/main/java/io/github/rosemoe/sora/widget/layout/LineBreakLayout.java @@ -255,13 +255,14 @@ public int getLayoutHeight() { return text.getLineCount() * editor.getRowHeight(); } + @NonNull @Override - public long getCharPositionForLayoutOffset(float xOffset, float yOffset) { + public VisualLocation getVisualPositionForLayoutOffset(float offsetX, float offsetY) { int lineCount = text.getLineCount(); - int line = Math.min(lineCount - 1, Math.max((int) (yOffset / editor.getRowHeight()), 0)); + int line = Math.min(lineCount - 1, Math.max((int) (offsetY / editor.getRowHeight()), 0)); var tr = editor.getRenderer().createTextRow(line); - int res = tr.getIndexForCursorOffset(xOffset); - return IntPair.pack(line, res); + var pos = tr.getElementPositionForCursorOffset(offsetX); + return new VisualLocation(line, pos.textOffset, pos.element, pos.isInElementBounds); } @NonNull diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/layout/WordwrapLayout.java b/editor/src/main/java/io/github/rosemoe/sora/widget/layout/WordwrapLayout.java index 251ae523..5a027097 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/widget/layout/WordwrapLayout.java +++ b/editor/src/main/java/io/github/rosemoe/sora/widget/layout/WordwrapLayout.java @@ -396,25 +396,26 @@ public void invalidateLines(StyleUpdateRange range) { } } + @NonNull @Override - public long getCharPositionForLayoutOffset(float xOffset, float yOffset) { + public VisualLocation getVisualPositionForLayoutOffset(float offsetX, float offsetY) { if (rowTable.isEmpty()) { int lineCount = text.getLineCount(); - int line = Math.min(lineCount - 1, Math.max((int) (yOffset / editor.getRowHeight()), 0)); + int line = Math.min(lineCount - 1, Math.max((int) (offsetY / editor.getRowHeight()), 0)); var tr = editor.getRenderer().createTextRow(line); - int res = tr.getIndexForCursorOffset(xOffset); - return IntPair.pack(line, res); + var pos = tr.getElementPositionForCursorOffset(offsetX); + return new VisualLocation(line, pos.textOffset, pos.element, pos.isInElementBounds); } - int row = (int) (yOffset / editor.getRowHeight()); + int row = (int) (offsetY / editor.getRowHeight()); row = Math.max(0, Math.min(row, rowTable.size() - 1)); RowRegion region = rowTable.get(row); if (region.startColumn != 0) { - xOffset -= miniGraphWidth; + offsetX -= miniGraphWidth; } - xOffset -= region.getRenderTranslateX(width); + offsetX -= region.getRenderTranslateX(width); var tr = editor.getRenderer().createTextRow(row); - int column = tr.getIndexForCursorOffset(xOffset); - return IntPair.pack(region.line, column); + var pos = tr.getElementPositionForCursorOffset(offsetX); + return new VisualLocation(region.line, pos.textOffset, pos.element, pos.isInElementBounds); } @NonNull From 1a7cf4be2184377ed0d0d5d31480992ad776539a Mon Sep 17 00:00:00 2001 From: Rosemoe <2073412493@qq.com> Date: Sun, 18 Jan 2026 22:36:08 +0800 Subject: [PATCH 06/22] chore(deps): downgrade kotlin to v2.2.x --- build.gradle.kts | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 135c4012..7d40186e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -67,7 +67,7 @@ fun Project.configureAndroidAndKotlin() { extensions.findByType()?.apply { compilerOptions { - languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_3 + languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2 jvmTarget = JvmTarget.JVM_17 } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fd5e4075..f240b419 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] agp = "9.0.0" -kotlin = "2.3.0" +kotlin = "2.2.21" tsBinding = "4.3.2" lsp4j = "0.24.0" androidxAnnotation = "1.9.1" From 494b9308b04824b671e3e13ff5ce6784637c83bd Mon Sep 17 00:00:00 2001 From: Rosemoe <2073412493@qq.com> Date: Mon, 19 Jan 2026 15:28:36 +0800 Subject: [PATCH 07/22] feat(editor): implement simple preserve-case replacing (close #792) --- .../github/rosemoe/sora/app/MainActivity.kt | 3 + .../sora/text/PreserveCaseReplace.java | 79 ++++++++++++++++ .../rosemoe/sora/widget/EditorSearcher.java | 90 ++++++++++++++++--- 3 files changed, 161 insertions(+), 11 deletions(-) create mode 100644 editor/src/main/java/io/github/rosemoe/sora/text/PreserveCaseReplace.java diff --git a/app/src/main/java/io/github/rosemoe/sora/app/MainActivity.kt b/app/src/main/java/io/github/rosemoe/sora/app/MainActivity.kt index 9d4fcae4..28b422cd 100644 --- a/app/src/main/java/io/github/rosemoe/sora/app/MainActivity.kt +++ b/app/src/main/java/io/github/rosemoe/sora/app/MainActivity.kt @@ -92,6 +92,7 @@ import io.github.rosemoe.sora.utils.codePointStringAt import io.github.rosemoe.sora.utils.escapeCodePointIfNecessary import io.github.rosemoe.sora.utils.toast import io.github.rosemoe.sora.widget.CodeEditor +import io.github.rosemoe.sora.widget.EditorSearcher import io.github.rosemoe.sora.widget.EditorSearcher.SearchOptions import io.github.rosemoe.sora.widget.SelectionMovement import io.github.rosemoe.sora.widget.component.EditorAutoCompletion @@ -260,6 +261,7 @@ class MainActivity : AppCompatActivity() { } } + searcher.replaceOptions = EditorSearcher.ReplaceOptions(true) // Handle span interactions EditorSpanInteractionHandler(this) getComponent() @@ -289,6 +291,7 @@ class MainActivity : AppCompatActivity() { updateBtnState() switchThemeIfRequired(this, binding.editor) + computeSearchOptions() } /** diff --git a/editor/src/main/java/io/github/rosemoe/sora/text/PreserveCaseReplace.java b/editor/src/main/java/io/github/rosemoe/sora/text/PreserveCaseReplace.java new file mode 100644 index 00000000..e0a8ca08 --- /dev/null +++ b/editor/src/main/java/io/github/rosemoe/sora/text/PreserveCaseReplace.java @@ -0,0 +1,79 @@ +/* + * 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 androidx.annotation.NonNull; + + +public class PreserveCaseReplace { + + private PreserveCaseReplace() { + + } + + public static String getReplacementSimple(@NonNull String oldStr, @NonNull String newStr) { + if (oldStr.isEmpty() || newStr.isEmpty()) return newStr; + // Analyze the case of old string + var isUpperCase = true; + var isLowerCase = true; + var isCapitalized = true; + for (int i = 0; i < oldStr.length(); i++) { + var ch = oldStr.charAt(i); + if (!Character.isLetter(ch)) continue; + var upper = Character.isUpperCase(ch); + var lower = Character.isLowerCase(ch); + if (isUpperCase && isLowerCase) { + isCapitalized = upper; + } else { + isCapitalized &= lower; + } + isUpperCase &= upper; + isLowerCase &= lower; + } + // No letter found + if (isUpperCase && isLowerCase) return newStr; + // Upper / Lower / Capitalized + if (isUpperCase) return newStr.toUpperCase(); + if (isLowerCase) return newStr.toLowerCase(); + if (isCapitalized) + return newStr.substring(0, 1).toUpperCase() + newStr.substring(1).toLowerCase(); + // Mixed case + var sb = new StringBuilder(); + for (int i = 0; i < newStr.length(); i++) { + var ch = newStr.charAt(i); + if (!Character.isLetter(ch) || i >= oldStr.length()) { + sb.append(ch); + continue; + } + var oldCh = oldStr.charAt(i); + if (Character.isLetter(oldCh)) { + sb.append(Character.isUpperCase(oldCh) ? Character.toUpperCase(ch) : Character.toLowerCase(ch)); + } else { + sb.append(ch); + } + } + return sb.toString(); + } + +} diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/EditorSearcher.java b/editor/src/main/java/io/github/rosemoe/sora/widget/EditorSearcher.java index 632844f9..b26c1f21 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/widget/EditorSearcher.java +++ b/editor/src/main/java/io/github/rosemoe/sora/widget/EditorSearcher.java @@ -31,6 +31,7 @@ import androidx.annotation.Nullable; import java.util.List; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -40,6 +41,7 @@ import io.github.rosemoe.sora.event.PublishSearchResultEvent; import io.github.rosemoe.sora.event.SelectionChangeEvent; import io.github.rosemoe.sora.text.Content; +import io.github.rosemoe.sora.text.PreserveCaseReplace; import io.github.rosemoe.sora.text.TextUtils; import io.github.rosemoe.sora.util.IntPair; import io.github.rosemoe.sora.util.LongArrayList; @@ -50,6 +52,7 @@ /** * Search text in editor. + *

* Note that editor searches text in another thread, so results may not be available immediately. Also, * the searcher does not match empty text. For example, you will never match a single empty * line by regex '^.*$'. What's more, zero-length pattern is not permitted. @@ -57,15 +60,16 @@ * is invoked. So be careful that the search result is changing and {@link PublishSearchResultEvent} is * re-triggered when search result is available for changed text. * + * @author Rosemoe * @see PublishSearchResultEvent * @see SearchOptions - * @author Rosemoe */ public class EditorSearcher { private final CodeEditor editor; protected String currentPattern; protected SearchOptions searchOptions; + protected ReplaceOptions replaceOptions; protected Thread currentThread; /** * Search results. Note that it is naturally sorted by start index (and also end index). @@ -81,10 +85,12 @@ public class EditorSearcher { executeMatch(); } })); + replaceOptions = ReplaceOptions.DEFAULT; } /** * Jump cyclically when calling {@link #gotoNext()} and {@link #gotoPrevious()} + * * @see #isCyclicJumping() */ public void setCyclicJumping(boolean cyclicJumping) { @@ -98,6 +104,22 @@ public boolean isCyclicJumping() { return cyclicJumping; } + /** + * Set the options when replacing text + */ + public void setReplaceOptions(@NonNull ReplaceOptions replaceOptions) { + this.replaceOptions = Objects.requireNonNull(replaceOptions); + } + + /** + * Get the options when replacing text + * + * @see #setReplaceOptions(ReplaceOptions) + */ + public ReplaceOptions getReplaceOptions() { + return replaceOptions; + } + /** * Search text with the given pattern and options. If you use {@link SearchOptions#TYPE_REGULAR_EXPRESSION}, * the pattern will be your regular expression. @@ -108,7 +130,8 @@ public boolean isCyclicJumping() { * avoid lags in main thread. If you want to be notified when the results is available, refer to * {@link PublishSearchResultEvent}. Also be careful that, the event is also triggered when {@link #stopSearch()} * is called. - * @throws IllegalArgumentException if pattern length is zero + * + * @throws IllegalArgumentException if pattern length is zero * @throws java.util.regex.PatternSyntaxException if pattern is invalid when regex is enabled. */ public void search(@NonNull String pattern, @NonNull SearchOptions options) { @@ -168,6 +191,7 @@ private void checkState() { /** * Find current selected region in search results and return the index in search result. * Or {@code -1} if result is not available or the current selected region is not in result. + * * @throws IllegalStateException if no search is in progress */ public int getCurrentMatchedPositionIndex() { @@ -195,6 +219,7 @@ public int getCurrentMatchedPositionIndex() { /** * Get item count of search result. Or {@code 0} if result is not available or no item is found. + * * @throws IllegalStateException if no search is in progress */ public int getMatchedPositionCount() { @@ -208,9 +233,10 @@ public int getMatchedPositionCount() { /** * Goto next matched position based on cursor position. - * @see #setCyclicJumping(boolean) + * * @return if any jumping action is performed * @throws IllegalStateException if no search is in progress + * @see #setCyclicJumping(boolean) */ public boolean gotoNext() { checkState(); @@ -238,9 +264,10 @@ public boolean gotoNext() { /** * Goto last matched position based on cursor position. - * @see #setCyclicJumping(boolean) + * * @return if any jumping action is performed * @throws IllegalStateException if no search is in progress + * @see #setCyclicJumping(boolean) */ public boolean gotoPrevious() { checkState(); @@ -252,7 +279,7 @@ public boolean gotoPrevious() { var left = editor.getCursor().getLeft(); var index = res.lowerBoundByFirst(left); if (index == res.size() || IntPair.getFirst(res.get(index)) >= index) { - index --; + index--; } if (index < 0 && cyclicJumping) { index = res.size() - 1; @@ -271,6 +298,7 @@ public boolean gotoPrevious() { /** * Check if selected region is exactly a search result + * * @throws IllegalStateException if no search is in progress */ public boolean isMatchedPositionSelected() { @@ -308,7 +336,7 @@ public void replaceCurrentMatch(@NonNull String replacement) { if (searchOptions.type == SearchOptions.TYPE_REGULAR_EXPRESSION && searchOptions.regexBackrefGrammar != null) { var cursor = editor.getCursor(); - String currentText = editor.getText().substring(cursor.getLeft(), cursor.getRight()); + var currentText = editor.getText().substring(cursor.getLeft(), cursor.getRight()); var pattern = Pattern.compile(currentPattern, (searchOptions.caseInsensitive ? Pattern.CASE_INSENSITIVE : 0) | Pattern.MULTILINE); var matcher = pattern.matcher(currentText); if (!matcher.find()) { @@ -316,6 +344,11 @@ public void replaceCurrentMatch(@NonNull String replacement) { } replacement = RegexBackrefHelper.computeReplacement(matcher, searchOptions.regexBackrefGrammar, replacement); } + if (replaceOptions.preserveCase) { + var cursor = editor.getCursor(); + var currentText = editor.getText().substring(cursor.getLeft(), cursor.getRight()); + replacement = PreserveCaseReplace.getReplacementSimple(currentText, replacement); + } editor.commitText(replacement, false, false); } } else { @@ -326,6 +359,7 @@ public void replaceCurrentMatch(@NonNull String replacement) { /** * Replace all matched position. Note that after invoking this, a blocking {@link ProgressDialog} * is shown until the action is done (either succeeded or failed). + * * @param replacement The text for replacement * @throws IllegalStateException if no search is in progress */ @@ -338,7 +372,7 @@ public void replaceAll(@NonNull String replacement) { * is shown until the action is done (either succeeded or failed). The given callback will be executed * on success. * - * @param replacement The text for replacement + * @param replacement The text for replacement * @param whenSucceeded Callback when action is succeeded * @throws IllegalStateException if no search is in progress */ @@ -384,6 +418,12 @@ public void replaceAll(@NonNull String replacement, @Nullable final Runnable whe var computedReplacement = RegexBackrefHelper.computeReplacement(matcher, tokens); var newLength = computedReplacement.length(); var oldLength = end - start; + if (replaceOptions.preserveCase) { + computedReplacement = PreserveCaseReplace.getReplacementSimple( + sb.substring(start + delta, end + delta), + computedReplacement + ); + } sb.replace(start + delta, end + delta, computedReplacement); delta += newLength - oldLength; } @@ -395,7 +435,11 @@ public void replaceAll(@NonNull String replacement, @Nullable final Runnable whe var start = IntPair.getFirst(region); var end = IntPair.getSecond(region); var oldLength = end - start; - sb.replace(start + delta, end + delta, replacement); + var replaceText = replacement; + if (replaceOptions.preserveCase) { + replaceText = PreserveCaseReplace.getReplacementSimple(sb.substring(start + delta, end + delta), replacement); + } + sb.replace(start + delta, end + delta, replaceText); delta += newLength - oldLength; } } @@ -451,8 +495,9 @@ public SearchOptions(boolean caseInsensitive, boolean useRegex) { /** * Create a new searching option with the given attributes. - * @param type type of searching method - * @param caseInsensitive Case insensitive + * + * @param type type of searching method + * @param caseInsensitive Case-insensitive * @see #TYPE_NORMAL * @see #TYPE_WHOLE_WORD * @see #TYPE_REGULAR_EXPRESSION @@ -465,7 +510,7 @@ public SearchOptions(@IntRange(from = 1, to = 3) int type, boolean caseInsensiti * Create a new searching option with the given attributes. * * @param type type of searching method - * @param caseInsensitive Case insensitive + * @param caseInsensitive Case-insensitive * @param regexBackrefGrammar Back reference grammar in regular expression replace mode * @see #TYPE_NORMAL * @see #TYPE_WHOLE_WORD @@ -482,6 +527,29 @@ public SearchOptions(@IntRange(from = 1, to = 3) int type, boolean caseInsensiti } + /** + * Replace options for {@link EditorSearcher#replaceCurrentMatch} and {@link EditorSearcher#replaceAll} + */ + public static class ReplaceOptions { + + public final static ReplaceOptions DEFAULT = new ReplaceOptions(false); + + /** + * Preserve the case of text being replaced + */ + public final boolean preserveCase; + + /** + * Create a new replace option with given attributes + * + * @param preserveCase Whether to preserve the case of text being replaced + */ + public ReplaceOptions(boolean preserveCase) { + this.preserveCase = preserveCase; + } + + } + /** * Run for regex matching */ From fb6a4c8baf52feceb77ed0986a69e45c07c1ff0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=80=E5=89=AA=E6=B2=90=E6=A9=99?= <3578557729@qq.com> Date: Thu, 22 Jan 2026 13:51:00 +0800 Subject: [PATCH 08/22] fix(editor): UI update from LSP on non-main thread (#793) --- .../rosemoe/sora/lsp/editor/LspEditorUIDelegate.kt | 10 +++++----- .../rosemoe/sora/lsp/events/color/DocumentColor.kt | 2 +- .../sora/lsp/events/inlayhint/InlayHintEvent.kt | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorUIDelegate.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorUIDelegate.kt index ce46cb4b..0de300e8 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorUIDelegate.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorUIDelegate.kt @@ -197,7 +197,7 @@ internal class LspEditorUIDelegate(private val editor: LspEditor) { val editorInstance = currentEditorRef.get() ?: return if (highlights.isNullOrEmpty()) { - editorInstance.highlightTexts = null + editorInstance.post { editorInstance.highlightTexts = null } return } @@ -228,7 +228,7 @@ internal class LspEditorUIDelegate(private val editor: LspEditor) { ) } - editorInstance.highlightTexts = container + editorInstance.post { editorInstance.highlightTexts = container } } fun showInlayHints(inlayHints: List?) { @@ -256,7 +256,7 @@ internal class LspEditorUIDelegate(private val editor: LspEditor) { val hasDocumentColors = !cachedDocumentColors.isNullOrEmpty() if (!hasInlayHints && !hasDocumentColors) { - editorInstance.inlayHints = null + editorInstance.post { editorInstance.inlayHints = null } return } @@ -264,7 +264,7 @@ internal class LspEditorUIDelegate(private val editor: LspEditor) { cachedInlayHints?.inlayHintToDisplay()?.forEach(container::add) cachedDocumentColors?.colorInfoToDisplay()?.forEach(container::add) - editorInstance.inlayHints = container + editorInstance.post { editorInstance.inlayHints = container } } private fun resetInlinePresentations() { @@ -272,7 +272,7 @@ internal class LspEditorUIDelegate(private val editor: LspEditor) { cachedDocumentColors = null currentEditorRef.get()?.let { if (it.inlayHints != null) { - it.inlayHints = null + it.post { it.inlayHints = null } } } } diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/color/DocumentColor.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/color/DocumentColor.kt index 8cb745d0..bfbb4b48 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/color/DocumentColor.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/color/DocumentColor.kt @@ -140,4 +140,4 @@ class DocumentColorEvent : AsyncEventListener() { @get:Experimental val EventType.documentColor: String - get() = "textDocument/documentColor" \ No newline at end of file + get() = "textDocument/documentColor" diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/inlayhint/InlayHintEvent.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/inlayhint/InlayHintEvent.kt index 9e251a28..55f0a43e 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/inlayhint/InlayHintEvent.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/inlayhint/InlayHintEvent.kt @@ -164,4 +164,4 @@ class InlayHintEvent : AsyncEventListener() { @get:Experimental val EventType.inlayHint: String - get() = "textDocument/inlayHint" \ No newline at end of file + get() = "textDocument/inlayHint" From cc5448dc49e4d75d2db409a099186c5add78d250 Mon Sep 17 00:00:00 2001 From: Rosemoe <2073412493@qq.com> Date: Thu, 22 Jan 2026 22:09:32 +0800 Subject: [PATCH 09/22] fix(editor): wordwrap forces to break after a trailing period --- .../rosemoe/sora/text/breaker/WordBreakerProgram.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 f1d1b433..704e5131 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 @@ -29,8 +29,11 @@ public class WordBreakerProgram extends WordBreakerIcu { + protected final int length; + public WordBreakerProgram(@NonNull ContentLine text) { super(text); + this.length = text.length(); } @Override @@ -39,6 +42,10 @@ public int getOptimizedBreakPoint(int start, int end) { if (icuResult != end || end <= start || /* end > start */ Character.isWhitespace(chars[end - 1])) { return icuResult; } + // The content can be placed on a single row + if (end == length) { + return end; + } // Add extra opportunities for dots int index = end - 1; while (index > start) { From 82fdbbb66a0db76a45ebf7d184db6caf1fcfed2d Mon Sep 17 00:00:00 2001 From: Rosemoe <2073412493@qq.com> Date: Fri, 23 Jan 2026 11:40:35 +0800 Subject: [PATCH 10/22] feat(editor): add border color customization for built-in color inlay hint renderer --- .../sora/graphics/inlayHint/ColorInlayHintRenderer.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/editor/src/main/java/io/github/rosemoe/sora/graphics/inlayHint/ColorInlayHintRenderer.kt b/editor/src/main/java/io/github/rosemoe/sora/graphics/inlayHint/ColorInlayHintRenderer.kt index 46ae1576..58e78fc5 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/graphics/inlayHint/ColorInlayHintRenderer.kt +++ b/editor/src/main/java/io/github/rosemoe/sora/graphics/inlayHint/ColorInlayHintRenderer.kt @@ -28,11 +28,15 @@ import android.graphics.Canvas import android.graphics.Color import io.github.rosemoe.sora.graphics.InlayHintRenderParams import io.github.rosemoe.sora.graphics.Paint +import io.github.rosemoe.sora.lang.styling.color.ConstColor +import io.github.rosemoe.sora.lang.styling.color.ResolvableColor import io.github.rosemoe.sora.lang.styling.inlayHint.ColorInlayHint import io.github.rosemoe.sora.lang.styling.inlayHint.InlayHint import io.github.rosemoe.sora.widget.schemes.EditorColorScheme -open class ColorInlayHintRenderer() : InlayHintRenderer() { +open class ColorInlayHintRenderer( + val borderColor: ResolvableColor = ConstColor(Color.WHITE) +) : InlayHintRenderer() { companion object { val DefaultInstance = ColorInlayHintRenderer() @@ -76,7 +80,7 @@ open class ColorInlayHintRenderer() : InlayHintRenderer() { centerY + halfSize, localPaint ) - localPaint.color = Color.WHITE + localPaint.color = borderColor.resolve(colorScheme) localPaint.style = android.graphics.Paint.Style.STROKE canvas.drawRect( centerX - halfSize, From c1401194812e75f09ee46de2271976ed4fddd7de Mon Sep 17 00:00:00 2001 From: KonerDev <105141148+KonerDev@users.noreply.github.com> Date: Fri, 23 Jan 2026 20:04:59 +0100 Subject: [PATCH 11/22] feat(editor): move auto completion description to the right, add support for detail property, add support for deprecated auto completion items --- .../sora/lang/completion/CompletionItem.java | 11 +++ .../DefaultCompletionItemAdapter.java | 20 ++++- .../widget/schemes/EditorColorScheme.java | 8 +- .../layout/default_completion_result_item.xml | 85 +++++++++++-------- 4 files changed, 82 insertions(+), 42 deletions(-) diff --git a/editor/src/main/java/io/github/rosemoe/sora/lang/completion/CompletionItem.java b/editor/src/main/java/io/github/rosemoe/sora/lang/completion/CompletionItem.java index ba1315d9..8f645e8e 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/lang/completion/CompletionItem.java +++ b/editor/src/main/java/io/github/rosemoe/sora/lang/completion/CompletionItem.java @@ -58,11 +58,22 @@ public abstract class CompletionItem { */ public CharSequence label; + /** + * Text to display less prominently directly after label, without any spacing. + */ + public CharSequence detail; + /** * Text to display as description in adapter */ public CharSequence desc; + /** + * Indicates whether this completion item is deprecated. + * When true, the item is typically rendered using strike-through text. + */ + public boolean deprecated; + /** * The kind of this completion item. Based on the kind * an icon is chosen by the editor. diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/component/DefaultCompletionItemAdapter.java b/editor/src/main/java/io/github/rosemoe/sora/widget/component/DefaultCompletionItemAdapter.java index 43c52577..a3608005 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/widget/component/DefaultCompletionItemAdapter.java +++ b/editor/src/main/java/io/github/rosemoe/sora/widget/component/DefaultCompletionItemAdapter.java @@ -31,6 +31,7 @@ import android.widget.TextView; import io.github.rosemoe.sora.R; +import io.github.rosemoe.sora.graphics.Paint; import io.github.rosemoe.sora.widget.schemes.EditorColorScheme; /** @@ -39,7 +40,6 @@ * @author Rose */ public final class DefaultCompletionItemAdapter extends EditorCompletionAdapter { - @Override public int getItemHeight() { // 45 dp @@ -56,10 +56,27 @@ public View getView(int pos, View view, ViewGroup parent, boolean isCurrentCurso TextView tv = view.findViewById(R.id.result_item_label); tv.setText(item.label); tv.setTextColor(getThemeColor(EditorColorScheme.COMPLETION_WND_TEXT_PRIMARY)); + tv.setSelected(isCurrentCursorPosition); + if (item.deprecated) { + tv.setPaintFlags(tv.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } else { + tv.setPaintFlags(tv.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG)); + } + + tv = view.findViewById(R.id.result_item_detail); + tv.setText(item.detail); + tv.setTextColor(getThemeColor(EditorColorScheme.COMPLETION_WND_TEXT_SECONDARY)); + tv.setSelected(isCurrentCursorPosition); + if (item.deprecated) { + tv.setPaintFlags(tv.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } else { + tv.setPaintFlags(tv.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG)); + } tv = view.findViewById(R.id.result_item_desc); tv.setText(item.desc); tv.setTextColor(getThemeColor(EditorColorScheme.COMPLETION_WND_TEXT_SECONDARY)); + tv.setSelected(isCurrentCursorPosition); view.setTag(pos); if (isCurrentCursorPosition) { @@ -71,5 +88,4 @@ public View getView(int pos, View view, ViewGroup parent, boolean isCurrentCurso iv.setImageDrawable(item.icon); return view; } - } diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/schemes/EditorColorScheme.java b/editor/src/main/java/io/github/rosemoe/sora/widget/schemes/EditorColorScheme.java index 35d729c7..a99f1c6d 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/widget/schemes/EditorColorScheme.java +++ b/editor/src/main/java/io/github/rosemoe/sora/widget/schemes/EditorColorScheme.java @@ -40,8 +40,8 @@ /** * This class manages the colors of editor. * You can use color IDs that are not in pre-defined id pool for custom languages. We recommend - * adding a base offset for your custom color IDs. For example, first custom color ID is 256. This - * leaves enough space for editor's future built-in colors. + * adding a base offset for your custom color IDs. For example, first custom color ID is 256. This + * leaves enough space for editor's future built-in colors. *

* This is also the default color scheme of editor. * Be careful to change this class, because this can cause its @@ -393,10 +393,12 @@ private void applyDefault(int type) { color = 0xff3f51b5; break; case COMPLETION_WND_TEXT_PRIMARY: - case COMPLETION_WND_TEXT_SECONDARY: case TEXT_INLAY_HINT_FOREGROUND: color = isDark() ? 0xffffffff : 0xff000000; break; + case COMPLETION_WND_TEXT_SECONDARY: + color = isDark() ? 0xffaaaaaa : 0xff333333; + break; case COMPLETION_WND_ITEM_CURRENT: color = 0xffeeeeee; break; diff --git a/editor/src/main/res/layout/default_completion_result_item.xml b/editor/src/main/res/layout/default_completion_result_item.xml index 60308725..9e06259a 100644 --- a/editor/src/main/res/layout/default_completion_result_item.xml +++ b/editor/src/main/res/layout/default_completion_result_item.xml @@ -23,48 +23,59 @@ ~ additional information or have any questions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> - + - + - + - + - + - + + + From 460789d7762e94d8cba112c28b167cd3f9d64328 Mon Sep 17 00:00:00 2001 From: KonerDev <105141148+KonerDev@users.noreply.github.com> Date: Fri, 23 Jan 2026 20:08:34 +0100 Subject: [PATCH 12/22] feat(editor-lsp): add support for detail text and deprecation --- .../lsp/editor/completion/LspCompletionItem.kt | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/completion/LspCompletionItem.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/completion/LspCompletionItem.kt index fe2cedd0..f1c296ca 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/completion/LspCompletionItem.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/completion/LspCompletionItem.kt @@ -38,6 +38,7 @@ import io.github.rosemoe.sora.text.Content import io.github.rosemoe.sora.util.Logger import io.github.rosemoe.sora.widget.CodeEditor import org.eclipse.lsp4j.CompletionItem +import org.eclipse.lsp4j.CompletionItemTag import org.eclipse.lsp4j.InsertTextFormat import org.eclipse.lsp4j.TextEdit @@ -60,8 +61,15 @@ class LspCompletionItem( sortText = completionItem.sortText filterText = completionItem.filterText val labelDetails = completionItem.labelDetails - if (labelDetails != null && labelDetails.description?.isNotEmpty() == true) { - desc = labelDetails.description + if (labelDetails != null) { + if (labelDetails.description?.isNotEmpty() == true) { + desc = labelDetails.description + } + detail = labelDetails.detail + } + val tags = completionItem.tags + if (tags != null) { + deprecated = tags.contains(CompletionItemTag.Deprecated) } icon = draw(kind ?: CompletionItemKind.Text) } @@ -84,7 +92,10 @@ class LspCompletionItem( if (completionItem.textEdit != null && completionItem.textEdit.isLeft) { textEdit = completionItem.textEdit.left } else if (completionItem.textEdit?.isRight == true) { - textEdit = TextEdit(completionItem.textEdit.right.insert, completionItem.textEdit.right.newText) + textEdit = TextEdit( + completionItem.textEdit.right.insert, + completionItem.textEdit.right.newText + ) } if (textEdit.newText == null && completionItem.label != null) { From 4aa37ef97adf1f0322a10de8c2457f0c59c87ac3 Mon Sep 17 00:00:00 2001 From: KonerDev <105141148+KonerDev@users.noreply.github.com> Date: Fri, 23 Jan 2026 20:09:01 +0100 Subject: [PATCH 13/22] chore: replace `.gitmodules` SSH with HTTPS URL --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index bf6718fe..fcc0ac94 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "oniguruma-native/src/main/cpp/oniguruma"] path = oniguruma-native/src/main/cpp/oniguruma - url = git@github.com:project-sora/oniguruma.git + url = https://github.com/project-sora/oniguruma.git From ab711e604c019a8238b6055b6b9f812ee95123db Mon Sep 17 00:00:00 2001 From: KonerDev <105141148+KonerDev@users.noreply.github.com> Date: Fri, 23 Jan 2026 20:38:48 +0100 Subject: [PATCH 14/22] fix(editor-lsp): fix copy button overlapping with container message --- .../res/layout/lsp_diagnostic_tooltip_window.xml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/editor-lsp/src/main/res/layout/lsp_diagnostic_tooltip_window.xml b/editor-lsp/src/main/res/layout/lsp_diagnostic_tooltip_window.xml index 15f44701..3fd011f5 100644 --- a/editor-lsp/src/main/res/layout/lsp_diagnostic_tooltip_window.xml +++ b/editor-lsp/src/main/res/layout/lsp_diagnostic_tooltip_window.xml @@ -1,4 +1,5 @@ -