feat: add support for more tree-sitter query predicates

#match?
#not-match?
#any-of?
This commit is contained in:
Akash Yadav
2023-02-28 21:07:34 +05:30
parent 15e9483d95
commit acb7042a20
8 changed files with 723 additions and 10 deletions
+15
View File
@@ -549,6 +549,21 @@
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="CALL_PARAMETERS_LPAREN_ON_NEXT_LINE" value="false" />
<option name="CALL_PARAMETERS_RPAREN_ON_NEXT_LINE" value="false" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="false" />
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="false" />
<option name="ASSIGNMENT_WRAP" value="0" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="protobuf">
<option name="RIGHT_MARGIN" value="80" />
-1
View File
@@ -1,6 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
<option name="PREFERRED_PROJECT_CODE_STYLE" value="GoogleStyle" />
</state>
</component>
@@ -0,0 +1,412 @@
;;; Identifiers
(simple_identifier) @variable
; `this` this keyword inside classes
(this_expression) @variable.builtin
; `super` keyword inside classes
(super_expression) @variable.builtin
(class_parameter
(simple_identifier) @property)
(class_body
(property_declaration
(variable_declaration
(simple_identifier) @property)))
; id_1.id_2.id_3: `id_2` and `id_3` are assumed as object properties
(_
(navigation_suffix
(simple_identifier) @property))
; SCREAMING CASE identifiers are assumed to be constants
((simple_identifier) @constant
(#match? @constant "^[A-Z][A-Z0-9_]*$"))
(_
(navigation_suffix
(simple_identifier) @constant
(#lua-match? @constant "^[A-Z][A-Z0-9_]*$")))
(enum_entry
(simple_identifier) @constant)
(type_identifier) @type
; '?' operator, replacement for Java @Nullable
(nullable_type) @punctuation.special
(type_alias
(type_identifier) @type.definition)
((type_identifier) @type.builtin
(#any-of? @type.builtin
"Byte"
"Short"
"Int"
"Long"
"UByte"
"UShort"
"UInt"
"ULong"
"Float"
"Double"
"Boolean"
"Char"
"String"
"Array"
"ByteArray"
"ShortArray"
"IntArray"
"LongArray"
"UByteArray"
"UShortArray"
"UIntArray"
"ULongArray"
"FloatArray"
"DoubleArray"
"BooleanArray"
"CharArray"
"Map"
"Set"
"List"
"EmptyMap"
"EmptySet"
"EmptyList"
"MutableMap"
"MutableSet"
"MutableList"
))
(package_header "package" @keyword
. (identifier (simple_identifier) @namespace))
(import_header
"import" @include)
; The last `simple_identifier` in a `import_header` will always either be a function
; or a type. Classes can appear anywhere in the import path, unlike functions
(import_header
(identifier
(simple_identifier) @type @_import)
(import_alias
(type_identifier) @type.definition)?
(#match? @_import "^[A-Z]"))
(import_header
(identifier
(simple_identifier) @function @_import .)
(import_alias
(type_identifier) @function)?
(#match? @_import "^[a-z]"))
; TODO: Separate labeled returns/breaks/continue/super/this
; Must be implemented in the parser first
(label) @label
;;; Function definitions
(function_declaration
(simple_identifier) @function)
(getter
("get") @function.builtin)
(setter
("set") @function.builtin)
(primary_constructor) @constructor
(secondary_constructor
("constructor") @constructor)
(constructor_invocation
(user_type
(type_identifier) @constructor))
(anonymous_initializer
("init") @constructor)
(parameter
(simple_identifier) @parameter)
(parameter_with_optional_type
(simple_identifier) @parameter)
; lambda parameters
(lambda_literal
(lambda_parameters
(variable_declaration
(simple_identifier) @parameter)))
;;; Function calls
; function()
(call_expression
. (simple_identifier) @function.call)
; ::function
(callable_reference
. (simple_identifier) @function.call)
; object.function() or object.property.function()
(call_expression
(navigation_expression
(navigation_suffix
(simple_identifier) @function.call) . ))
(call_expression
. (simple_identifier) @function.builtin
(#any-of? @function.builtin
"arrayOf"
"arrayOfNulls"
"byteArrayOf"
"shortArrayOf"
"intArrayOf"
"longArrayOf"
"ubyteArrayOf"
"ushortArrayOf"
"uintArrayOf"
"ulongArrayOf"
"floatArrayOf"
"doubleArrayOf"
"booleanArrayOf"
"charArrayOf"
"emptyArray"
"mapOf"
"setOf"
"listOf"
"emptyMap"
"emptySet"
"emptyList"
"mutableMapOf"
"mutableSetOf"
"mutableListOf"
"print"
"println"
"error"
"TODO"
"run"
"runCatching"
"repeat"
"lazy"
"lazyOf"
"enumValues"
"enumValueOf"
"assert"
"check"
"checkNotNull"
"require"
"requireNotNull"
"with"
"suspend"
"synchronized"
))
;;; Literals
(comment) @comment
(shebang_line) @preproc
(comment) @spell
(real_literal) @float
[
(integer_literal)
(long_literal)
(hex_literal)
(bin_literal)
(unsigned_literal)
] @number
[
"null" ; should be highlighted the same as booleans
(boolean_literal)
] @boolean
(character_literal) @character
[
(line_string_literal)
(multi_line_string_literal)
] @string
; NOTE: Escapes not allowed in multi-line strings
(line_string_literal (character_escape_seq) @string.escape)
; There are 3 ways to define a regex
; - "[abc]?".toRegex()
(call_expression
(navigation_expression
([(line_string_literal) (multi_line_string_literal)] @string.regex)
(navigation_suffix
((simple_identifier) @_function
(#eq? @_function "toRegex")))))
; - Regex("[abc]?")
(call_expression
((simple_identifier) @_function
(#eq? @_function "Regex"))
(call_suffix
(value_arguments
(value_argument
[ (line_string_literal) (multi_line_string_literal) ] @string.regex))))
; - Regex.fromLiteral("[abc]?")
(call_expression
(navigation_expression
((simple_identifier) @_class
(#eq? @_class "Regex"))
(navigation_suffix
((simple_identifier) @_function
(#eq? @_function "fromLiteral"))))
(call_suffix
(value_arguments
(value_argument
[ (line_string_literal) (multi_line_string_literal) ] @string.regex))))
;;; Keywords
(type_alias "typealias" @keyword)
(companion_object "companion" @keyword)
[
(class_modifier)
(member_modifier)
(function_modifier)
(property_modifier)
(platform_modifier)
(variance_modifier)
(parameter_modifier)
(visibility_modifier)
(reification_modifier)
(inheritance_modifier)
] @type.qualifier
[
"val"
"var"
"enum"
"class"
"object"
"interface"
"fun"
; "typeof" ; NOTE: It is reserved for future use
] @keyword
("fun") @keyword.function
(jump_expression) @keyword.return
[
"if"
"else"
"when"
] @conditional
[
"for"
"do"
"while"
] @repeat
[
"try"
"catch"
"throw"
"finally"
] @exception
(annotation
"@" @attribute (use_site_target)? @attribute)
(annotation
(user_type
(type_identifier) @attribute))
(annotation
(constructor_invocation
(user_type
(type_identifier) @attribute)))
(file_annotation
"@" @attribute "file" @attribute ":" @attribute)
(file_annotation
(user_type
(type_identifier) @attribute))
(file_annotation
(constructor_invocation
(user_type
(type_identifier) @attribute)))
;;; Operators & Punctuation
[
"!"
"!="
"!=="
"="
"=="
"==="
">"
">="
"<"
"<="
"||"
"&&"
"+"
"++"
"+="
"-"
"--"
"-="
"*"
"*="
"/"
"/="
"%"
"%="
"?."
"?:"
"!!"
"is"
"!is"
"in"
"!in"
"as"
"as?"
".."
"->"
] @operator
[
"(" ")"
"[" "]"
"{" "}"
] @punctuation.bracket
[
"."
","
";"
":"
"::"
] @punctuation.delimiter
; NOTE: `interpolated_identifier`s can be highlighted in any way
(line_string_literal
"$" @punctuation.special
(interpolated_identifier) @none @variable)
(line_string_literal
"${" @punctuation.special
(interpolated_expression) @none
"}" @punctuation.special)
(multi_line_string_literal
"$" @punctuation.special
(interpolated_identifier) @none @variable)
(multi_line_string_literal
"${" @punctuation.special
(interpolated_expression) @none
"}" @punctuation.special)
@@ -0,0 +1,69 @@
/*
* 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.predicates
import com.itsaky.androidide.treesitter.TSQuery
import com.itsaky.androidide.treesitter.TSQueryMatch
import com.itsaky.androidide.treesitter.TSQueryPredicateStep
import io.github.rosemoe.sora.editor.ts.predicate.PredicateResult
import io.github.rosemoe.sora.editor.ts.predicate.TsClientPredicateStep
import io.github.rosemoe.sora.editor.ts.predicate.TsPredicate
import io.github.rosemoe.sora.editor.ts.predicate.builtin.getCaptureContent
/**
* [TsPredicate] implementation for '#any-of?' query predicates.
*
* Syntax : `"#any-of?" @capture "string" ["string", ... ] Done`
*
* Checks if the text of `@capture` matches (literally) any of the `"string"` defined.
*
* @author Akash Yadav
*/
object AnyOfPredicate : TreeSitterPredicate() {
override val name: String
get() = "any-of"
override fun canHandle(steps: List<TsClientPredicateStep>): Boolean {
return steps.size > 4 &&
steps.let {
it[0].predicateType == TSQueryPredicateStep.Type.String &&
it[1].predicateType == TSQueryPredicateStep.Type.Capture &&
it[it.lastIndex].predicateType == TSQueryPredicateStep.Type.Done &&
it.subList(2, it.lastIndex - 1).all { step ->
step.predicateType == TSQueryPredicateStep.Type.String
}
}
}
override fun doPredicateInternal(
tsQuery: TSQuery,
text: CharSequence,
match: TSQueryMatch,
predicate: List<TsClientPredicateStep>
): PredicateResult {
val captured = getCaptureContent(tsQuery, match, predicate[1].content, text)
val toMatch = predicate.subList(2, predicate.lastIndex - 1).map { it.content }
for (capture in captured) {
if (capture !in toMatch) {
return PredicateResult.REJECT
}
}
return PredicateResult.ACCEPT
}
}
@@ -0,0 +1,86 @@
/*
* 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.predicates
import com.itsaky.androidide.treesitter.TSQuery
import com.itsaky.androidide.treesitter.TSQueryMatch
import com.itsaky.androidide.treesitter.TSQueryPredicateStep
import io.github.rosemoe.sora.editor.ts.predicate.PredicateResult
import io.github.rosemoe.sora.editor.ts.predicate.TsClientPredicateStep
import io.github.rosemoe.sora.editor.ts.predicate.TsPredicate
import io.github.rosemoe.sora.editor.ts.predicate.builtin.getCaptureContent
import io.github.rosemoe.sora.editor.ts.predicate.builtin.parametersMatch
import java.util.concurrent.ConcurrentHashMap
import java.util.regex.PatternSyntaxException
/**
* [TsPredicate] implementation for '#match?' query predicates.
*
* @author Akash Yadav
*/
open class MatchPredicate protected constructor() : TreeSitterPredicate() {
override val name: String
get() = "match"
companion object {
@JvmField
val INSTANCE = MatchPredicate()
@JvmField
val PARAMETERS =
arrayOf(
TSQueryPredicateStep.Type.String,
TSQueryPredicateStep.Type.Capture,
TSQueryPredicateStep.Type.String,
TSQueryPredicateStep.Type.Done
)
}
private val cache = ConcurrentHashMap<String, Regex>()
override fun doPredicateInternal(
tsQuery: TSQuery,
text: CharSequence,
match: TSQueryMatch,
predicate: List<TsClientPredicateStep>
): PredicateResult {
val captured = getCaptureContent(tsQuery, match, predicate[1].content, text)
try {
var regex = cache[predicate[2].content]
if (regex == null) {
regex = Regex(predicate[2].content)
cache[predicate[2].content] = regex
}
for (str in captured) {
if (regex.find(str) == null) {
return PredicateResult.REJECT
}
}
return PredicateResult.ACCEPT
} catch (e: PatternSyntaxException) {
e.printStackTrace()
return PredicateResult.UNHANDLED
}
}
override fun canHandle(steps: List<TsClientPredicateStep>): Boolean {
return parametersMatch(steps, PARAMETERS)
}
}
@@ -0,0 +1,50 @@
/*
* 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.predicates
import com.itsaky.androidide.treesitter.TSQuery
import com.itsaky.androidide.treesitter.TSQueryMatch
import io.github.rosemoe.sora.editor.ts.predicate.PredicateResult
import io.github.rosemoe.sora.editor.ts.predicate.TsClientPredicateStep
import io.github.rosemoe.sora.editor.ts.predicate.TsPredicate
/**
* [TsPredicate] implementation for '#not-match?' query predicates.
*
* @author Akash Yadav
*/
object NotMatchPredicate : MatchPredicate() {
override val name: String
get() = "not-match"
override fun doPredicateInternal(
tsQuery: TSQuery,
text: CharSequence,
match: TSQueryMatch,
predicate: List<TsClientPredicateStep>
): PredicateResult {
// Invert the result of the match predicate
return when (val result = super.doPredicateInternal(tsQuery, text, match, predicate)) {
PredicateResult.ACCEPT -> PredicateResult.REJECT
PredicateResult.REJECT -> PredicateResult.ACCEPT
else -> result
}
}
}
@@ -0,0 +1,74 @@
/*
* 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.predicates
import com.itsaky.androidide.treesitter.TSQuery
import com.itsaky.androidide.treesitter.TSQueryMatch
import io.github.rosemoe.sora.editor.ts.predicate.PredicateResult
import io.github.rosemoe.sora.editor.ts.predicate.TsClientPredicateStep
import io.github.rosemoe.sora.editor.ts.predicate.TsPredicate
/**
* Base class for tree-sitter predicate implementations.
*
* @author Akash Yadav
*/
abstract class TreeSitterPredicate : TsPredicate {
/** The name of the predicate that will be used to match. */
abstract val name: String
/**
* Whether the implmentation can handle the given predicate steps.
*
* @param steps The predicate steps.
* @return `true` if and only if the implementatin can handle the given predicate steps, `false`
* otherwise.
*/
abstract fun canHandle(steps: List<TsClientPredicateStep>): Boolean
/**
* Performs the predicate check.
*
* @param tsQuery The [TSQuery] for the predicate.
* @param text The editor text.
* @param match The [TSQueryMatch] object.
* @param predicate The predicate steps.
* @return The result of the predicate check.
*/
abstract fun doPredicateInternal(
tsQuery: TSQuery,
text: CharSequence,
match: TSQueryMatch,
predicate: List<TsClientPredicateStep>
): PredicateResult
override fun doPredicate(
tsQuery: TSQuery,
text: CharSequence,
match: TSQueryMatch,
predicate: List<TsClientPredicateStep>
): PredicateResult {
if (predicate.isEmpty() || predicate[0].content != "${name}?" || !canHandle(predicate)) {
return PredicateResult.UNHANDLED
}
return doPredicateInternal(tsQuery, text, match, predicate)
}
}
@@ -19,6 +19,9 @@ package com.itsaky.androidide.editor.schemes
import android.content.Context
import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguageSpec
import com.itsaky.androidide.editor.language.treesitter.predicates.AnyOfPredicate
import com.itsaky.androidide.editor.language.treesitter.predicates.MatchPredicate
import com.itsaky.androidide.editor.language.treesitter.predicates.NotMatchPredicate
import com.itsaky.androidide.treesitter.TSLanguage
import com.itsaky.androidide.utils.ILogger
import io.github.rosemoe.sora.editor.ts.LocalsCaptureSpec
@@ -33,7 +36,7 @@ object LanguageSpecProvider {
const val BASE_SPEC_PATH = "editor/treesitter"
private val log = ILogger.newInstance("LanguageSpecProvider")
@JvmStatic
@JvmOverloads
fun getLanguageSpec(
@@ -42,15 +45,20 @@ object LanguageSpecProvider {
lang: TSLanguage,
localsCaptureSpec: LocalsCaptureSpec = LocalsCaptureSpec.DEFAULT
): TreeSitterLanguageSpec {
val editorLangSpec = TsLanguageSpec(
language = lang,
highlightScmSource = readScheme(context, type, "highlights"),
localsScmSource = readScheme(context, type, "locals"),
codeBlocksScmSource = readScheme(context, type, "blocks"),
bracketsScmSource = readScheme(context, type, "brackets"),
localsCaptureSpec = localsCaptureSpec
val editorLangSpec =
TsLanguageSpec(
language = lang,
highlightScmSource = readScheme(context, type, "highlights"),
localsScmSource = readScheme(context, type, "locals"),
codeBlocksScmSource = readScheme(context, type, "blocks"),
bracketsScmSource = readScheme(context, type, "brackets"),
localsCaptureSpec = localsCaptureSpec,
predicates = listOf(MatchPredicate.INSTANCE, NotMatchPredicate, AnyOfPredicate)
)
return TreeSitterLanguageSpec(
spec = editorLangSpec,
indentsQueryScm = readScheme(context, type, "indents")
)
return TreeSitterLanguageSpec(spec = editorLangSpec, indentsQueryScm = readScheme(context, type, "indents"))
}
private fun readScheme(context: Context, type: String, name: String): String {