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) {