diff --git a/.gitignore b/.gitignore index 17a4290..9edf612 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ .externalNativeBuild .cxx local.properties +/.kotlin/ diff --git a/.run/iosExample.run.xml b/.run/iosExample.run.xml index 4419f81..f91f1c3 100644 --- a/.run/iosExample.run.xml +++ b/.run/iosExample.run.xml @@ -1,5 +1,5 @@ - + diff --git a/androidExample/build.gradle.kts b/androidExample/build.gradle.kts index ca013c2..06405c8 100644 --- a/androidExample/build.gradle.kts +++ b/androidExample/build.gradle.kts @@ -59,5 +59,5 @@ dependencies { implementation(compose.material3) implementation(compose.ui) implementation(compose.materialIconsExtended) - implementation(libs.kodeview) + implementation(project(":kodeview")) } \ No newline at end of file diff --git a/desktopExample/build.gradle.kts b/desktopExample/build.gradle.kts index 511e5c5..3b39176 100644 --- a/desktopExample/build.gradle.kts +++ b/desktopExample/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.jvm) alias(libs.plugins.compose) + alias(libs.plugins.compose.compiler) } dependencies { @@ -14,8 +15,7 @@ dependencies { @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) implementation(compose.components.resources) implementation(compose.desktop.currentOs) - implementation(libs.kodeview) - alias(libs.plugins.compose.compiler) + implementation(project(":kodeview")) } compose.desktop { diff --git a/desktopExample/src/main/kotlin/Main.kt b/desktopExample/src/main/kotlin/Main.kt index 69f2bec..1b04ab1 100644 --- a/desktopExample/src/main/kotlin/Main.kt +++ b/desktopExample/src/main/kotlin/Main.kt @@ -2,22 +2,22 @@ package dev.snipme.desktopexample import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.Divider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.VerticalDivider import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign @@ -31,10 +31,8 @@ import dev.snipme.highlights.model.SyntaxLanguage import dev.snipme.highlights.model.SyntaxTheme import dev.snipme.highlights.model.SyntaxThemes import dev.snipme.highlights.model.SyntaxThemes.useDark -import dev.snipme.kodeview.view.material3.CodeEditText import dev.snipme.kodeview.view.CodeTextView - -private val windowSize = 600.dp +import dev.snipme.kodeview.view.material3.CodeEditText private val sampleCode = """ @@ -72,18 +70,14 @@ fun main() = application { Window( onCloseRequest = ::exitApplication, title = "KodeView example", - state = rememberWindowState( - width = windowSize, - height = windowSize, - ) + state = rememberWindowState() ) { Surface { Column( modifier = Modifier .fillMaxSize() .padding(16.dp), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.SpaceBetween + verticalArrangement = Arrangement.Center, ) { Spacer(Modifier.height(8.dp)) @@ -106,36 +100,38 @@ fun main() = application { Spacer(modifier = Modifier.size(16.dp)) - CodeTextView(highlights = highlights) - - Spacer(modifier = Modifier.size(16.dp)) - - Divider() - - Spacer(modifier = Modifier.size(16.dp)) - - Text("Edit this...") - CodeEditText( - highlights = highlights, - onValueChange = { textValue -> - highlightsState.value = highlights.getBuilder() - .code(textValue) - .build() - }, - colors = TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - errorIndicatorColor = Color.Transparent, - ), - ) + Row( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + ) { + CodeTextView( + modifier = Modifier.weight(1f), + highlights = highlights, + ) + VerticalDivider(Modifier.padding(8.dp)) + CodeEditText( + modifier = Modifier.weight(1f), + label = { Text("Edit code") }, + highlights = highlights, + onValueChange = { textValue -> + highlightsState.value = highlights.getBuilder() + .code(textValue) + .build() + }, + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent, + ), + ) + } Spacer(modifier = Modifier.size(16.dp)) - Spacer(modifier = Modifier.weight(1f)) - Dropdown( options = SyntaxThemes.getNames(), selected = SyntaxThemes.themes().keys.indexOf(highlights.getTheme().key), diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e82fd9a..78767f8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ kotlinMultiplatform = "2.0.20" compose = "1.6.11" androidLibrary = "8.6.1" kodeview = "0.8.0" -highlights = "0.7.1" +highlights = "1.0.0" composeMaterial = "1.4.0" [libraries] diff --git a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeEditText.kt b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeEditText.kt index 6d43a6c..80230a5 100644 --- a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeEditText.kt +++ b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeEditText.kt @@ -9,16 +9,20 @@ import androidx.compose.material.TextField import androidx.compose.material.TextFieldColors import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation -import calculateFieldPhraseUpdate +import copySpanStyles +import dev.snipme.highlights.DefaultHighlightsResultListener +import updateIndentations import dev.snipme.highlights.Highlights +import dev.snipme.highlights.model.CodeHighlight import generateAnnotatedString @Composable @@ -28,7 +32,7 @@ fun CodeEditText( modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, - translateTabToSpaces: Boolean = true, + handleIndentations: Boolean = true, textStyle: TextStyle = LocalTextStyle.current, label: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null, @@ -47,24 +51,37 @@ fun CodeEditText( ) { val currentText = remember { mutableStateOf( - TextFieldValue() + TextFieldValue( + AnnotatedString(highlights.getCode()) + ) + ) + } + + LaunchedEffect(highlights) { + highlights.getHighlightsAsync(object : DefaultHighlightsResultListener() { + override fun onSuccess(result: List) { + currentText.value = currentText.value.copy( + annotatedString = result.generateAnnotatedString(currentText.value.text), + ) + } + }) + } + + fun updateNewValue(change: TextFieldValue) { + val updated = change.updateIndentations(handleIndentations) + if (updated.text != currentText.value.text) { + onValueChange(updated.text) + } + + currentText.value = updated.copySpanStyles( + currentText.value ) } TextField( modifier = modifier.fillMaxWidth(), - onValueChange = { - val fieldUpdate = it.calculateFieldPhraseUpdate(translateTabToSpaces) - currentText.value = fieldUpdate - onValueChange(fieldUpdate.text) - }, - value = TextFieldValue( - selection = currentText.value.selection, - composition = currentText.value.composition, - annotatedString = buildAnnotatedString { - generateAnnotatedString(highlights) - }, - ), + onValueChange = ::updateNewValue, + value = currentText.value, enabled = enabled, readOnly = readOnly, textStyle = textStyle, diff --git a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeEditTextExtensions.kt b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeEditTextExtensions.kt deleted file mode 100644 index 2365479..0000000 --- a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeEditTextExtensions.kt +++ /dev/null @@ -1,44 +0,0 @@ -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.TextFieldValue -import dev.snipme.highlights.Highlights -import dev.snipme.highlights.model.BoldHighlight -import dev.snipme.highlights.model.ColorHighlight - -internal const val TAB_LENGTH = 4 -internal const val TAB_CHAR = "\t" - -internal fun TextFieldValue.calculateFieldPhraseUpdate(translateTabToSpaces: Boolean) = - if (translateTabToSpaces && text.contains(TAB_CHAR)) { - val result = text.replace(TAB_CHAR, " ".repeat(TAB_LENGTH)) - this.copy(text = result, TextRange(selection.start + TAB_LENGTH - 1)) - } else { - this - } - -internal fun AnnotatedString.Builder.generateAnnotatedString(highlights: Highlights) { - append(highlights.getCode()) - - highlights.getHighlights() - .filterIsInstance() - .forEach { - addStyle( - SpanStyle(color = Color(it.rgb).copy(alpha = 1f)), - start = it.location.start, - end = it.location.end, - ) - } - - highlights.getHighlights() - .filterIsInstance() - .forEach { - addStyle( - SpanStyle(fontWeight = FontWeight.Bold), - start = it.location.start, - end = it.location.end, - ) - } -} \ No newline at end of file diff --git a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeTextView.kt b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeTextView.kt index 7b7dd68..0035379 100644 --- a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeTextView.kt +++ b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/CodeTextView.kt @@ -1,48 +1,47 @@ package dev.snipme.kodeview.view import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.AnnotatedString import dev.snipme.highlights.Highlights -import dev.snipme.highlights.model.BoldHighlight -import dev.snipme.highlights.model.ColorHighlight +import generateAnnotatedString @Composable fun CodeTextView( modifier: Modifier = Modifier.background(Color.Transparent), highlights: Highlights ) { - Surface { - Text( - modifier = modifier, - text = buildAnnotatedString { - append(highlights.getCode()) + var textState by remember { + mutableStateOf(AnnotatedString(highlights.getCode())) + } - highlights.getHighlights() - .filterIsInstance() - .forEach { - addStyle( - SpanStyle(color = Color(it.rgb).copy(alpha = 1f)), - start = it.location.start, - end = it.location.end, - ) - } + LaunchedEffect(highlights) { + textState = highlights + .getHighlights() + .generateAnnotatedString(highlights.getCode()) + } - highlights.getHighlights() - .filterIsInstance() - .forEach { - addStyle( - SpanStyle(fontWeight = FontWeight.Bold), - start = it.location.start, - end = it.location.end, - ) - } - }) + Surface( + modifier = modifier, + color = Color.Transparent + ) { + Text( + modifier = modifier + .verticalScroll(rememberScrollState()) + .horizontalScroll(rememberScrollState()), + text = textState + ) } } \ No newline at end of file diff --git a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/Extensions.kt b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/Extensions.kt new file mode 100644 index 0000000..3156faa --- /dev/null +++ b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/Extensions.kt @@ -0,0 +1,54 @@ +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue +import dev.snipme.highlights.model.BoldHighlight +import dev.snipme.highlights.model.CodeHighlight +import dev.snipme.highlights.model.ColorHighlight + +internal const val TAB_LENGTH = 4 +internal const val TAB_CHAR = "\t" + +fun List.generateAnnotatedString(code: String) = + buildAnnotatedString { + append(code) + + forEach { + when (it) { + is BoldHighlight -> addStyle( + SpanStyle(fontWeight = FontWeight.Bold), + start = it.location.start, + end = it.location.end, + ) + + is ColorHighlight -> addStyle( + SpanStyle(color = Color(it.rgb).copy(alpha = 1f)), + start = it.location.start, + end = it.location.end, + ) + } + } + } + + +internal fun TextFieldValue.updateIndentations(handleIndentations: Boolean) = + if (handleIndentations && text.contains(TAB_CHAR)) { + val result = text.replace(TAB_CHAR, " ".repeat(TAB_LENGTH)) + this.copy(text = result, TextRange(selection.start + TAB_LENGTH - 1)) + } else { + this + } + +internal fun TextFieldValue.copySpanStyles(source: TextFieldValue) = + this.copy( + annotatedString = buildAnnotatedString { + append(text) + source.annotatedString.spanStyles.forEach { + if (it.start >= 0 && it.end > it.start && it.end < text.length) { + addStyle(it.item, it.start, it.end) + } + } + } + ) \ No newline at end of file diff --git a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/material3/CodeEditText.kt b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/material3/CodeEditText.kt index 45c5f41..07b207f 100644 --- a/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/material3/CodeEditText.kt +++ b/kodeview/src/commonMain/kotlin/dev/snipme/kodeview/view/material3/CodeEditText.kt @@ -5,16 +5,20 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation -import calculateFieldPhraseUpdate +import copySpanStyles +import updateIndentations +import dev.snipme.highlights.DefaultHighlightsResultListener import dev.snipme.highlights.Highlights +import dev.snipme.highlights.model.CodeHighlight import generateAnnotatedString import androidx.compose.material3.LocalTextStyle as LocalTextStyle3 import androidx.compose.material3.TextField as TextField3 @@ -28,7 +32,7 @@ fun CodeEditText( modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, - translateTabToSpaces: Boolean = true, + handleIndentations: Boolean = true, textStyle: TextStyle = LocalTextStyle3.current, label: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null, @@ -47,24 +51,37 @@ fun CodeEditText( ) { val currentText = remember { mutableStateOf( - TextFieldValue() + TextFieldValue( + AnnotatedString(highlights.getCode()) + ) + ) + } + + LaunchedEffect(highlights) { + highlights.getHighlightsAsync(object : DefaultHighlightsResultListener() { + override fun onSuccess(result: List) { + currentText.value = currentText.value.copy( + annotatedString = result.generateAnnotatedString(currentText.value.text), + ) + } + }) + } + + fun updateNewValue(change: TextFieldValue) { + val updated = change.updateIndentations(handleIndentations) + if (updated.text != currentText.value.text) { + onValueChange(updated.text) + } + + currentText.value = updated.copySpanStyles( + currentText.value ) } TextField3( modifier = modifier.fillMaxWidth(), - onValueChange = { - val fieldUpdate = it.calculateFieldPhraseUpdate(translateTabToSpaces) - currentText.value = fieldUpdate - onValueChange(fieldUpdate.text) - }, - value = TextFieldValue( - selection = currentText.value.selection, - composition = currentText.value.composition, - annotatedString = buildAnnotatedString { - generateAnnotatedString(highlights) - }, - ), + onValueChange = ::updateNewValue, + value = currentText.value, enabled = enabled, readOnly = readOnly, textStyle = textStyle, @@ -92,7 +109,7 @@ fun CodeEditTextSwiftUi( modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, - translateTabToSpaces: Boolean = true, + handleIndentations: Boolean = true, textStyle: TextStyle = LocalTextStyle3.current, label: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null, @@ -109,35 +126,40 @@ fun CodeEditTextSwiftUi( shape: Shape = TextFieldDefaults3.shape, colors: TextFieldColors3 = TextFieldDefaults3.colors() ) { - val highlightsState = remember { - mutableStateOf(highlights) - } - val currentText = remember { mutableStateOf( - TextFieldValue().copy( - annotatedString = buildAnnotatedString { - generateAnnotatedString(highlightsState.value) - } + TextFieldValue( + AnnotatedString(highlights.getCode()) ) ) } + LaunchedEffect(highlights) { + highlights.getHighlightsAsync(object : DefaultHighlightsResultListener() { + override fun onSuccess(result: List) { + currentText.value = currentText.value.copy( + annotatedString = result.generateAnnotatedString(currentText.value.text), + ) + } + }) + } + + fun updateNewValue(change: TextFieldValue) { + val updated = change.updateIndentations(handleIndentations) + + if (updated.text != currentText.value.text) { + onValueChange(updated) + } + + currentText.value = updated.copySpanStyles( + currentText.value + ) + } + TextField3( modifier = modifier.fillMaxWidth(), value = currentText.value, - onValueChange = { - val fieldUpdate = it.calculateFieldPhraseUpdate(translateTabToSpaces) - highlightsState.value = - highlightsState.value.getBuilder().code(fieldUpdate.text).build() - onValueChange(fieldUpdate) - currentText.value = - fieldUpdate.copy( - annotatedString = buildAnnotatedString { - generateAnnotatedString(highlightsState.value) - } - ) - }, + onValueChange = ::updateNewValue, enabled = enabled, readOnly = readOnly, textStyle = textStyle, diff --git a/webExample/build.gradle.kts b/webExample/build.gradle.kts index 8b62e53..7606433 100644 --- a/webExample/build.gradle.kts +++ b/webExample/build.gradle.kts @@ -19,7 +19,7 @@ kotlin { implementation(compose.material) implementation(compose.material3) implementation(compose.materialIconsExtended) - implementation(libs.kodeview) + implementation(project(":kodeview")) } } }