From ce4295b63e03e1506daad2f4cfd4b2a5c902d53c Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Mon, 25 Sep 2023 23:33:49 +0530 Subject: [PATCH] fix(editor): support static span colors for hex colors --- .../editor/schemes/default-dark/java.json | 3 +- .../editor/schemes/default-dark/json.json | 5 +- .../editor/schemes/default-dark/kotlin.json | 3 +- .../editor/schemes/default-dark/scheme.prop | 2 +- .../editor/schemes/default-dark/xml.json | 9 +- .../assets/editor/schemes/default/java.json | 3 +- .../assets/editor/schemes/default/json.json | 5 +- .../assets/editor/schemes/default/kotlin.json | 3 +- .../assets/editor/schemes/default/scheme.prop | 2 +- .../assets/editor/schemes/default/xml.json | 9 +- .../editor/treesitter/xml/highlights.scm | 2 + .../treesitter/TreeSitterAnalyzeManager.kt | 51 ++++++++ .../language/treesitter/TreeSitterLanguage.kt | 17 ++- .../treesitter/TreeSitterSpanFactory.kt | 119 ++++++++++++++++++ .../editor/language/treesitter/common.kt | 22 ++++ .../editor/schemes/IDEColorScheme.kt | 44 +++++-- .../editor/schemes/internal/parser/common.kt | 2 +- 17 files changed, 277 insertions(+), 24 deletions(-) create mode 100644 editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterAnalyzeManager.kt create mode 100644 editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterSpanFactory.kt create mode 100644 editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/common.kt diff --git a/editor/src/main/assets/editor/schemes/default-dark/java.json b/editor/src/main/assets/editor/schemes/default-dark/java.json index abda35ccc..0c1bdcaea 100644 --- a/editor/src/main/assets/editor/schemes/default-dark/java.json +++ b/editor/src/main/assets/editor/schemes/default-dark/java.json @@ -26,7 +26,8 @@ }, "string": { "fg": "@string", - "completion": false + "completion": false, + "maybeHexColor": true }, "variable.builtin": { "fg": "@keyword", diff --git a/editor/src/main/assets/editor/schemes/default-dark/json.json b/editor/src/main/assets/editor/schemes/default-dark/json.json index 3fcdbe9ef..5c94da0ca 100644 --- a/editor/src/main/assets/editor/schemes/default-dark/json.json +++ b/editor/src/main/assets/editor/schemes/default-dark/json.json @@ -2,7 +2,10 @@ "types": ["json"], "styles": { "string.special.key": "@json.key", - "string" : "@string", + "string" : { + "fg": "@string", + "maybeHexColor": true + }, "number" : "@number", "constant.builtin": "@keyword", "escape": "@json.escape", diff --git a/editor/src/main/assets/editor/schemes/default-dark/kotlin.json b/editor/src/main/assets/editor/schemes/default-dark/kotlin.json index 3e82a30b4..22a88ecd7 100644 --- a/editor/src/main/assets/editor/schemes/default-dark/kotlin.json +++ b/editor/src/main/assets/editor/schemes/default-dark/kotlin.json @@ -43,7 +43,8 @@ }, "string": { "fg": "@string", - "completion": false + "completion": false, + "maybeHexColor": true }, "string.regex": { "fg": "@string", diff --git a/editor/src/main/assets/editor/schemes/default-dark/scheme.prop b/editor/src/main/assets/editor/schemes/default-dark/scheme.prop index 17fb89e07..dc9122120 100644 --- a/editor/src/main/assets/editor/schemes/default-dark/scheme.prop +++ b/editor/src/main/assets/editor/schemes/default-dark/scheme.prop @@ -2,7 +2,7 @@ scheme.name=AndroidIDE Default - Dark # The version code of the color scheme -scheme.version=12 +scheme.version=13 # Whether the scheme is dark or light scheme.isDark=true diff --git a/editor/src/main/assets/editor/schemes/default-dark/xml.json b/editor/src/main/assets/editor/schemes/default-dark/xml.json index f9bc4aef3..a0cc77e2b 100644 --- a/editor/src/main/assets/editor/schemes/default-dark/xml.json +++ b/editor/src/main/assets/editor/schemes/default-dark/xml.json @@ -28,6 +28,13 @@ "xmlns.prefix": "@ns_prefix", "attr.prefix" : "@ns_prefix", "attr.name": "@onSurface", - "attr.value": "@string" + "attr.value": { + "fg": "@string", + "maybeHexColor": true + }, + "text": { + "fg": "@onSurface", + "maybeHexColor": true + } } } \ No newline at end of file diff --git a/editor/src/main/assets/editor/schemes/default/java.json b/editor/src/main/assets/editor/schemes/default/java.json index abda35ccc..0c1bdcaea 100644 --- a/editor/src/main/assets/editor/schemes/default/java.json +++ b/editor/src/main/assets/editor/schemes/default/java.json @@ -26,7 +26,8 @@ }, "string": { "fg": "@string", - "completion": false + "completion": false, + "maybeHexColor": true }, "variable.builtin": { "fg": "@keyword", diff --git a/editor/src/main/assets/editor/schemes/default/json.json b/editor/src/main/assets/editor/schemes/default/json.json index 3fcdbe9ef..5c94da0ca 100644 --- a/editor/src/main/assets/editor/schemes/default/json.json +++ b/editor/src/main/assets/editor/schemes/default/json.json @@ -2,7 +2,10 @@ "types": ["json"], "styles": { "string.special.key": "@json.key", - "string" : "@string", + "string" : { + "fg": "@string", + "maybeHexColor": true + }, "number" : "@number", "constant.builtin": "@keyword", "escape": "@json.escape", diff --git a/editor/src/main/assets/editor/schemes/default/kotlin.json b/editor/src/main/assets/editor/schemes/default/kotlin.json index 3e82a30b4..22a88ecd7 100644 --- a/editor/src/main/assets/editor/schemes/default/kotlin.json +++ b/editor/src/main/assets/editor/schemes/default/kotlin.json @@ -43,7 +43,8 @@ }, "string": { "fg": "@string", - "completion": false + "completion": false, + "maybeHexColor": true }, "string.regex": { "fg": "@string", diff --git a/editor/src/main/assets/editor/schemes/default/scheme.prop b/editor/src/main/assets/editor/schemes/default/scheme.prop index 44ee0174d..36c604589 100644 --- a/editor/src/main/assets/editor/schemes/default/scheme.prop +++ b/editor/src/main/assets/editor/schemes/default/scheme.prop @@ -2,7 +2,7 @@ scheme.name=AndroidIDE Default # The version code of the color scheme -scheme.version=12 +scheme.version=13 # Whether the scheme is dark or light scheme.isDark=false diff --git a/editor/src/main/assets/editor/schemes/default/xml.json b/editor/src/main/assets/editor/schemes/default/xml.json index f9bc4aef3..a0cc77e2b 100644 --- a/editor/src/main/assets/editor/schemes/default/xml.json +++ b/editor/src/main/assets/editor/schemes/default/xml.json @@ -28,6 +28,13 @@ "xmlns.prefix": "@ns_prefix", "attr.prefix" : "@ns_prefix", "attr.name": "@onSurface", - "attr.value": "@string" + "attr.value": { + "fg": "@string", + "maybeHexColor": true + }, + "text": { + "fg": "@onSurface", + "maybeHexColor": true + } } } \ No newline at end of file diff --git a/editor/src/main/assets/editor/treesitter/xml/highlights.scm b/editor/src/main/assets/editor/treesitter/xml/highlights.scm index 9c4b1d0ca..46507e658 100644 --- a/editor/src/main/assets/editor/treesitter/xml/highlights.scm +++ b/editor/src/main/assets/editor/treesitter/xml/highlights.scm @@ -40,6 +40,8 @@ (cdata_end) @cdata.end (cdata) @cdata.data +(char_data) @text + (eq) @operator ["<" "/" ">" ""] @operator \ No newline at end of file diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterAnalyzeManager.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterAnalyzeManager.kt new file mode 100644 index 000000000..1e1188f23 --- /dev/null +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterAnalyzeManager.kt @@ -0,0 +1,51 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.editor.language.treesitter + +import com.itsaky.androidide.editor.schemes.LanguageScheme +import io.github.rosemoe.sora.editor.ts.TsAnalyzeManager +import io.github.rosemoe.sora.editor.ts.TsLanguageSpec +import io.github.rosemoe.sora.editor.ts.TsTheme +import io.github.rosemoe.sora.lang.styling.Styles + +/** + * [TsAnalyzeManager] implementation for tree sitter languages. + * + * @author Akash Yadav + */ +class TreeSitterAnalyzeManager( + languageSpec: TsLanguageSpec, + theme: TsTheme +) : TsAnalyzeManager(languageSpec, theme) { + + override var styles: Styles = Styles() + set(value) { + field = value + resetSpanFactory(value, langScheme) + } + + internal var langScheme: LanguageScheme? = null + set(value) { + field = value + resetSpanFactory(styles, value) + } + + private fun resetSpanFactory(styles: Styles, langScheme: LanguageScheme?) { + spanFactory = TreeSitterSpanFactory(reference, languageSpec.tsQuery, styles, langScheme) + } +} \ No newline at end of file diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguage.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguage.kt index 54eba9220..dca252098 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguage.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguage.kt @@ -23,6 +23,7 @@ import com.itsaky.androidide.editor.language.newline.TSBracketsHandler import com.itsaky.androidide.editor.language.utils.CommonSymbolPairs import com.itsaky.androidide.editor.schemes.IDEColorScheme import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider +import com.itsaky.androidide.editor.schemes.LanguageScheme import com.itsaky.androidide.editor.schemes.LanguageSpecProvider.getLanguageSpec import com.itsaky.androidide.editor.schemes.LocalCaptureSpecProvider.newLocalCaptureSpec import com.itsaky.androidide.treesitter.TSLanguage @@ -31,7 +32,6 @@ import com.itsaky.androidide.treesitter.TSQueryCapture import com.itsaky.androidide.treesitter.TSQueryCursor import com.itsaky.androidide.treesitter.TSQueryMatch import com.itsaky.androidide.utils.ILogger -import io.github.rosemoe.sora.editor.ts.TsAnalyzeManager import io.github.rosemoe.sora.editor.ts.TsTheme import io.github.rosemoe.sora.lang.analysis.AnalyzeManager import io.github.rosemoe.sora.text.ContentReference @@ -48,10 +48,11 @@ abstract class TreeSitterLanguage(context: Context, lang: TSLanguage, type: Stri private lateinit var tsTheme: TsTheme private lateinit var languageSpec: TreeSitterLanguageSpec - private val analyzer by lazy { TsAnalyzeManager(languageSpec.spec, tsTheme) } - + private val analyzer by lazy { TreeSitterAnalyzeManager(languageSpec.spec, tsTheme) } private val newlineHandlersLazy by lazy { createNewlineHandlers() } + private var languageScheme: LanguageScheme? = null + private val log = ILogger.newInstance("TreeSitterLanguage") init { @@ -69,6 +70,7 @@ abstract class TreeSitterLanguage(context: Context, lang: TSLanguage, type: Stri } val langScheme = scheme.languages[type] ?: return@readScheme + this.languageScheme = langScheme langScheme.styles.forEach { tsTheme.putStyleRule(it.key, it.value.makeStyle()) } } } @@ -78,7 +80,11 @@ abstract class TreeSitterLanguage(context: Context, lang: TSLanguage, type: Stri } override fun getAnalyzeManager(): AnalyzeManager { - return this.analyzer + return this.analyzer.also { + if (it.langScheme == null) { + it.langScheme = this.languageScheme + } + } } override fun getSymbolPairs(): SymbolPairMatch { @@ -152,7 +158,8 @@ abstract class TreeSitterLanguage(context: Context, lang: TSLanguage, type: Stri } override fun destroy() { - languageSpec.close() + this.languageSpec.close() + this.languageScheme = null } /** A [Factory] creates instance of a specific [TreeSitterLanguage] implementation. */ diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterSpanFactory.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterSpanFactory.kt new file mode 100644 index 000000000..b05284be1 --- /dev/null +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterSpanFactory.kt @@ -0,0 +1,119 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.editor.language.treesitter + +import android.graphics.Color +import androidx.core.graphics.ColorUtils +import com.itsaky.androidide.editor.schemes.LanguageScheme +import com.itsaky.androidide.treesitter.TSQuery +import com.itsaky.androidide.treesitter.TSQueryCapture +import com.itsaky.androidide.utils.ILogger +import io.github.rosemoe.sora.editor.ts.spans.DefaultSpanFactory +import io.github.rosemoe.sora.editor.ts.spans.TsSpanFactory +import io.github.rosemoe.sora.lang.styling.Span +import io.github.rosemoe.sora.lang.styling.StaticColorSpan +import io.github.rosemoe.sora.lang.styling.Styles +import io.github.rosemoe.sora.text.ContentReference + +/** + * [TsSpanFactory] for tree sitter languages. + * + * @author Akash Yadav + */ +class TreeSitterSpanFactory( + private var content: ContentReference?, + private var query: TSQuery?, + private var styles: Styles?, + private var langScheme: LanguageScheme? +) : DefaultSpanFactory() { + + companion object { + + private val log = ILogger.newInstance("TreeSitterSpanFactory") + + @JvmStatic + private val HEX_REGEX = "#\\b([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\\b".toRegex() + } + + override fun close() { + content = null + query = null + styles = null + langScheme = null + } + + override fun createSpan(capture: TSQueryCapture, column: Int, spanStyle: Long): Span { + val content = this.content?.reference ?: return super.createSpan(capture, column, spanStyle) + val query = this.query ?: return super.createSpan(capture, column, spanStyle) + val langScheme = this.langScheme ?: return super.createSpan(capture, column, spanStyle) + + val captureName = query.getCaptureNameForId(capture.index) + val styleDef = langScheme.getStyles()[captureName] + if (styleDef?.maybeHexColor != true) { + return super.createSpan(capture, column, spanStyle) + } + + val (start, end) = content.indexer.run { + getCharPosition(capture.node.startByte / 2) to getCharPosition(capture.node.endByte / 2) + } + + if (start.line != end.line || start.column != column) { + // A HEX color can only defined on a single line + return super.createSpan(capture, column, spanStyle) + } + + var text: CharSequence = content.subContent(start.line, start.column + 1, end.line, end.column) + + // TODO(itsaky): update the API in sora-editor such that we can add multiple spans for this capture + // This method should probably return a list of spans + val result = HEX_REGEX.find(text) ?: run { + // Does not contain a HEX color string + return super.createSpan(capture, column, spanStyle) + } + + val color = try { + text = result.groupValues[1] + if (text.length == 3){ + // HEX color is in the form of #FFF + // convert it to #FFFFFF format (6 character long) + val r = text[0] + val g = text[1] + val b = text[2] + text = "$r$r$g$g$b$b" + } + + if (text.length == 6) { + // Prepend alpha value + text = "FF${text}" + } + + java.lang.Long.parseLong(text, 16) + } catch (e: Exception) { + log.error("Failed to parse hex color. color=$text", e) + return super.createSpan(capture, column, spanStyle) + }.toInt() + + val textColor = if (ColorUtils.calculateLuminance(color) > 0.5f) { + Color.BLACK + } else { + Color.WHITE + } + + return StaticColorSpan.obtain(color, textColor, column, styleDef.makeStaticStyle()) + } +} \ No newline at end of file diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/common.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/common.kt new file mode 100644 index 000000000..863177d6b --- /dev/null +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/common.kt @@ -0,0 +1,22 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.editor.language.treesitter + +import com.itsaky.androidide.editor.schemes.LanguageScheme + +typealias LangScheme = Pair \ No newline at end of file diff --git a/editor/src/main/java/com/itsaky/androidide/editor/schemes/IDEColorScheme.kt b/editor/src/main/java/com/itsaky/androidide/editor/schemes/IDEColorScheme.kt index b944cbbf5..fbe3064c4 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/schemes/IDEColorScheme.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/schemes/IDEColorScheme.kt @@ -20,6 +20,7 @@ package com.itsaky.androidide.editor.schemes import com.itsaky.androidide.editor.schemes.internal.parser.SchemeParser import com.itsaky.androidide.syntax.colorschemes.DynamicColorScheme import io.github.rosemoe.sora.lang.styling.TextStyle +import io.github.rosemoe.sora.widget.schemes.EditorColorScheme import java.io.File import java.util.TreeSet @@ -31,19 +32,19 @@ class IDEColorScheme(internal val file: File, val key: String) : DynamicColorSch var name: String = "" internal set - + var version: Int = 0 internal set var isDarkScheme: Boolean = false internal set - + var darkVariant: IDEColorScheme? = null internal set var definitions: Map = emptyMap() internal set - + private var colorId = endColorId internal fun load() { @@ -65,7 +66,7 @@ class IDEColorScheme(internal val file: File, val key: String) : DynamicColorSch // in this case, the below properties will be null return editorScheme?.get(type) ?: colorIds?.get(type) ?: super.getColor(type) } - + override fun isDark(): Boolean { return this.isDarkScheme } @@ -94,8 +95,8 @@ class LanguageScheme { fun isLocalScope(capture: String): Boolean { return localScopes.contains(capture) } - - fun isMembersScope(capture: String) : Boolean { + + fun isMembersScope(capture: String): Boolean { return localMembersScopes.contains(capture) } @@ -106,6 +107,7 @@ class LanguageScheme { fun isLocalDefVal(capture: String): Boolean { return localDefVals.contains(capture) } + fun isLocalRef(capture: String): Boolean { return localRefs.contains(capture) } @@ -120,17 +122,43 @@ class LanguageScheme { * @property italic Whether the highlighted region should have italic text. * @property strikeThrough Whether the highlighted region should have strikethrough text. * @property completion Whether code completions can be performed in the highlighted region. + * @property maybeHexColor Whether the node represented by this style can contain HEX color strings. + * If this value is `true`, the node's text will be parsed to check if it represents a valid HEX color. + * If it does, that color will be used as the node's background color. The foreground color of the node + * will be automatically selected based on the HEX color's brightness. */ data class StyleDef( - var fg: Int, + var fg: Int = EditorColorScheme.TEXT_NORMAL, var bg: Int = 0, var bold: Boolean = false, var italic: Boolean = false, var strikeThrough: Boolean = false, - var completion: Boolean = true + var completion: Boolean = true, + var maybeHexColor: Boolean = false ) { + /** + * Make the style for the style definition. + * + * @see TextStyle.makeStyle + */ fun makeStyle(): Long { return TextStyle.makeStyle(fg, bg, bold, italic, strikeThrough, !completion) } + + /** + * Make the static style for this style definition. The background color ID in the returned style is + * always [EditorColorScheme.STATIC_SPAN_BACKGROUND] and the foreground color ID is always + * [EditorColorScheme.STATIC_SPAN_FOREGROUND]. + */ + fun makeStaticStyle(): Long { + return TextStyle.makeStyle( + EditorColorScheme.STATIC_SPAN_FOREGROUND, + EditorColorScheme.STATIC_SPAN_BACKGROUND, + bold, + italic, + strikeThrough, + !completion + ) + } } diff --git a/editor/src/main/java/com/itsaky/androidide/editor/schemes/internal/parser/common.kt b/editor/src/main/java/com/itsaky/androidide/editor/schemes/internal/parser/common.kt index 4174c0846..5b3f264ed 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/schemes/internal/parser/common.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/schemes/internal/parser/common.kt @@ -23,7 +23,6 @@ import com.google.gson.stream.JsonToken.STRING import com.itsaky.androidide.editor.schemes.IDEColorScheme import com.itsaky.androidide.editor.schemes.LanguageScheme import com.itsaky.androidide.editor.schemes.StyleDef -import com.itsaky.androidide.editor.schemes.internal.parser.SchemeParser.EditorColors import java.io.File /** @author Akash Yadav */ @@ -126,6 +125,7 @@ fun IDEColorScheme.parseStyleDef(reader: JsonReader): StyleDef { "italic" -> def.italic = reader.nextBoolean() "strikethrough" -> def.strikeThrough = reader.nextBoolean() "completion" -> def.completion = reader.nextBoolean() + "maybeHexColor" -> def.maybeHexColor = reader.nextBoolean() } } if (def.fg == 0) {