diff --git a/.gitmodules b/.gitmodules index 5b6609a80..fa1e0900a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -209,7 +209,7 @@ [submodule "helix-syntax/languages/tree-sitter-graphql"] path = helix-syntax/languages/tree-sitter-graphql url = https://github.com/bkegley/tree-sitter-graphql - shallow = true + shallow = true [submodule "helix-syntax/languages/tree-sitter-elm"] path = helix-syntax/languages/tree-sitter-elm url = https://github.com/elm-tooling/tree-sitter-elm @@ -229,3 +229,8 @@ [submodule "helix-syntax/languages/tree-sitter-erlang"] path = helix-syntax/languages/tree-sitter-erlang url = https://github.com/the-mikedavis/tree-sitter-erlang + shallow = true +[submodule "helix-syntax/languages/tree-sitter-kotlin"] + path = helix-syntax/languages/tree-sitter-kotlin + url = https://github.com/fwcd/tree-sitter-kotlin.git + shallow = true diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 894050731..495382152 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -27,6 +27,7 @@ | javascript | ✓ | | ✓ | `typescript-language-server` | | json | ✓ | | ✓ | | | julia | ✓ | | | `julia` | +| kotlin | ✓ | | | `kotlin-language-server` | | latex | ✓ | | | | | lean | ✓ | | | `lean` | | ledger | ✓ | | | | diff --git a/helix-syntax/languages/tree-sitter-kotlin b/helix-syntax/languages/tree-sitter-kotlin new file mode 160000 index 000000000..a4f71eb9b --- /dev/null +++ b/helix-syntax/languages/tree-sitter-kotlin @@ -0,0 +1 @@ +Subproject commit a4f71eb9b8c9b19ded3e0e9470be4b1b77c2b569 diff --git a/languages.toml b/languages.toml index 35758566a..9876bcf10 100644 --- a/languages.toml +++ b/languages.toml @@ -741,3 +741,12 @@ file-types = ["erl", "hrl", "app", "rebar.config"] roots = ["rebar.config"] comment-token = "%%" indent = { tab-width = 4, unit = " " } + +[[language]] +name = "kotlin" +scope = "source.kotlin" +file-types = ["kt", "kts"] +roots = ["settings.gradle", "settings.gradle.kts"] +comment-token = "//" +indent = { tab-width = 4, unit = " " } +language-server = { command = "kotlin-language-server" } diff --git a/runtime/queries/kotlin/folds.scm b/runtime/queries/kotlin/folds.scm new file mode 100644 index 000000000..768972b8b --- /dev/null +++ b/runtime/queries/kotlin/folds.scm @@ -0,0 +1,17 @@ +[ + (import_list) + + (when_expression) + (control_structure_body) + + (lambda_literal) + (function_body) + (primary_constructor) + (secondary_constructor) + (anonymous_initializer) + + (class_body) + (enum_class_body) + + (interpolated_expression) +] @fold diff --git a/runtime/queries/kotlin/highlights.scm b/runtime/queries/kotlin/highlights.scm new file mode 100644 index 000000000..7b90fcf92 --- /dev/null +++ b/runtime/queries/kotlin/highlights.scm @@ -0,0 +1,295 @@ +;;; Operators & Punctuation + +(multi_line_string_literal + "$" @punctuation + (interpolated_identifier) @none) +(multi_line_string_literal + "${" @punctuation + (interpolated_expression) @none + "}" @punctuation.) + +; NOTE: `interpolated_identifier`s can be highlighted in any way +(line_string_literal + "$" @punctuation + (interpolated_identifier) @none) +(line_string_literal + "${" @punctuation + (interpolated_expression) @none + "}" @punctuation) + +[ + "." + "," + ";" + ":" + "::" +] @punctuation.delimiter + +[ + "(" ")" + "[" "]" + "{" "}" +] @punctuation.bracket + +[ + "!" + "!=" + "!==" + "=" + "==" + "===" + ">" + ">=" + "<" + "<=" + "||" + "&&" + "+" + "++" + "+=" + "-" + "--" + "-=" + "*" + "*=" + "/" + "/=" + "%" + "%=" + "?." + "?:" + "!!" + "is" + "!is" + "in" + "!in" + "as" + "as?" + ".." + "->" +] @operator + +;;; Keywords + +(type_alias "typealias" @keyword) +[ + (class_modifier) + (member_modifier) + (function_modifier) + (property_modifier) + (platform_modifier) + (variance_modifier) + (parameter_modifier) + (visibility_modifier) + (reification_modifier) + (inheritance_modifier) +]@keyword + +[ + "val" + "var" + "enum" + "class" + "object" + "interface" +; "typeof" ; NOTE: It is reserved for future use +] @keyword + +("fun") @keyword.function + +(jump_expression) @keyword.control.return + +[ + "if" + "else" + "when" +] @keyword.control.conditional + +[ + "for" + "do" + "while" +] @keyword.control.repeat + +[ + "try" + "catch" + "throw" + "finally" +] @keyword.control.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))) + +;;; Literals +; NOTE: Escapes not allowed in multi-line strings +(line_string_literal (character_escape_seq) @constant.character.escape) + +[ + (line_string_literal) + (multi_line_string_literal) +] @string + +(character_literal) @constant.character + +[ + "null" ; should be highlighted the same as booleans + (boolean_literal) +] @constant.builtin.boolean + +(real_literal) @constant.numeric.float +[ + (integer_literal) + (long_literal) + (hex_literal) + (bin_literal) + (unsigned_literal) +] @constant.numeric.integer + +[ + (comment) + (shebang_line) +] @comment + +;;; Function calls + +(call_expression + . (simple_identifier) @function.builtin + (#match? @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)$")) + +; object.function() or object.property.function() +(call_expression + (navigation_expression + (navigation_suffix + (simple_identifier) @function) . )) + +; function() +(call_expression + . (simple_identifier) @function) + +;;; Function definitions + +; lambda parameters +(lambda_literal + (lambda_parameters + (variable_declaration + (simple_identifier) @variable.parameter))) + +(parameter_with_optional_type + (simple_identifier) @variable.parameter) + +(parameter + (simple_identifier) @variable.parameter) + +(anonymous_initializer + ("init") @constructor) + +(constructor_invocation + (user_type + (type_identifier) @constructor)) + +(secondary_constructor + ("constructor") @constructor) +(primary_constructor) @constructor + +(getter + ("get") @function.builtin) +(setter + ("set") @function.builtin) + +(function_declaration + . (simple_identifier) @function) + +; TODO: Seperate labeled returns/breaks/continue/super/this +; Must be implemented in the parser first +(label) @label + +(import_header + (identifier + (simple_identifier) @function @_import .) + (import_alias + (type_identifier) @function)? + (#match? @_import "^[a-z]")) + +; 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)? + (#match? @_import "^[A-Z]")) + +(import_header + "import" @keyword.control.import) + +(package_header + . (identifier)) @namespace + +((type_identifier) @type.builtin + (#match? @function.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)$")) + +(type_identifier) @type + +(enum_entry + (simple_identifier) @constant) + +(_ + (navigation_suffix + (simple_identifier) @constant + (#match? @constant "^[A-Z][A-Z0-9_]*$"))) + +; SCREAMING CASE identifiers are assumed to be constants +((simple_identifier) @constant +(#match? @constant "^[A-Z][A-Z0-9_]*$")) + +; id_1.id_2.id_3: `id_2` and `id_3` are assumed as object properties +(_ + (navigation_suffix + (simple_identifier) @variable.other.member)) + +(class_body + (property_declaration + (variable_declaration + (simple_identifier) @variable.other.member))) + +(class_parameter + (simple_identifier) @variable.other.member) + +; `super` keyword inside classes +(super_expression) @variable.builtin + +; `this` this keyword inside classes +(this_expression) @variable.builtin + +;;; Identifiers +; `field` keyword inside property getter/setter +; FIXME: This will highlight the keyword outside of getters and setters +; since tree-sitter does not allow us to check for arbitrary nestation +((simple_identifier) @variable.builtin +(#eq? @variable.builtin "field")) + +; `it` keyword inside lambdas +; FIXME: This will highlight the keyword outside of lambdas since tree-sitter +; does not allow us to check for arbitrary nestation +((simple_identifier) @variable.builtin +(#eq? @variable.builtin "it")) + +(simple_identifier) @variable diff --git a/runtime/queries/kotlin/injections.scm b/runtime/queries/kotlin/injections.scm new file mode 100644 index 000000000..abdc20502 --- /dev/null +++ b/runtime/queries/kotlin/injections.scm @@ -0,0 +1,36 @@ +((comment) @injection.content + (#set! injection.language "comment")) + +; There are 3 ways to define a regex +; - "[abc]?".toRegex() +((call_expression + (navigation_expression + ([(line_string_literal) (multi_line_string_literal)] @injection.content) + (navigation_suffix + ((simple_identifier) @_function + (#eq? @_function "toRegex"))))) + (#set! injection.language "regex")) + +; - Regex("[abc]?") +((call_expression + ((simple_identifier) @_function + (#eq? @_function "Regex")) + (call_suffix + (value_arguments + (value_argument + [ (line_string_literal) (multi_line_string_literal) ] @injection.content)))) + (#set! injection.language "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) ] @injection.content)))) + (#set! injection.language "regex"))