-
-
Notifications
You must be signed in to change notification settings - Fork 86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Spell Checking #428
Open
Wavesonics
wants to merge
86
commits into
MohamedRejeb:main
Choose a base branch
from
Wavesonics:spell-check-test
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Spell Checking #428
Changes from 76 commits
Commits
Show all changes
86 commits
Select commit
Hold shift + click to select a range
74727aa
Added textChanges SharedFlow to follow text changes
Wavesonics 2254b20
API
Wavesonics ad23f9d
Clean up
Wavesonics b47e25f
Handle link clicks in BasicRichTextEditor
Wavesonics 3392198
Re move arg
Wavesonics e4fc462
Add callback for handling click on RichSpans
Wavesonics 32a7430
Add some code for testing span clicks
Wavesonics b57f070
Spellcheck test
Wavesonics f6b926f
Make explicitly public
Wavesonics 707110d
It compiles again
Wavesonics 61a75f5
API
Wavesonics d04676f
Add click position to RichSpanClickListener
Wavesonics 2bda581
API
Wavesonics a348e8b
Mocking up spellcheck
Wavesonics add5521
Fix null
Wavesonics 9c3a529
Provide access to the rich spans
Wavesonics 8c5ad1b
Added spellcheck screen
Wavesonics c57dd80
Added spellcheck
Wavesonics 9dc71df
Don't need this
Wavesonics 9ce658c
API
Wavesonics 52e1b42
utils
Wavesonics 2b03d3a
SymSpell version bump (contains other targets)
Wavesonics 1e85ffc
Add getAllRichSpans()
Wavesonics fcab6f9
Added textChanges SharedFlow to follow text changes
Wavesonics 8661764
More realistic Spell Checker
Wavesonics 0cb0f14
API
Wavesonics 0b010a1
dont convert to string every time
Wavesonics 9ed977f
Solve the no-suggestions case
Wavesonics 3ae282c
Spellcheck now appears to be bug free
Wavesonics 5e6adf8
Refactored so we dont need to expose things
Wavesonics 3efc2ce
Reduce public API surface
Wavesonics cbde9c5
Fix adding of original term
Wavesonics b0e8087
API Dump
Wavesonics 363ef42
Fix multi-line spell check ranges
Wavesonics b357bfa
Better spellcheck options
Wavesonics 56c7cf6
re-organized into a more production ready structure
Wavesonics 661f4c4
API dump
Wavesonics e202ab2
Damn KorgeIO doesn't have all of the KMP targets
Wavesonics 2ea9bfa
Revert "Damn KorgeIO doesn't have all of the KMP targets"
Wavesonics 402b791
this version maybe?
Wavesonics 08da237
Fix merge
Wavesonics 781a381
Handle link clicks in BasicRichTextEditor
Wavesonics 5025119
Re move arg
Wavesonics 270609a
Add callback for handling click on RichSpans
Wavesonics 6a86da6
Add some code for testing span clicks
Wavesonics 65ffeec
Spellcheck test
Wavesonics 1eadc30
Make explicitly public
Wavesonics dde84e5
It compiles again
Wavesonics b9f89ff
API
Wavesonics e12da22
Add click position to RichSpanClickListener
Wavesonics 3a7a4f6
API
Wavesonics d63b539
Refactored so we dont need to expose things
Wavesonics 0940b42
Add onRichSpanClick to Outline
Wavesonics e5d525d
Correct touch position for contentPadding
Wavesonics 702948b
Will CI build this?
Wavesonics 12a8d64
Ooohhh this fixes it
Wavesonics f786215
clean up
Wavesonics ab9bddf
more cleanup
Wavesonics bae05e8
Provide a more robust way of handling different kinds of interactions…
Wavesonics cf1edd6
API Dump
Wavesonics 61f1247
Merge branch 'handle-link-click-in-editor' into spell-check-test
Wavesonics d471a4a
Simplified
Wavesonics 1d6990a
Filter for click type
Wavesonics e9c0cdc
Right click on desktop, tap on mobile
Wavesonics 3e26ba2
Fixed Touch VS Mouse input
Wavesonics 65a76d4
Clear menu when not handled
Wavesonics 9e4319f
Not needed
Wavesonics f7301f0
New min required for running on SDK 35 devices
Wavesonics ab11ee0
Not needed anymore
Wavesonics 5505a9c
Not needed anymore
Wavesonics 793b3d4
Split up the file
Wavesonics 16966e7
trivial cleanup
Wavesonics 29c5438
Re-organized into a spellcheck module
Wavesonics 0891bf1
Change SpellCheck to draw the traditional red squiggle
Wavesonics 47a9c60
Merge branch 'handle-link-click-in-editor' into spell-check-test
Wavesonics 8bf8154
Merge branch 'textchangeflow' into spell-check-test
Wavesonics 2287833
API Dump
Wavesonics e3cf052
Make SymSpell an API dependency
Wavesonics 4b9bc2e
Sample now uses the binary .fdic format for it's sample dictionary
Wavesonics e568d7b
Removed Korge, no longer needed
Wavesonics 9f6ab27
symspell version bump
Wavesonics e6328aa
Moved spell check utils to library
Wavesonics c057947
Merge branch 'main' into spell-check-test
Wavesonics 9f018f1
Remove the textChanges Flow
Wavesonics 87905ce
Moby Dick as spell check text
Wavesonics 4776c1e
Merge branch 'main' into spell-check-test
Wavesonics File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/build |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi | ||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl | ||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget | ||
|
||
plugins { | ||
alias(libs.plugins.kotlinMultiplatform) | ||
alias(libs.plugins.compose.compiler) | ||
alias(libs.plugins.composeMultiplatform) | ||
alias(libs.plugins.androidLibrary) | ||
alias(libs.plugins.bcv) | ||
id("module.publication") | ||
} | ||
|
||
kotlin { | ||
explicitApi() | ||
applyDefaultHierarchyTemplate() | ||
|
||
androidTarget { | ||
publishLibraryVariants("release") | ||
@OptIn(ExperimentalKotlinGradlePluginApi::class) | ||
compilerOptions { | ||
jvmTarget.set(JvmTarget.JVM_1_8) | ||
} | ||
} | ||
|
||
jvm("desktop") { | ||
@OptIn(ExperimentalKotlinGradlePluginApi::class) | ||
compilerOptions { | ||
jvmTarget.set(JvmTarget.JVM_11) | ||
} | ||
} | ||
|
||
js(IR) { | ||
browser() | ||
} | ||
@OptIn(ExperimentalWasmDsl::class) | ||
wasmJs { | ||
browser { | ||
testTask { | ||
enabled = false | ||
} | ||
} | ||
} | ||
|
||
iosX64() | ||
iosArm64() | ||
iosSimulatorArm64() | ||
|
||
sourceSets.commonMain.dependencies { | ||
implementation(projects.richeditorCompose) | ||
|
||
implementation(compose.ui) | ||
implementation(compose.foundation) | ||
implementation(compose.material3) | ||
|
||
// Spell Check | ||
implementation(libs.symspellkt) | ||
} | ||
|
||
sourceSets.commonTest.dependencies { | ||
implementation(kotlin("test")) | ||
} | ||
} | ||
|
||
android { | ||
namespace = "com.mohamedrejeb.richeditor.compose.spellcheck" | ||
compileSdk = libs.versions.android.compileSdk.get().toInt() | ||
|
||
defaultConfig { | ||
minSdk = libs.versions.android.minSdk.get().toInt() | ||
} | ||
|
||
compileOptions { | ||
sourceCompatibility = JavaVersion.VERSION_1_8 | ||
targetCompatibility = JavaVersion.VERSION_1_8 | ||
} | ||
} | ||
|
||
apiValidation { | ||
@OptIn(kotlinx.validation.ExperimentalBCVApi::class) | ||
klib { | ||
enabled = true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Add project specific ProGuard rules here. | ||
# You can control the set of applied configuration files using the | ||
# proguardFiles setting in build.gradle. | ||
# | ||
# For more details, see | ||
# http://developer.android.com/guide/developing/tools/proguard.html | ||
|
||
# If your project uses WebView with JS, uncomment the following | ||
# and specify the fully qualified class name to the JavaScript interface | ||
# class: | ||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { | ||
# public *; | ||
#} | ||
|
||
# Uncomment this to preserve the line number information for | ||
# debugging stack traces. | ||
#-keepattributes SourceFile,LineNumberTable | ||
|
||
# If you keep the line number information, uncomment this to | ||
# hide the original source file name. | ||
#-renamesourcefileattribute SourceFile |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
...k/src/commonMain/kotlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckState.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package com.mohamedrejeb.richeditor.compose.spellcheck | ||
|
||
import androidx.compose.ui.geometry.Offset | ||
import androidx.compose.ui.text.TextRange | ||
import androidx.compose.ui.util.fastForEach | ||
import com.darkrockstudios.symspellkt.api.SpellChecker | ||
import com.darkrockstudios.symspellkt.common.SuggestionItem | ||
import com.darkrockstudios.symspellkt.common.Verbosity | ||
import com.mohamedrejeb.richeditor.compose.spellcheck.utils.applyCapitalizationStrategy | ||
import com.mohamedrejeb.richeditor.compose.spellcheck.utils.isSpelledCorrectly | ||
import com.mohamedrejeb.richeditor.compose.spellcheck.utils.spellingIsCorrect | ||
import com.mohamedrejeb.richeditor.model.RichSpanStyle | ||
import com.mohamedrejeb.richeditor.model.RichTextState | ||
import com.mohamedrejeb.richeditor.utils.WordSegment | ||
|
||
public class SpellCheckState( | ||
public val richTextState: RichTextState, | ||
public var spellChecker: SpellChecker? | ||
) { | ||
private var lastTextHash = -1 | ||
private val misspelledWords = mutableListOf<WordSegment>() | ||
|
||
public fun handleSpanClick(span: RichSpanStyle, range: TextRange, click: Offset): WordSegment? { | ||
return if (span is SpellCheck) { | ||
findWordSegmentContainingRange( | ||
misspelledWords, | ||
range | ||
) | ||
} else { | ||
null | ||
} | ||
} | ||
|
||
public fun correctSpelling(segment: WordSegment, correction: String) { | ||
val currentStyle = richTextState.getSpanStyle(segment.range) | ||
richTextState.replaceTextRange(segment.range, correction) | ||
|
||
val correctionRange = | ||
TextRange(start = segment.range.start, end = segment.range.start + correction.length) | ||
richTextState.addSpanStyle(currentStyle, correctionRange) | ||
} | ||
|
||
/** | ||
* This is a very naive algorithm that just removes all spell check spans and | ||
* reruns the entire spell check again. | ||
*/ | ||
public fun runSpellCheck() { | ||
val sp = spellChecker ?: return | ||
|
||
richTextState.apply { | ||
// Remove all existing spell checks | ||
getAllRichSpans() | ||
.filter { it.first is SpellCheck } | ||
.forEach { span -> | ||
removeRichSpan(SpellCheck, span.second) | ||
} | ||
|
||
misspelledWords.clear() | ||
getWords().mapNotNullTo(misspelledWords) { segment -> | ||
val suggestions = sp.lookup(segment.text) | ||
if (suggestions.spellingIsCorrect(segment.text)) { | ||
null | ||
} else { | ||
segment | ||
} | ||
} | ||
|
||
misspelledWords.fastForEach { wordSegment -> | ||
addRichSpan(SpellCheck, wordSegment.range) | ||
} | ||
} | ||
} | ||
|
||
public fun onTextChange(richTextState: RichTextState) { | ||
val newTextHash = richTextState.annotatedString.hashCode() | ||
if (lastTextHash != newTextHash) { | ||
runSpellCheck() | ||
lastTextHash = newTextHash | ||
} | ||
} | ||
|
||
private fun findWordSegmentContainingRange( | ||
segments: List<WordSegment>, | ||
range: TextRange | ||
): WordSegment? { | ||
return segments.find { wordSegment -> | ||
val segmentRange = wordSegment.range | ||
range.start >= segmentRange.start && range.end <= segmentRange.end | ||
} | ||
} | ||
|
||
public fun getSuggestions(word: String): List<SuggestionItem> { | ||
val sp = spellChecker ?: return emptyList() | ||
|
||
val suggestions = sp.lookup(word, verbosity = Verbosity.All) | ||
val proposedSuggestions = if (word.isSpelledCorrectly(suggestions).not()) { | ||
// If things are misspelled, see if it just needs to be broken up | ||
val composition = sp.wordBreakSegmentation(word) | ||
val segmentedWord = composition.segmentedString | ||
if (segmentedWord != null | ||
&& segmentedWord.equals(word, ignoreCase = true).not() | ||
&& suggestions.find { it.term.equals(segmentedWord, ignoreCase = true) } == null | ||
) { | ||
// Add the segmented suggest as first item if it didn't already exist | ||
listOf(SuggestionItem(segmentedWord, 1.0, 0.1)) + suggestions | ||
} else { | ||
suggestions | ||
} | ||
} else { | ||
emptyList() | ||
} | ||
|
||
return proposedSuggestions.map { suggestionItem -> | ||
suggestionItem.copy( | ||
term = applyCapitalizationStrategy( | ||
source = word, | ||
target = suggestionItem.term | ||
) | ||
) | ||
} | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
...otlin/com/mohamedrejeb/richeditor/compose/spellcheck/SpellCheckTextContextMenuProvider.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package com.mohamedrejeb.richeditor.compose.spellcheck | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.MutableState | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.geometry.Offset | ||
import com.mohamedrejeb.richeditor.utils.WordSegment | ||
|
||
@Composable | ||
public expect fun SpellCheckTextContextMenuProvider( | ||
modifier: Modifier, | ||
spellCheckMenuState: SpellCheckMenuState, | ||
content: @Composable () -> Unit | ||
) | ||
|
||
public data class SpellCheckMenuState( | ||
val spellCheckState: SpellCheckState, | ||
) { | ||
val missSpelling: MutableState<MissSpelling?> = mutableStateOf(null) | ||
|
||
public fun clearSpellCheck() { | ||
missSpelling.value = null | ||
} | ||
|
||
public fun performCorrection(toReplace: WordSegment, correction: String) { | ||
spellCheckState.correctSpelling(toReplace, correction) | ||
clearSpellCheck() | ||
} | ||
|
||
public data class MissSpelling(val wordSegment: WordSegment, val menuPosition: Offset) | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't install APKs that have min SDK set below 26 on devices running SDK 35+