Add Kotlin support

Closes #220
pull/261/head
Wilfred Hughes 2022-04-14 00:21:29 +07:00
parent 600a4fb039
commit 059fc824ff
10 changed files with 459 additions and 2 deletions

@ -1,5 +1,9 @@
## 0.27 (unreleased)
### Parsing
Added support for Kotlin.
### Command Line Interface
Difftastic now validates environment variables the same way as

@ -144,6 +144,11 @@ fn main() {
src_dir: "vendor/tree-sitter-json-src",
extra_files: vec![],
},
TreeSitterParser {
name: "tree-sitter-kotlin",
src_dir: "vendor/tree-sitter-kotlin-src",
extra_files: vec!["scanner.c"],
},
TreeSitterParser {
name: "tree-sitter-lua",
src_dir: "vendor/tree-sitter-lua-src",

@ -20,6 +20,7 @@ tool that understands syntax. It supports the following languages:
* Java
* JavaScript (and JSX)
* JSON
* Kotlin
* Lua
* Nix
* OCaml

@ -21,6 +21,7 @@ Difftastic uses the following tree-sitter parsers:
| Java | [tree-sitter/tree-sitter-java](https://github.com/tree-sitter/tree-sitter-java) |
| JavaScript, JSX | [tree-sitter/tree-sitter-javascript](https://github.com/tree-sitter/tree-sitter-javascript) |
| JSON | [tree-sitter/tree-sitter-json](https://github.com/tree-sitter/tree-sitter-json) |
| Kotlin | [fwcd/tree-sitter-kotlin](https://github.com/fwcd/tree-sitter-kotlin) |
| Lua | [nvim-treesitter/tree-sitter-lua](https://github.com/nvim-treesitter/tree-sitter-lua) |
| Nix | [cstrahan/tree-sitter-nix](https://github.com/cstrahan/tree-sitter-nix) |
| OCaml | [tree-sitter/tree-sitter-ocaml](https://github.com/tree-sitter/tree-sitter-ocaml) |

@ -0,0 +1,216 @@
/*
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.iosched.model
import com.google.samples.apps.iosched.model.SessionType.Companion.reservableTypes
import org.threeten.bp.ZonedDateTime
typealias SessionId = String
/**
* Describes a conference session. Sessions have specific start and end times, and they represent a
* variety of conference events: talks, sandbox demos, office hours, etc. A session is usually
* associated with one or more [Tag]s.
*/
data class Session(
/**
* Unique string identifying this session.
*/
val id: SessionId,
/**
* Start time of the session
*/
val startTime: ZonedDateTime,
/**
* End time of the session
*/
val endTime: ZonedDateTime,
/**
* Session title.
*/
val title: String,
/**
* Body of text explaining this session in detail.
*/
val description: String,
/**
* The session room.
*/
val room: Room?,
/**
* Full URL for the session online.
*/
val sessionUrl: String,
/**
* Indicates if the Session has a live stream.
*/
val isLivestream: Boolean,
/**
* Full URL to YouTube.
*/
val youTubeUrl: String,
/**
* URL to the Dory page.
*/
val doryLink: String,
/**
* The [Tag]s associated with the session. Ordered, with the most important tags appearing
* first.
*/
val tags: List<Tag>,
/**
* Subset of [Tag]s that are for visual consumption.
*/
val displayTags: List<Tag>,
/**
* The session speakers.
*/
val speakers: Set<Speaker>,
/**
* The session's photo URL.
*/
val photoUrl: String,
/**
* IDs of the sessions related to this session.
*/
val relatedSessions: Set<SessionId>
) {
/**
* Returns whether the session is currently being live streamed or not.
*/
fun isLive(): Boolean {
val now = ZonedDateTime.now()
// TODO: Determine when a session is live based on the time AND the liveStream being
// available.
return startTime <= now && endTime >= now
}
val hasPhoto inline get() = photoUrl.isNotEmpty()
/**
* Returns whether the session has a video or not. A session could be live streaming or have a
* recorded session. Both live stream and recorded videos are stored in [Session.youTubeUrl].
*/
val hasVideo inline get() = youTubeUrl.isNotEmpty()
val hasPhotoOrVideo inline get() = hasPhoto || hasVideo
/**
* The year the session was held.
*/
val year = startTime.year
/**
* The duration of the session.
*/
// TODO: Get duration from the YouTube video. Not every talk fills the full session time.
val duration = endTime.toInstant().toEpochMilli() - startTime.toInstant().toEpochMilli()
/**
* The type of the event e.g. Session, Codelab etc.
*/
val type: SessionType by lazy(LazyThreadSafetyMode.PUBLICATION) {
SessionType.fromTags(tags)
}
fun levelTag(): Tag? {
return tags.firstOrNull { it.category == Tag.CATEGORY_LEVEL }
}
/**
* Whether this event is reservable, based upon [type].
*/
val isReservable: Boolean by lazy(LazyThreadSafetyMode.PUBLICATION) {
type in reservableTypes
}
fun isOverlapping(session: Session): Boolean {
return this.startTime < session.endTime && this.endTime > session.startTime
}
/**
* Detailed description of this event. Includes description, speakers, and live-streaming URL.
*/
fun getCalendarDescription(
paragraphDelimiter: String,
speakerDelimiter: String
): String = buildString {
append(description)
append(paragraphDelimiter)
append(speakers.joinToString(speakerDelimiter) { speaker -> speaker.name })
if (!isLivestream && !youTubeUrl.isEmpty()) {
append(paragraphDelimiter)
append(youTubeUrl)
}
}
}
/**
* Represents the type of the event e.g. Session, Codelab etc.
*/
enum class SessionType(val displayName: String) {
KEYNOTE("Keynote"),
SESSION("Session"),
APP_REVIEW("App Reviews"),
GAME_REVIEW("Game Reviews"),
OFFICE_HOURS("Office Hours"),
CODELAB("Codelab"),
MEETUP("Meetup"),
AFTER_DARK("After Dark"),
UNKNOWN("Unknown");
companion object {
/**
* Examine the given [tags] to determine the [SessionType]. Defaults to [SESSION] if no
* category tag is found.
*/
fun fromTags(tags: List<Tag>): SessionType {
val typeTag = tags.firstOrNull { it.category == Tag.CATEGORY_TYPE }
return when (typeTag?.tagName) {
Tag.TYPE_KEYNOTE -> KEYNOTE
Tag.TYPE_SESSIONS -> SESSION
Tag.TYPE_APP_REVIEWS -> APP_REVIEW
Tag.TYPE_GAME_REVIEWS -> GAME_REVIEW
Tag.TYPE_OFFICEHOURS -> OFFICE_HOURS
Tag.TYPE_CODELABS -> CODELAB
Tag.TYPE_MEETUPS -> MEETUP
Tag.TYPE_AFTERDARK -> AFTER_DARK
else -> UNKNOWN
}
}
internal val reservableTypes = listOf(SESSION, OFFICE_HOURS, APP_REVIEW, GAME_REVIEW)
}
}

@ -0,0 +1,212 @@
/*
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.iosched.model
import com.google.samples.apps.iosched.model.SessionType.Companion.reservableTypes
import org.threeten.bp.ZonedDateTime
typealias SessionId = String
/**
* Describes a conference session. Sessions have specific start and end times, and they represent a
* variety of conference events: talks, sandbox demos, office hours, etc. A session is usually
* associated with one or more [Tag]s.
*/
data class Session(
/**
* Unique string identifying this session.
*/
val id: SessionId,
/**
* Start time of the session
*/
val startTime: ZonedDateTime,
/**
* End time of the session
*/
val endTime: ZonedDateTime,
/**
* Session title.
*/
val title: String,
/**
* Body of text explaining this session in detail.
*/
val description: String,
/**
* The session room.
*/
val room: Room?,
/**
* Full URL for the session online.
*/
val sessionUrl: String,
/**
* Indicates if the Session has a live stream.
*/
val isLivestream: Boolean,
/**
* Full URL to YouTube.
*/
val youTubeUrl: String,
/**
* URL to the Dory page.
*/
val doryLink: String,
/**
* The [Tag]s associated with the session. Ordered, with the most important tags appearing
* first.
*/
val tags: List<Tag>,
/**
* Subset of [Tag]s that are for visual consumption.
*/
val displayTags: List<Tag>,
/**
* The session speakers.
*/
val speakers: Set<Speaker>,
/**
* The session's photo URL.
*/
val photoUrl: String?,
/**
* IDs of the sessions related to this session.
*/
val relatedSessions: Set<SessionId>
) {
/**
* Returns whether the session is currently being live streamed or not.
*/
fun isLive(): Boolean {
val now = ZonedDateTime.now()
// TODO: Determine when a session is live based on the time AND the liveStream being
// available.
return startTime <= now && endTime >= now
}
/**
* Returns whether the session has a video or not. A session could be live streaming or have a
* recorded session. Both live stream and recorded videos are stored in [Session.youTubeUrl].
*/
fun hasVideo() = youTubeUrl.isNotEmpty()
/**
* The year the session was held.
*/
val year = startTime.year
/**
* The duration of the session.
*/
// TODO: Get duration from the YouTube video. Not every talk fills the full session time.
val duration = endTime.toInstant().toEpochMilli() - startTime.toInstant().toEpochMilli()
/**
* The type of the event e.g. Session, Codelab etc.
*/
val type: SessionType by lazy(LazyThreadSafetyMode.PUBLICATION) {
SessionType.fromTags(tags)
}
fun levelTag(): Tag? {
return tags.firstOrNull { it.category == Tag.CATEGORY_LEVEL }
}
/**
* Whether this event is reservable, based upon [type].
*/
val isReservable: Boolean by lazy(LazyThreadSafetyMode.PUBLICATION) {
type in reservableTypes
}
fun isOverlapping(session: Session): Boolean {
return this.startTime < session.endTime && this.endTime > session.startTime
}
/**
* Detailed description of this event. Includes description, speakers, and live-streaming URL.
*/
fun getCalendarDescription(
paragraphDelimiter: String,
speakerDelimiter: String
): String = buildString {
append(description)
append(paragraphDelimiter)
append(speakers.joinToString(speakerDelimiter) { speaker -> speaker.name })
if (!isLivestream && !youTubeUrl.isEmpty()) {
append(paragraphDelimiter)
append(youTubeUrl)
}
}
}
/**
* Represents the type of the event e.g. Session, Codelab etc.
*/
enum class SessionType(val displayName: String) {
KEYNOTE("Keynote"),
SESSION("Session"),
APP_REVIEW("App Reviews"),
GAME_REVIEW("Game Reviews"),
OFFICE_HOURS("Office Hours"),
CODELAB("Codelab"),
MEETUP("Meetup"),
AFTER_DARK("After Dark"),
UNKNOWN("Unknown");
companion object {
/**
* Examine the given [tags] to determine the [SessionType]. Defaults to [SESSION] if no
* category tag is found.
*/
fun fromTags(tags: List<Tag>): SessionType {
val typeTag = tags.firstOrNull { it.category == Tag.CATEGORY_TYPE }
return when (typeTag?.tagName) {
Tag.TYPE_KEYNOTE -> KEYNOTE
Tag.TYPE_SESSIONS -> SESSION
Tag.TYPE_APP_REVIEWS -> APP_REVIEW
Tag.TYPE_GAME_REVIEWS -> GAME_REVIEW
Tag.TYPE_OFFICEHOURS -> OFFICE_HOURS
Tag.TYPE_CODELABS -> CODELAB
Tag.TYPE_MEETUPS -> MEETUP
Tag.TYPE_AFTERDARK -> AFTER_DARK
else -> UNKNOWN
}
}
internal val reservableTypes = listOf(SESSION, OFFICE_HOURS, APP_REVIEW, GAME_REVIEW)
}
}

@ -37,6 +37,7 @@ pub enum Language {
JavaScript,
Json,
Jsx,
Kotlin,
Lua,
Nix,
OCaml,
@ -210,6 +211,7 @@ pub fn from_extension(extension: &OsStr) -> Option<Language> {
"cjs" | "js" | "mjs" => Some(JavaScript),
"jsx" => Some(Jsx),
"json" => Some(Json),
"kt" | "ktm" | "kts" => Some(Kotlin),
"lua" => Some(Lua),
"nix" => Some(Nix),
"ml" => Some(OCaml),

@ -61,8 +61,8 @@ fn prefer_outer_delimiter(language: guess_language::Language) -> bool {
// languages have syntax like `foo(bar)` or `foo[bar]` where
// the inner delimiter is more relevant.
Bash | C | CPlusPlus | CSharp | Css | Dart | Elixir | Elm | Gleam | Go | Haskell | Java
| JavaScript | Jsx | Lua | Nix | OCaml | OCamlInterface | Php | Python | Ruby | Rust
| Scala | Tsx | TypeScript | Yaml | Zig => false,
| JavaScript | Jsx | Kotlin | Lua | Nix | OCaml | OCamlInterface | Php | Python | Ruby
| Rust | Scala | Tsx | TypeScript | Yaml | Zig => false,
}
}

@ -61,6 +61,7 @@ extern "C" {
fn tree_sitter_java() -> ts::Language;
fn tree_sitter_javascript() -> ts::Language;
fn tree_sitter_json() -> ts::Language;
fn tree_sitter_kotlin() -> ts::Language;
fn tree_sitter_lua() -> ts::Language;
fn tree_sitter_nix() -> ts::Language;
fn tree_sitter_ocaml() -> ts::Language;
@ -379,6 +380,20 @@ pub fn from_language(language: guess::Language) -> TreeSitterConfig {
.unwrap(),
}
}
Kotlin => {
let language = unsafe { tree_sitter_kotlin() };
TreeSitterConfig {
name: "Kotlin",
language,
atom_nodes: vec!["line_string_literal", "character_literal"]
.into_iter()
.collect(),
delimiter_tokens: vec![("(", ")"), ("{", "}"), ("[", "]"), ("<", ">")]
.into_iter()
.collect(),
highlight_query: ts::Query::new(language, "").unwrap(),
}
}
Lua => {
let language = unsafe { tree_sitter_lua() };
TreeSitterConfig {

@ -0,0 +1 @@
tree-sitter-kotlin/src