fix(editor): support static span colors for hex colors

This commit is contained in:
Akash Yadav
2023-09-25 23:33:49 +05:30
parent 30a03cb13a
commit ce4295b63e
17 changed files with 277 additions and 24 deletions
@@ -26,7 +26,8 @@
},
"string": {
"fg": "@string",
"completion": false
"completion": false,
"maybeHexColor": true
},
"variable.builtin": {
"fg": "@keyword",
@@ -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",
@@ -43,7 +43,8 @@
},
"string": {
"fg": "@string",
"completion": false
"completion": false,
"maybeHexColor": true
},
"string.regex": {
"fg": "@string",
@@ -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
@@ -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
}
}
}
@@ -26,7 +26,8 @@
},
"string": {
"fg": "@string",
"completion": false
"completion": false,
"maybeHexColor": true
},
"variable.builtin": {
"fg": "@keyword",
@@ -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",
@@ -43,7 +43,8 @@
},
"string": {
"fg": "@string",
"completion": false
"completion": false,
"maybeHexColor": true
},
"string.regex": {
"fg": "@string",
@@ -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
@@ -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
}
}
}
@@ -40,6 +40,8 @@
(cdata_end) @cdata.end
(cdata) @cdata.data
(char_data) @text
(eq) @operator
["<" "/" ">" "<?" "?>"] @operator
@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}
@@ -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. */
@@ -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 <https://www.gnu.org/licenses/>.
*/
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())
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.editor.language.treesitter
import com.itsaky.androidide.editor.schemes.LanguageScheme
typealias LangScheme = Pair<String, LanguageScheme>
@@ -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<String, Int> = 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
)
}
}
@@ -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) {