diff --git a/doc/indentation_logic/.gitignore b/doc/indentation_logic/.gitignore new file mode 100644 index 0000000..63fe867 --- /dev/null +++ b/doc/indentation_logic/.gitignore @@ -0,0 +1 @@ +/pages diff --git a/doc/indentation_logic/Makefile b/doc/indentation_logic/Makefile new file mode 100644 index 0000000..5a53c6f --- /dev/null +++ b/doc/indentation_logic/Makefile @@ -0,0 +1,11 @@ +pages.pdf: pages/page_000.pdf + pdfunite pages/page_*.pdf pages.pdf + +pages/page_000.pdf: pages/page_000.svg + for i in pages/page_*.svg ; do inkscape --export-pdf=pages/"$$( basename "$${i}" .svg )".pdf "$${i}" ; done + +pages/page_000.svg: src/pages.svg src/split_pages.kts + kotlinc-jvm -script src/split_pages.kts src/pages.svg + +clean: + rm -rf pages.pdf pages diff --git a/doc/indentation_logic/pages.pdf b/doc/indentation_logic/pages.pdf new file mode 100644 index 0000000..b0d2fde Binary files /dev/null and b/doc/indentation_logic/pages.pdf differ diff --git a/doc/indentation_logic/src/pages.svg b/doc/indentation_logic/src/pages.svg new file mode 100644 index 0000000..13b414a --- /dev/null +++ b/doc/indentation_logic/src/pages.svg @@ -0,0 +1,3322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + Indentation logic of kotlin-mode + 2019-12-01 + + + Basic idea (1/2) + Programs consist of... + List of statements/expressions ,sorrounded by (curly/round) brackets ,preceded by some texts + + for (x in xs) { print(x); print(x); print(x);} + + + 1 + + + + 2 + + + + 3 + + + + + + 2 + + + + + return foo( a + b, b + c, c + d) + + + 1 + + + + + 2 + + + + 2 + + + + 3 + + + + + + + return foo( a + b, b + c, ccc + ddd) + Basic idea (2/2) + We have four cases to indent + + Case 1. after an element delimiter, such as semicolon or commaCase 2. after an open bracketCase 3. before close bracketCase 4. other case, that is to say, inside a list element + + + 1 + + + + 2 + + + + 3 + + + + 4 + + + + + + + + Case 1: after an element delimiter + + + return foo( a, b, c, d, e, f) + + + Align with the preceding element at the start of a line.To seek the preceding element at the start of a line,we seek an element delimiter (comma or semicolon) at the end of a line or open bracket before the element.Then the next token is the token we align to.Elements may start with tokens of various types,but it ends with tokens of handful types, so seeking it is easier. + + ←Seek this + then align with the next token→ + We call this token “parent”. + + + + + + bar();return foo( a, b, c, d, e, f) + + Case 2. after an open bracket + + + + + Align with the start of the “preceding text” with offset.The procedure of seeking the start of the preceding text is same as the case 1. Seek a parent token, then the next token is the token align to. + + ←Seek this + then align with the next token→ + with offset + + + + bar();return foo( a, b, c, d, e, f) + Case 3. before close bracket + Align with the start of the preceding text of the open bracketwithout offset.To find the open bracket, we can use the ‘backward-list’. + + + ←then seek this + then align with the next token→ + Seek this + + + + + Case 4. other case, inside a list element + + + foo();val x = 1 + 2 + 3 + + + + + + + + foo();val x = 1 + 2 + 3 + + + If the line is the second line,align with the start ofthe element with offset. + If the line is the third orfollowing lines,align with the the previous line. + + + if-else statement + + aaa();if (foo) bar()else baz(); + + + + + + aaa();if (foo) bar()else baz(); + + + aaa();if (foo) bar()else baz(); + + + + + If the point is after if (...), then align with the if token with offset.If the point is before else token, then align with the matching if token without offset.If the point is after else token, then align with the else token with offset.Note that if-else can be nested, so when seeking the matching if token, we have to count number of else and if tokens. + We have similar rules for for, while, and do-while. + + + Advanced topics + + + Implicit semicolons + In Kotlin and other languages, a statement may end with a newline.We use a heuristic function ‘kotlin-mode--implicit-semi-pto detect it.It examines tokens before and after the newline.Example: + for (x in xs) { aaa() if (bbb) if (ccc) ddd() else eee() fff()}ccc() + ← No implicit semicolon here... + ← Implicit semicolon here + ← ... to aligh this line to the first if token rather than the else token. + ← ... only here ... + ← Implicit semicolon here + ← Implicit semicolon here + + + Ambiguous commas, colons, curly brackets, and objects + Commas are not always contained by brackets.Texts before brackets may contain another brackets.We handle them carefully. + class C: A by object: A1, A2 { fun aaa() {} }, B by object: B1, B2 { fun bbb() {} } { fun ccc(x: X): Int { return when (x) { object: X1 by object: XX1 { fun xxx1() {} }, X2 { fun xxx2() {} }, object: Y1, Y2 { fun yyy() {} } -> 1 else -> 2 } }} + When seeking the previous element of this line, + if we got a pair of curly brackets, + then jump to the object tokenand resume seeking, + + + + to skip this comma. + + + + Ambiguous arrows + val f = { g: (Int) -> (Int) -> Int -> g(1, 2)}when (x) { 1 -> f1 as (Int) -> Int f2 as (Int) -> Int -> f3 is (Int) -> Int -> f4} + ← arrow for function type + ← arrow for function type + ← arrow for lambda parameters + ← arrow for when-entry + ← arrow for function type + ← arrow for function type + ← arrow for when-entry + ← arrow for function type + ← arrow for when-entry + Arrows have many meanings and indentation rules.We use heuristics for this, but it is not precise. + Cannot handle those cases for now.We assume all arrows inside a when-expression are parts of when-entries. + + + + + + Angle brackets <> + Token ‘<’ and ‘>’ may be used as inequality operators or angle brackets for type parameters.We use heuristics to distinguish them:- Angle bracket must be balanced.- Angle bracket cannot contain some kind of tokens. + + + Ambiguous operators + We cannot handle those cases for now. + var shl = 1val x = shl shl shl // The last “shl” is a variable named “shl”.shl < 100 && foo() // This is not a continuation of the previous line.var shl = 1val x = shl shl // The last “shl” is a shift operator. shl < 100 && foo() // This is a continuation of the previous line.var shl = 1val x = shl shl shl ++ // postfix increment operatorshl < 100 && foo() // This is not a continuation of the previous line.var shl = 1val x = shl shl ++ // prefix increment operator shl < 100 && foo() // This is a continuation of the previous line.val x = foo()!! // postfix operatorfoo() // This is not a continuation of the previous line.val x = !! // two prefix operators foo() // This is a continuation of the previous line. + + + Implementation + kotlin-mode--indent-line ← entry point for indenting line kotlin-mode--calculate-indent ← calculate the amount of the indentation kotlin-mode--calculate-indent-of-multiline-comment ← when the point is inside a multiline comment kotlin-mode--calculate-indent-of-multiline-string ← when the point is inside a multiline string kotlin-mode--calculate-indent-of-single-line-comment ← when the point is before a single-line comment kotlin-mode--calculate-indent-of-code ← other case, including before a single-line string kotlin-mode--forward-token ← lexer kotlin-mode--forward-token-simple ← lexer without unbounded recursion kotlin-mode--implicit-semi-p ← determinate implicit semicolon ... kotlin-mode--backward-token ← lexer kotlin-mode--backward-token-simple ← lexer without unbounded recursion kotlin-mode--implicit-semi-p ... kotlin-mode--calculate-indent-after-open-curly-brace ← when the point is after ‘{’ kotlin-mode--curly-brace-type ← determinate the type of the block kotlin-mode--find-parent-and-align-with-next kotlin-mode--backward-sexps-until kotlin-mode--backward-token-or-list kotlin-mode--backward-token kotlin-mode--forward-token-or-list kotlin-mode--forward-token kotlin-mode--calculate-indent-after-comma ... kotlin-mode--calculate-indent-after-semicolon ... kotlin-mode--calculate-indent-of-expression ... kotlin-mode--find-parent-and-align-with-next ... ... + Overview of functions for indentation. Details are omitted. + + + Data types + kotlin-mode--token Lexical tokens. Consists of the type, the text, and the location (start and end) of the token.kotlin-mode--indentation Location of anchor point paired with offset. + Other notable functions + kotlin-mode--indent-new-comment-line Replacement for indent-new-comment-line. Break a line, indent it, and tweak comment delimiters.kotlin-mode--post-self-insert Do electric indentation. + + diff --git a/doc/indentation_logic/src/split_pages.kts b/doc/indentation_logic/src/split_pages.kts new file mode 100644 index 0000000..a2f1334 --- /dev/null +++ b/doc/indentation_logic/src/split_pages.kts @@ -0,0 +1,56 @@ +/** + * Write each layer of Inkscape SVG file into separate SVG files. + */ +import java.io.File +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult +import org.w3c.dom.Element + +val INKSCAPE_NS = "http://www.inkscape.org/namespaces/inkscape" + +if (args.size < 1) { + System.err.println("Usage: kotlinc-jvm -script split_pages.kts pages.svg") + System.exit(-1) +} + +val transformer = TransformerFactory.newInstance().newTransformer() + +val inputFile = File(args[0]) +val document = DocumentBuilderFactory + .newDefaultInstance() + .also { it.setNamespaceAware(true) } + .newDocumentBuilder() + .parse(inputFile) + +val svgElement = document.documentElement +val childNodes = svgElement.childNodes +val layers = 0.until(childNodes.length) + .map { childNodes.item(it) } + .filter { child -> + child is Element && + child.localName == "g" && + child.getAttributeNS(INKSCAPE_NS, "groupmode") == "layer" + } + +for (layer in layers) { + svgElement.removeChild(layer) +} + +val outputDirectory = File("pages") + +outputDirectory.mkdirs() + +for ((index, layer) in layers.withIndex()) { + svgElement.appendChild(layer) + + // Assuming `style="display:none"` + layer.attributes.removeNamedItem("style") + + val outputFile = File(outputDirectory, "page_%03d.svg".format(index)) + + transformer.transform(DOMSource(document), StreamResult(outputFile)) + + svgElement.removeChild(layer) +} diff --git a/kotlin-mode-indent.el b/kotlin-mode-indent.el new file mode 100644 index 0000000..0ca7802 --- /dev/null +++ b/kotlin-mode-indent.el @@ -0,0 +1,1955 @@ +;;; kotlin-mode-indent.el --- Major mode for kotlin, indentation -*- lexical-binding: t; -*- + +;; Copyright (C) 2019 taku0 + +;; Authors: taku0 (http://github.com/taku0) +;; Keywords: languages +;; Package-Requires: ((emacs "24.3")) +;; Version: 0.0.1 +;; URL: https://github.com/Emacs-Kotlin-Mode-Maintainers/kotlin-mode + +;; This file is not part of GNU Emacs. + +;; This program 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. + +;; This program 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 this program. If not, see . + +;;; Commentary: + +;; Routines for indentation + +;;; Code: + +(require 'rx) +(require 'cl-lib) + +(require 'kotlin-mode-lexer) + +;;; Customizations + +(defcustom kotlin-mode--basic-offset 4 + "Amount of indentation for block contents. + +Example: + +class Foo { + func foo() {} // offset of this line +}" + :type 'integer + :group 'kotlin + :safe 'integerp) + +(defcustom kotlin-mode--parenthesized-expression-offset 4 + "Amount of indentation inside parentheses and square brackets. + +Example: + +foo( + 1 // offset of this line +)" + :type 'integer + :group 'kotlin + :safe 'integerp) + +(defcustom kotlin-mode--multiline-statement-offset 4 + "Amount of indentation for continuations of expressions. + +Example: + +val x = 1 + + 2 // offset of this line" + :type 'integer + :group 'kotlin + :safe 'integerp) + +(defcustom kotlin-mode--prepend-asterisk-to-comment-line t + "Automatically insert a asterisk to each comment line if non-nil. + +Example: if the enter key is pressed when the point is after A below, + +/* + * A + */ + +an asterisk is inserted to the newline: + +/* + * A + * + */" + :type 'boolean + :group 'kotlin + :safe 'booleanp) + +(defcustom kotlin-mode--insert-space-after-asterisk-in-comment t + "Automatically insert a space after asterisk in comment if non-nil. + +Example: if an asterisk is inserted before A below, + +/* +A + */ + +a space is inserted after asterisk: + +/* + * A + */" + :type 'boolean + :group 'kotlin + :safe 'booleanp) + +(defcustom kotlin-mode--auto-close-multiline-comment t + "If non-nil, `indent-new-comment-line' automatically close multiline comment. + +Example: when the enter key is pressed after unclosed comment below, + +/** + +a closing delimiter is inserted automatically: + +/** + * // cursor is here + */" + :type 'boolean + :group 'kotlin + :safe 'booleanp) + +(defcustom kotlin-mode--fix-comment-close t + "Fix \"* /\" in incomplete multiline comment to \"*/\" if non-nil. + +Example: + +/* + * + * // when a slash is inserted here + +/* + * + */ // it become like this + + +/* + * + * / // rather than like this." + :type 'boolean + :group 'kotlin + :safe 'booleanp) + +(defcustom kotlin-mode--break-line-before-comment-close t + "If non-nil, break line before the closing delimiter of multiline comments. + +Example: if line break is inserted before A below, + +/** A */ + +it becomes like this: + +/** + * A + */ + +rather than like this: + +/** + * A */" + :type 'boolean + :group 'kotlin + :safe 'booleanp) + +(defcustom kotlin-mode--indent-nonempty-line-in-multiline-string nil + "If non-nil, indent nonempty line in multiline string. + +`indent-according-to-mode' is no-op otherwise." + :type 'boolean + :group 'kotlin + :safe 'booleanp) + +(defcustom kotlin-mode--highlight-anchor nil + "Highlight anchor point for indentation if non-nil. + +Intended for debugging." + :type 'boolean + :group 'kotlin + :safe 'booleanp) + +;;; Constants and variables + +(defconst kotlin-mode--statement-parent-tokens + '(\( \[ { anonymous-function-parameter-arrow when-expression-arrow + bare-else \(\)-before-control-structure-body \; implicit-\;) + "Parent tokens for statements. + +Parent tokens are tokens before the beginning of statements.") + +(defconst kotlin-mode--expression-parent-tokens + (append kotlin-mode--statement-parent-tokens + '(\, "where" string-chunk-before-template-expression)) + "Parent tokens for expressions. + +Parent tokens are tokens before the beginning of expressions.") + +(defvar-local kotlin-mode--anchor-overlay nil) +(defvar-local kotlin-mode--anchor-overlay-timer nil) + +;;; Indentation struct + +(defclass kotlin-mode--indentation () + ((position :initarg :position + :type number + :accessor kotlin-mode--indentation-position + :documentation "the position of the anchor point, such as +the start of the previous line or the start of the class declaration.") + (offset :initarg :offset + :type number + :initform 0 + :accessor kotlin-mode--indentation-offset + :documentation "the offset from the anchor point. For +example, when indenting the first line of a class body, its anchor +point is the start of the class declaration and its offset is +`kotlin-mode--basic-offset'.")) + "Indentation.") + +;;; Indentation logic + +(defun kotlin-mode--indent-line () + "Indent the current line." + (let* ((indentation (save-excursion (kotlin-mode--calculate-indent))) + (indentation-column + (save-excursion + (goto-char (kotlin-mode--indentation-position indentation)) + (+ (current-column) (kotlin-mode--indentation-offset indentation)))) + (current-indent + (save-excursion (back-to-indentation) (current-column)))) + (if (<= (current-column) current-indent) + ;; The point is on the left margin. + ;; Move the point to the new leftmost non-comment char. + (indent-line-to indentation-column) + ;; Keep current relative position from leftmost non-comment char. + (save-excursion (indent-line-to indentation-column))) + (when kotlin-mode--highlight-anchor + (kotlin-mode--highlight-anchor indentation)))) + +(defun kotlin-mode--calculate-indent () + "Return the indentation of the current line." + (back-to-indentation) + + (let ((parser-state (syntax-ppss))) + (cond + ((nth 4 parser-state) + ;; If the 4th element of `(syntax-ppss)' is non-nil, the point is on + ;; the 2nd or following lines of a multiline comment, because: + ;; + ;; - The 4th element of `(syntax-ppss)' is nil on the comment starter. + ;; - We have called `back-to-indentation`. + (kotlin-mode--calculate-indent-of-multiline-comment)) + + ((eq (nth 3 parser-state) t) + (kotlin-mode--calculate-indent-of-multiline-string)) + + ((looking-at "//") + (kotlin-mode--calculate-indent-of-single-line-comment)) + + (t + (kotlin-mode--calculate-indent-of-code))))) + +(defun kotlin-mode--calculate-indent-of-multiline-comment () + "Return the indentation of the current line inside a multiline comment." + (back-to-indentation) + (let ((comment-beginning-position (nth 8 (syntax-ppss))) + (starts-with-asterisk (eq (char-after) ?*))) + (forward-line -1) + (back-to-indentation) + (cond + ;; 2nd line of the comment + ((<= (point) comment-beginning-position) + ;; The point was on the 2nd line of the comment. + (goto-char comment-beginning-position) + (forward-char) + ;; If there are extra characters or spaces after asterisks, align with + ;; the first non-space character or end of line. Otherwise, align with + ;; the first asterisk. + (when (and + (looking-at + (rx (seq (zero-or-more "*") (one-or-more (not (any "\n*")))))) + (not (and kotlin-mode--prepend-asterisk-to-comment-line + starts-with-asterisk))) + (skip-chars-forward "*") + (skip-syntax-forward " ")) + (make-instance 'kotlin-mode--indentation :position (point) :offset 0)) + + ;; The point was on the 3rd or following lines of the comment. + + ;; Before closing delimiter + ((= (save-excursion + ;; Back to the original line. + (forward-line) + (back-to-indentation) + (point)) + (save-excursion + ;; Goto just before the closing delimiter. + (goto-char comment-beginning-position) + (if (forward-comment 1) + (progn + ;; slash + (backward-char) + (skip-chars-backward "*") + (point)) + -1))) + ;; Before the closing delimiter. Align with the first asterisk of the + ;; opening delimiter. + ;; + ;; TODO: If there are multiple asterisks on the closing + ;; delimiter, and the middle lines have no leading asterisks, + ;; align with the slash of the opening delimiter + ;; + ;; Example: + ;; + ;; /********* + ;; aaa + ;; **********/ + (goto-char comment-beginning-position) + (forward-char) + (make-instance 'kotlin-mode--indentation :position (point) :offset 0)) + + ;; Otherwise, align with a non-empty preceding line. + + ;; The previous line is empty + ((and (bolp) (eolp)) + ;; Seek a non-empty-line. + (while (and (bolp) (eolp) (not (bobp))) + (forward-line -1)) + (forward-line) + (kotlin-mode--calculate-indent-of-multiline-comment)) + + ;; The previous line is not empty + (t + ;; Align to this line. + (make-instance 'kotlin-mode--indentation :position (point) :offset 0))))) + +(defun kotlin-mode--calculate-indent-of-multiline-string () + "Return the indentation of the current line inside a multiline string." + (back-to-indentation) + (let ((string-beginning-position + (save-excursion (kotlin-mode--beginning-of-string)))) + (if (looking-at "\"\"\"") + ;; Indenting the closing delimiter. Align with the start of containing + ;; expression with extra indentation. + ;; + ;; Example: + ;; + ;; val someString = """ + ;; Hello. + ;; """ + ;; + ;; When aligning to opening delimiter, align without offset: + ;; foo(""" + ;; aaa + ;; """) + ;; + ;; Possible alternative indentation: + ;; val someString = """ + ;; Hello. + ;; """ + (progn + (goto-char string-beginning-position) + (let ((indentation + (kotlin-mode--calculate-indent-of-expression + kotlin-mode--multiline-statement-offset))) + (if (= (kotlin-mode--indentation-position indentation) + string-beginning-position) + (make-instance 'kotlin-mode--indentation + :position string-beginning-position + :offset 0) + indentation))) + ;; Other than the closing delimiter. + (if (and (not (eolp)) + (not kotlin-mode--indent-nonempty-line-in-multiline-string)) + ;; The user prefers to keep indentations inside multiline string. + (make-instance 'kotlin-mode--indentation :position (point) :offset 0) + ;; The user prefers to indent lines inside multiline string. + (beginning-of-line) + (backward-char) + (kotlin-mode--goto-non-template-expression-bol) + (back-to-indentation) + (cond + ;; 2nd line of string + ((<= (point) string-beginning-position) + ;; The point was on the 2nd line of the string. Align + ;; with the start of containing expression with extra + ;; indentation. + (goto-char string-beginning-position) + (kotlin-mode--calculate-indent-of-expression + kotlin-mode--multiline-statement-offset)) + + ;; The point was on the 3rd or following lines of the string. + ;; Align with a non-empty preceding line. + + ;; The previous line is empty + ((and (bolp) (eolp)) + ;; Seek a non-empty-line. + (while (and (bolp) (eolp) (not (bobp))) + (forward-line -1)) + (forward-line) + (kotlin-mode--calculate-indent-of-multiline-string)) + + ;; The previous line is not empty + (t + ;; Align to this line. + (make-instance 'kotlin-mode--indentation + :position (point) + :offset 0))))))) + +(defun kotlin-mode--goto-non-template-expression-bol () + "Back to the beginning of line that is not inside a template expression." + (let ((chunk-beginning-position (nth 8 (syntax-ppss))) + (matching-bracket t)) + (while (and matching-bracket + (< (line-beginning-position) chunk-beginning-position)) + (setq matching-bracket + (get-text-property + chunk-beginning-position 'kotlin-property--matching-bracket)) + (when matching-bracket + (goto-char matching-bracket) + (setq chunk-beginning-position (nth 8 (syntax-ppss))))) + (beginning-of-line))) + +(defun kotlin-mode--calculate-indent-of-single-line-comment () + "Return the indentation of the current line inside a single-line comment." + (cond + ;; When the comment is beginning of the buffer, indent to the column 0. + ((save-excursion + (beginning-of-line) + (bobp)) + (make-instance 'kotlin-mode--indentation :position (point-min) :offset 0)) + + ;; If the previous line is also a single-line comment, align with it. + ((save-excursion + (forward-line -1) + (skip-syntax-forward " ") + (looking-at "//")) + (forward-line -1) + (skip-syntax-forward " ") + (make-instance 'kotlin-mode--indentation :position (point) :offset 0)) + + ;; Otherwise, indent like a expression. + (t + (kotlin-mode--calculate-indent-of-code)))) + +(defun kotlin-mode--calculate-indent-of-code () + "Return the indentation of the current line outside comments or string." + (back-to-indentation) + (let* ((previous-token (save-excursion (kotlin-mode--backward-token))) + (previous-type (kotlin-mode--token-type previous-token)) + (previous-text (kotlin-mode--token-text previous-token)) + (next-token (save-excursion (kotlin-mode--forward-token))) + (next-type (kotlin-mode--token-type next-token)) + (next-text (kotlin-mode--token-text next-token)) + (next-is-on-current-line + (<= (kotlin-mode--token-start next-token) (line-end-position)))) + (cond + ;; Beginning of the buffer + ((eq previous-type 'outside-of-buffer) + (make-instance 'kotlin-mode--indentation :position (point-min) :offset 0)) + + ;; Before } on the current line. + ;; Align with the head of the statement. + ((and next-is-on-current-line (eq next-type '})) + (goto-char (kotlin-mode--token-end next-token)) + (backward-list) + (kotlin-mode--calculate-indent-after-open-curly-brace 0)) + + ;; Before ) or ] on the current line. + ;; Align with the head of the expression. + ((and next-is-on-current-line + (memq next-type '(\) \)-before-control-structure-body \]))) + (goto-char (kotlin-mode--token-end next-token)) + (backward-list) + (kotlin-mode--calculate-indent-of-expression 0)) + + ;; Before close angle bracket for generics (>) on the current line. + ((and next-is-on-current-line + (equal next-text ">") + (save-excursion + (goto-char (kotlin-mode--token-end next-token)) + (eq (kotlin-mode--token-type (kotlin-mode--backward-token-or-list)) + '<>))) + (goto-char (kotlin-mode--token-end next-token)) + (kotlin-mode--backward-token-or-list) + (kotlin-mode--calculate-indent-of-expression 0)) + + ;; Before end of a template expression on the current line. + ((and next-is-on-current-line + (eq next-type 'string-chunk-after-template-expression)) + (goto-char (get-text-property + (kotlin-mode--token-start next-token) + 'kotlin-property--matching-bracket)) + ;; Skip "${" + (forward-char 2) + (kotlin-mode--backward-string-chunk) + (kotlin-mode--calculate-indent-after-beginning-of-template-expression + 0)) + + ;; Before { on the current line + ((and next-is-on-current-line (eq next-type '{)) + (kotlin-mode--calculate-indent-after-open-curly-brace 0)) + + ;; Before "else" on the current line + ((and next-is-on-current-line (equal next-text "else")) + (kotlin-mode--calculate-indent-of-else)) + + ;; Before "where" on the current line + ((and next-is-on-current-line (equal next-text "where")) + (kotlin-mode--find-parent-and-align-with-next + kotlin-mode--statement-parent-tokens + kotlin-mode--multiline-statement-offset)) + + ;; Before "catch" or "finally" on the current line + ((and next-is-on-current-line (member next-text '("catch" "finally"))) + (kotlin-mode--find-parent-and-align-with-next + kotlin-mode--statement-parent-tokens + 0 + '() + '("catch" "finally") + 0)) + + ;; Before "=" on the current line + ;; + ;; fun foo(): T + ;; where + ;; T: A, + ;; T: B + ;; = bar() + ((and next-is-on-current-line (equal next-text "=")) + (kotlin-mode--find-parent-and-align-with-next + (remove '\, (remove "where" kotlin-mode--expression-parent-tokens)) + kotlin-mode--multiline-statement-offset)) + + ;; Before "," on the current line + ((and next-is-on-current-line (eq next-type '\,)) + (kotlin-mode--calculate-indent-of-prefix-comma)) + + ;; After "," + ((eq previous-type '\,) + (goto-char (kotlin-mode--token-start previous-token)) + (kotlin-mode--calculate-indent-after-comma)) + + ;; After "catch". Align with "try". + ((equal previous-text "catch") + (kotlin-mode--find-parent-and-align-with-next + kotlin-mode--statement-parent-tokens + kotlin-mode--multiline-statement-offset)) + + ;; After { + ((eq previous-type '{) + (goto-char (kotlin-mode--token-start previous-token)) + (kotlin-mode--calculate-indent-after-open-curly-brace + kotlin-mode--basic-offset)) + + ;; After ( or [ + ((memq previous-type '(\( \[)) + (goto-char (kotlin-mode--token-start previous-token)) + (kotlin-mode--calculate-indent-of-expression + kotlin-mode--parenthesized-expression-offset + kotlin-mode--parenthesized-expression-offset)) + + ;; After open angle bracket for generics (<) + ((and (equal previous-text "<") + (save-excursion + (goto-char (kotlin-mode--token-start previous-token)) + (eq (kotlin-mode--token-type (kotlin-mode--forward-token-or-list)) + '<>))) + (goto-char (kotlin-mode--token-start previous-token)) + (kotlin-mode--calculate-indent-of-expression + kotlin-mode--parenthesized-expression-offset + kotlin-mode--parenthesized-expression-offset)) + + ;; After beginning of a template expression + ((eq previous-type 'string-chunk-before-template-expression) + (goto-char (kotlin-mode--token-start previous-token)) + (kotlin-mode--calculate-indent-after-beginning-of-template-expression + kotlin-mode--parenthesized-expression-offset)) + + ;; After ; or implicit-\; + ((memq previous-type '(\; implicit-\;)) + (goto-char (kotlin-mode--token-start previous-token)) + (kotlin-mode--calculate-indent-after-semicolon)) + + ;; After "->" of lambda parameters + ((eq previous-type 'anonymous-function-parameter-arrow) + (goto-char (cdr (kotlin-mode--find-containing-brackets + (kotlin-mode--token-start previous-token)))) + (kotlin-mode--calculate-indent-after-open-curly-brace + kotlin-mode--basic-offset)) + + ;; Before "->" of lambda parameters on the current line + ((and next-is-on-current-line + (eq next-type 'anonymous-function-parameter-arrow)) + (if (eq (kotlin-mode--token-type (kotlin-mode--backward-token-or-list)) + '{) + (kotlin-mode--calculate-indent-after-open-curly-brace + kotlin-mode--basic-offset) + (goto-char (cdr (kotlin-mode--find-containing-brackets + (kotlin-mode--token-start next-token)))) + (kotlin-mode--align-with-next-token (kotlin-mode--forward-token)))) + + ;; After "->" of when expression + ((eq previous-type 'when-expression-arrow) + (goto-char (kotlin-mode--token-start previous-token)) + (kotlin-mode--find-parent-and-align-with-next + (cl-remove-if + (lambda (e) + (memq e '(when-expression-arrow + bare-else \(\)-before-control-structure-body))) + kotlin-mode--statement-parent-tokens) + kotlin-mode--basic-offset)) + + ;; Before "->" of when expression on the current line + ((and next-is-on-current-line (eq next-type 'when-expression-arrow)) + (if (equal (kotlin-mode--token-text previous-token) "else") + (kotlin-mode--align-with-token + previous-token + kotlin-mode--basic-offset) + (kotlin-mode--find-parent-and-align-with-next + (cl-remove-if + (lambda (e) + (memq e '(when-expression-arrow + bare-else \(\)-before-control-structure-body))) + kotlin-mode--statement-parent-tokens) + kotlin-mode--basic-offset))) + + ;; After "where" + ;; + ;; class Foo + ;; where + ;; T: A // align with "where" with offset + ;; + ;; class Foo where + ;; T: A // align with "class" with offset + ((equal previous-text "where") + (goto-char (kotlin-mode--token-start previous-token)) + (if (kotlin-mode--bol-other-than-comments-p) + (kotlin-mode--align-with-token + previous-token + kotlin-mode--multiline-statement-offset) + (kotlin-mode--find-parent-and-align-with-next + kotlin-mode--statement-parent-tokens + kotlin-mode--multiline-statement-offset))) + + ;; After if, when, for, or while + ((member previous-text '("if" "when" "for" "while")) + (kotlin-mode--find-parent-and-align-with-next + kotlin-mode--statement-parent-tokens + kotlin-mode--multiline-statement-offset)) + + ;; After do + ((equal previous-text "do") + (kotlin-mode--align-with-token + previous-token + (if (equal next-text "while") 0 kotlin-mode--basic-offset))) + + ;; After else + ((equal previous-text "else") + (kotlin-mode--align-with-token + previous-token + kotlin-mode--basic-offset)) + + ;; After "if ()", "while ()", or "for ()" + ((eq previous-type '\)-before-control-structure-body) + (kotlin-mode--backward-token-or-list) + (kotlin-mode--find-parent-and-align-with-next + kotlin-mode--statement-parent-tokens + kotlin-mode--basic-offset + '() + '("else") + kotlin-mode--basic-offset)) + + ;; Before ; on the current line + ((and next-is-on-current-line (eq next-type '\;)) + (kotlin-mode--find-parent-and-align-with-next + (remove '\; (remove 'implicit-\; kotlin-mode--statement-parent-tokens)) + 0 + '(\; implicit-\;))) + + ;; Before "in" on the current line + ((and next-is-on-current-line (equal next-text "in")) + (kotlin-mode--calculate-indent-before-in)) + + ;; After "in" + ;; + ;; Examples: + ;; for ( + ;; x + ;; in + ;; xs + ;; ) {} + ;; + ;; for (x + ;; in + ;; xs) {[]} + ;; + ;; when (x) { + ;; in + ;; xs -> y in + ;; ys + ;; in + ;; zs -> 1 + ;; } + ;; + ;; Foo< + ;; in + ;; X, + ;; @AAA in + ;; X + ;; > + ;; + ;; val x = 1 in + ;; array + ((equal previous-text "in") + (let* ((type-and-parent + (save-excursion + (goto-char (kotlin-mode--token-start previous-token)) + (kotlin-mode--find-parent-of-in))) + (type (car type-and-parent))) + (if (or (memq type '(for <>)) + (save-excursion + (goto-char (kotlin-mode--token-start previous-token)) + (not (kotlin-mode--bol-other-than-comments-p)))) + ;; for-statement and type parameters, or "in" is not at + ;; the beginning of the line. + (progn + ;; Indent like a expression + (goto-char (kotlin-mode--token-start previous-token)) + (kotlin-mode--calculate-indent-of-expression + kotlin-mode--multiline-statement-offset)) + ;; After "in" at the beginning of the line. + ;; + ;; Example: + ;; + ;; when (x) { + ;; in + ;; xs -> 1 + ;; in + ;; ys -> 1 + ;; } + ;; + ;; Pretend a semicolon exists before "in" + (goto-char (kotlin-mode--token-start previous-token)) + (kotlin-mode--align-with-next-token + (kotlin-mode--backward-token) + kotlin-mode--multiline-statement-offset)))) + + ;; Before "while" on the current line + ((and next-is-on-current-line (equal next-text "while")) + (let ((do-token (save-excursion (kotlin-mode--find-do-for-while)))) + (if do-token + (kotlin-mode--align-with-token do-token) + (kotlin-mode--calculate-indent-after-semicolon)))) + + ;; Inside annotation + ((or (and (eq previous-type 'annotation) + (or + ;; @file + ;; : // ← here + (eq next-type ':) + ;; @file:A + ;; .B // ← here + ;; @file:A + ;; // ← here + (member next-text '("." "<")))) + ;; @file: + ;; [ // ← here + (and (eq previous-type ':) + (eq next-type '\[) + (save-excursion + (goto-char (kotlin-mode--token-start previous-token)) + (eq (kotlin-mode--token-type (kotlin-mode--backward-token)) + 'annotation))) + ;; @file: + ;; A // ← here + ;; @file:A + ;; . + ;; B // ← here + ;; @file:A + ;; < + ;; B + ;; > // ← here + (and (or (eq next-type 'atom) (equal next-text ">")) + (eq (kotlin-mode--token-type + (save-excursion + (kotlin-mode--extend-annotation-token-backward + next-token))) + 'annotation))) + (let ((start-of-annotation + (cond + ((eq previous-type 'annotation) + (kotlin-mode--token-start previous-token)) + + ((eq previous-type ':) + (save-excursion + (goto-char (kotlin-mode--token-start previous-token)) + (kotlin-mode--token-start (kotlin-mode--backward-token)))) + + (t (kotlin-mode--token-start + (save-excursion (kotlin-mode--extend-annotation-token-backward + next-token)))))) + (start-of-previous-line + (save-excursion + (kotlin-mode--backward-token-or-list t) + (kotlin-mode--goto-non-comment-bol-with-same-nesting-level t) + (point)))) + (goto-char (max start-of-annotation start-of-previous-line)) + (kotlin-mode--align-with-current-line + (if (< start-of-annotation start-of-previous-line) + ;; 3rd or following lines of the annotation. + ;; Align with previous line without offset. + 0 + ;; 2nd line of the annotation. + ;; Align with the start of the annotation with offset. + kotlin-mode--multiline-statement-offset)))) + + ;; After annotations or labels + ((memq previous-type '(annotation label)) + (goto-char (kotlin-mode--token-start previous-token)) + ;; Align with preceding annotation or label at the beginning of + ;; a line, or indent like an expression if the annotations are in + ;; middle of a line. + (let ((token previous-token)) + (while (and (memq (kotlin-mode--token-type token) '(annotation label)) + (not (kotlin-mode--bol-other-than-comments-p))) + (setq token (kotlin-mode--backward-token))) + (unless (memq (kotlin-mode--token-type token) '(annotation label)) + ;; The annotations are in middle of a line. + (goto-char (kotlin-mode--token-end token)))) + (if (kotlin-mode--bol-other-than-comments-p) + ;; The annotations are at the beginning of a line. + (kotlin-mode--align-with-current-line) + ;; The annotations are in middle of a line. + (kotlin-mode--align-with-next-token + (kotlin-mode--find-parent-of-expression)))) + + ;; Otherwise, it is continuation of the previous line + (t + (goto-char (kotlin-mode--token-end previous-token)) + (kotlin-mode--backward-token-or-list) + (kotlin-mode--calculate-indent-of-expression + kotlin-mode--multiline-statement-offset))))) + +(defun kotlin-mode--find-parent-and-align-with-next + (parents + &optional + offset + stop-at-eol-token-types + stop-at-bol-token-types + bol-offset) + "Find the parent and return indentation based on it. + +A parent is, for example, the open bracket of the containing block or +semicolon of the preceding statement. + +PARENTS is a list of token types that precedes the expression or the statement. +OFFSET is the offset. If it is omitted, assumed to be 0. +See `kotlin-mode--backward-sexps-until' for the details of +STOP-AT-EOL-TOKEN-TYPES and STOP-AT-BOL-TOKEN-TYPES. +If scanning stops at STOP-AT-EOL-TOKEN-TYPES, align with the next token with +BOL-OFFSET. +If scanning stops at STOP-AT-BOL-TOKEN-TYPES, align with that token with +BOL-OFFSET. +If STOP-AT-BOL-TOKEN-TYPES or STOP-AT-BOL-TOKEN-TYPES is the symbol +`any', it matches all tokens. +The point is assumed to be on the previous line." + (save-excursion + (let* ((parent (kotlin-mode--backward-sexps-until + parents + stop-at-eol-token-types + stop-at-bol-token-types)) + (parent-end (kotlin-mode--token-end parent)) + (stopped-at-parent + (or (memq (kotlin-mode--token-type parent) parents) + (member (kotlin-mode--token-text parent) parents) + (eq (kotlin-mode--token-type parent) 'outside-of-buffer))) + (stopped-at-eol + (and + (not stopped-at-parent) + stop-at-eol-token-types + (or + (eq stop-at-eol-token-types 'any) + (memq (kotlin-mode--token-type parent) + stop-at-eol-token-types) + (member (kotlin-mode--token-text parent) + stop-at-eol-token-types))))) + (if stopped-at-parent + (kotlin-mode--align-with-next-token parent offset) + (when stopped-at-eol + (goto-char parent-end) + (forward-comment (point-max))) + (kotlin-mode--align-with-current-line bol-offset))))) + +(defun kotlin-mode--calculate-indent-of-expression + (&optional + offset + bol-offset) + "Return indentation of the current expression. + +the point is assumed to be on the previous line. + +OFFSET is the offset. If it is omitted, assumed to be 0. +If scanning stops at eol, align with the next token with BOL-OFFSET." + (save-excursion + (let* ((pos (point)) + (parent-of-previous-line + (save-excursion + (kotlin-mode--goto-non-comment-bol-with-same-nesting-level) + (kotlin-mode--backward-token))) + (parent-of-expression (kotlin-mode--find-parent-of-expression))) + ;; Skip annotations + (goto-char (kotlin-mode--token-end parent-of-expression)) + (kotlin-mode--forward-annotations) + (kotlin-mode--goto-non-comment-bol-with-same-nesting-level) + (when (or (< (point) (kotlin-mode--token-end parent-of-expression)) + (< pos (point))) + (goto-char (kotlin-mode--token-end parent-of-expression))) + (setq parent-of-expression (kotlin-mode--backward-token)) + (if (<= (kotlin-mode--token-start parent-of-previous-line) + (kotlin-mode--token-start parent-of-expression)) + ;; let x = + ;; 1 + // indenting here + ;; 2 + + ;; 3 + ;; + ;; Aligns with the parent of the expression with offset. + (kotlin-mode--align-with-next-token parent-of-expression offset) + ;; let x = + ;; 1 + + ;; 2 + // indenting here + ;; 3 // or here + ;; + ;; Aligns with the previous line. + (kotlin-mode--align-with-next-token parent-of-previous-line + bol-offset))))) + +(defun kotlin-mode--forward-annotations () + "Skip forward comments, whitespaces, labels, and annotations." + (let (token) + (while (progn + (forward-comment (point-max)) + (setq token (kotlin-mode--forward-token)) + (memq (kotlin-mode--token-type token) '(annotation label)))) + (goto-char (kotlin-mode--token-start token)))) + +(defun kotlin-mode--backward-annotations () + "Skip backward comments, whitespaces, labels, and annotations." + (let (token) + (while (progn + (forward-comment (- (point))) + (setq token (kotlin-mode--backward-token)) + (memq (kotlin-mode--token-type token) '(annotation label)))) + (goto-char (kotlin-mode--token-end token)))) + +(defun kotlin-mode--calculate-indent-after-open-curly-brace (offset) + "Return indentation after open curly braces. + +Assuming the point is on the open brace. +OFFSET is the offset of the contents. +This function is also used for close curly braces." + ;; If the statement is multiline expression, aligns with the start of + ;; the line on which the open brace is: + ;; + ;; foo() + ;; .then { x -> + ;; foo() + ;; foo() + ;; } + ;; .then { x -> + ;; foo() + ;; foo() + ;; } + ;; + ;; rather than + ;; + ;; foo() + ;; .then { x -> + ;; foo() + ;; foo() + ;; } + ;; .then { x -> + ;; foo() + ;; foo() + ;; } + ;; + ;; Otherwise, aligns with the start of the whole statement: + ;; + ;; class Foo: + ;; Bar { + ;; fun foo() {} + ;; } + ;; + ;; rather than + ;; + ;; class Foo: + ;; Bar { + ;; fun foo() {} + ;; } + ;; + ;; POSSIBLE IMPROVEMENT: those behavior should be configurable. + ;; + ;; FIXME: curly braces after binary operator is a part of a + ;; multiline expression: + ;; + ;; class Foo: + ;; Bar by xs someInfixOperator { x -> + ;; // this is not the body of the class. + ;; } { + ;; // The body of the class. + ;; } + (let* ((brace-type-and-keyword-token (kotlin-mode--curly-brace-type)) + (brace-type (car brace-type-and-keyword-token)) + (keyword-token (cdr brace-type-and-keyword-token))) + (cond + ((memq brace-type + '(object-literal + lambda-literal + if-expression + try-expression + catch-block + finally-block + when-expression)) + (goto-char (kotlin-mode--token-start keyword-token)) + (kotlin-mode--backward-annotations) + (unless (eq brace-type 'lambda-literal) + (forward-comment (point-max))) + (kotlin-mode--calculate-indent-of-expression offset offset)) + + ((eq brace-type 'else-block) + (goto-char (kotlin-mode--token-start keyword-token)) + (kotlin-mode--calculate-indent-of-else offset)) + + ((eq brace-type 'when-entry) + (goto-char (kotlin-mode--token-start keyword-token)) + (if (equal (kotlin-mode--token-text + (save-excursion (kotlin-mode--backward-token))) + "else") + (kotlin-mode--align-with-token (kotlin-mode--backward-token) offset) + (kotlin-mode--find-parent-and-align-with-next + (remove 'when-expression-arrow kotlin-mode--statement-parent-tokens) + offset))) + + ((memq brace-type '(getter setter)) + (goto-char (kotlin-mode--token-start keyword-token)) + (kotlin-mode--try-backward-modifiers) + (kotlin-mode--align-with-next-token (kotlin-mode--backward-token) offset)) + + (t + (goto-char (kotlin-mode--token-start keyword-token)) + (kotlin-mode--find-parent-and-align-with-next + kotlin-mode--statement-parent-tokens + offset))))) + +(defun kotlin-mode--curly-brace-type () + "Return information about curly brace. + +Return a cons (TYPE . KEYWORD-TOKEN) where TYPE is a symbol, and KEYWORD-TOKEN +is the keyword token: +- TYPE: class-declaration, KEYWORD-TOKEN: \"class\" +- TYPE: interface-declaration, KEYWORD-TOKEN: \"interface\" +- TYPE: object-declaration, KEYWORD-TOKEN: \"object\" +- TYPE: enum-entry, KEYWORD-TOKEN: identifier +- TYPE: object-literal, KEYWORD-TOKEN: \"object\" +- TYPE: anonymous-initializer, KEYWORD-TOKEN: \"init\" +- TYPE: function-declaration, KEYWORD-TOKEN: \"fun\" +- TYPE: getter, KEYWORD-TOKEN: \"get\" +- TYPE: setter, KEYWORD-TOKEN: \"set\" +- TYPE: secondary-constructor, KEYWORD-TOKEN: \"constructor\" +- TYPE: for-statement, KEYWORD-TOKEN: \"for\" +- TYPE: while-statement, KEYWORD-TOKEN: \"while\" +- TYPE: do-while-statement, KEYWORD-TOKEN: \"do\" +- TYPE: if-expression, KEYWORD-TOKEN: \"if\" +- TYPE: else-block, KEYWORD-TOKEN: \"else\" +- TYPE: when-entry, KEYWORD-TOKEN: \"->\" +- TYPE: try-expression, KEYWORD-TOKEN: \"try\" +- TYPE: catch-block, KEYWORD-TOKEN: \"catch\" +- TYPE: finally-block, KEYWORD-TOKEN: \"finally\" +- TYPE: lambda-literal, KEYWORD-TOKEN: \"{\" +- TYPE: when-expression, KEYWORD-TOKEN: \"when\" + +Assuming the point is just before the open curly brace." + ;; Other braces may appear between the keyword and the open brace: + ;; + ;; class Foo: Bar by if (aaa) {bbb} else {ccc} { + ;; } + ;; + ;; So we maintain stack of braces and match them with keywords. + (save-excursion + (let* ((pos (point)) + (open-brace-points (list pos)) + (containing-brackets (kotlin-mode--find-containing-brackets (point))) + previous-token + previous-type + previous-text + brace-type + result) + ;; Special handling for enumEntry. + (goto-char (cdr containing-brackets)) + (when (eq (car containing-brackets) '{) + (let ((token (kotlin-mode--backward-sexps-until + (append kotlin-mode--statement-parent-tokens + '("enum"))))) + (when (equal (kotlin-mode--token-text token) "enum") + (goto-char pos) + (when (eq (kotlin-mode--token-type + (kotlin-mode--backward-sexps-until + kotlin-mode--statement-parent-tokens)) + '{) + ;; This must be a enumEntry. + (goto-char pos) + (setq previous-token (kotlin-mode--backward-token-or-list)) + (when (eq (kotlin-mode--token-type previous-token) '\(\)) + (setq previous-token (kotlin-mode--backward-token-or-list))) + (setq result (cons 'enum-entry previous-token)))))) + (unless result + ;; Other cases + (goto-char pos) + (while open-brace-points + (setq previous-token (kotlin-mode--backward-token-or-list)) + (setq previous-type (kotlin-mode--token-type previous-token)) + (setq previous-text (kotlin-mode--token-text previous-token)) + + (cond + ;; whenEntry. + ;; Stop immediately. + ((eq previous-type 'when-expression-arrow) + (setq result (cons 'when-entry previous-token)) + (setq open-brace-points nil)) + + ;; Hit beginning of the statement without seeing keyword. + ;; Assume it is a lambda expression and stop the loop. + ((or (eq previous-type 'outside-of-buffer) + (memq previous-type kotlin-mode--statement-parent-tokens) + (member previous-text kotlin-mode--statement-parent-tokens)) + (setq result (cons 'lambda-literal + (progn + (goto-char pos) + (kotlin-mode--forward-token)))) + (setq open-brace-points nil)) + + ;; Found another braces. + ;; Push it to the stack and continue the loop. + ((eq previous-type '{}) + (push (point) open-brace-points)) + + ;; Simple cases. + ;; Those cannot be a part of an expression. + ;; Stop immediately. + ((member previous-text + '("class" "interface" "init" "fun" "get" "set" + "for" "while" "do")) + (setq brace-type (assoc-default + previous-text + '(("class" . class-declaration) + ("interface" . interface-declaration) + ("init" . anonymous-initializer) + ;; FIXME handle anonymous function + ("fun" . function-declaration) + ("get" . getter) + ("set" . setter) + ("for" . for-statement) + ("while" . while-statement) + ("do" . do-while-statement)))) + (setq result (cons brace-type previous-token)) + (setq open-brace-points nil)) + + ;; Semi-simple cases. + ;; Those can be a part of an expression and the braces are mandatory. + ((member previous-text '("try" "catch" "finally" "when")) + (pop open-brace-points) + (when (null open-brace-points) + (setq brace-type (assoc-default + previous-text + '(("try" . try-expression) + ("catch" . catch-block) + ("finally" . finally-block) + ("when" . when-expression)))) + (setq result (cons brace-type previous-token)))) + + ;; If and else. + ;; Those can be a part of an expression and the braces are optional. + ((member previous-text '("if" "else")) + (save-excursion + (goto-char (kotlin-mode--token-end previous-token)) + (when (equal previous-text "if") + (kotlin-mode--forward-token-or-list)) + + (forward-comment (point-max)) + (when (= (point) (car open-brace-points)) + (pop open-brace-points) + (when (null open-brace-points) + (setq brace-type + (if (equal previous-text "if") + 'if-expression + 'else-block)) + (setq result (cons brace-type previous-token)))))) + + ;; Object declaration or object literal. + ((equal previous-text "object") + (setq brace-type + (cond + ((save-excursion + (equal (kotlin-mode--token-text + (kotlin-mode--backward-token)) + "companion")) + 'object-declaration) + ((save-excursion + (goto-char (kotlin-mode--token-end previous-token)) + (eq (kotlin-mode--token-type (kotlin-mode--forward-token)) + 'atom)) + 'object-declaration) + (t 'object-literal))) + (pop open-brace-points) + (when (or (null open-brace-points) + (eq brace-type 'object-declaration)) + (setq result (cons brace-type previous-token)) + (setq open-brace-points nil))) + + ;; Primary constructor or secondary constructor. + ((equal previous-text "constructor") + (let ((parent-token + (kotlin-mode--backward-sexps-until + (append kotlin-mode--statement-parent-tokens '("class"))))) + (if (equal (kotlin-mode--token-text parent-token) "class") + ;; primary constructor + (setq result (cons 'class-declaration parent-token)) + ;; secondary constructor + (setq result (cons 'secondary-constructor previous-token))) + (setq open-brace-points nil)))))) + result))) + +(defun kotlin-mode--calculate-indent-of-else (&optional offset) + "Return indentation for \"else\" token with OFFSET." + ;; Align it with "if" token. + ;; Since if-else can be nested, we keep nesting level to find matching "if". + (let ((nesting-level 1) + previous-token) + (while (< 0 nesting-level) + (setq previous-token (kotlin-mode--backward-token-or-list)) + (cond + ((equal (kotlin-mode--token-text previous-token) "else") + (setq nesting-level (1+ nesting-level))) + + ((equal (kotlin-mode--token-text previous-token) "if") + (if (save-excursion + ;; "else if" on the same line + (and + (equal (kotlin-mode--token-text (kotlin-mode--backward-token)) + "else") + (= (progn (kotlin-mode--goto-non-comment-bol) + (point)) + (progn (goto-char (kotlin-mode--token-start previous-token)) + (kotlin-mode--goto-non-comment-bol) + (point))))) + ;; Skip "else-if" without changing nesting-level. + (kotlin-mode--backward-token) + (setq nesting-level (1- nesting-level)))) + + ((memq (kotlin-mode--token-type previous-token) + '({ \( outside-of-buffer)) + ;; Unmatched if-else + (setq nesting-level 0))))) + (kotlin-mode--forward-token) + (kotlin-mode--calculate-indent-of-expression (or offset 0) (or offset 0))) + +(defun kotlin-mode--calculate-indent-of-prefix-comma () + "Return indentation for prefix comma. + +Example: + +foo( 1 + , 2 + , 3 +) + +class Foo: A + , B + , C + +class D + where A: AAA + , B: BBB + , C: CCC + +This is also known as Utrecht-style in the Haskell community." + (let ((parent (kotlin-mode--find-parent-of-list-element t))) + (if (eq (kotlin-mode--token-type parent) '\,) + ;; The comma was the 2nd or the following commas. + ;; Align with the previous comma. + (kotlin-mode--align-with-current-line) + ;; The comma was the 1st comma. + ;; Align with the end of the parent. + (goto-char (kotlin-mode--token-end parent)) + (backward-char) + (make-instance 'kotlin-mode--indentation :position (point) :offset 0)))) + +(defun kotlin-mode--calculate-indent-after-comma () + "Return indentation after comma. + +Assuming the point is on the comma." + (kotlin-mode--align-with-next-token + (kotlin-mode--find-parent-of-list-element nil))) + +(defun kotlin-mode--find-parent-of-list-element (&optional utrecht-style) + "Move point backward to the parent token of the comma under the point. + +If UTRECHT-STYLE is non-nil, stop at a comma at bol. Otherwise, stop at a +comma at eol." + ;; Various examples: + ;; + ;; val x = foo( // simple case + ;; 1, + ;; 2, + ;; 3 + ;; ) + ;; + ;; val x = xs[ + ;; 1, + ;; 2, + ;; 3 + ;; ] + ;; + ;; class Foo: A1(), + ;; B1 by object: C1, + ;; C2 { + ;; }, + ;; @AAA D1 + ;; where A: B, + ;; A: C, + ;; A: D { + ;; } + ;; + ;; when (foo) { + ;; object: B1, + ;; B2 { + ;; }, + ;; object: B3 by object: B4 { + ;; }, + ;; B5 { + ;; } -> + ;; 1 + ;; } + ;; + ;; fun foo(x: T): Int + ;; where + ;; T: A, + ;; T: B, + ;; T: C { + ;; } + ;; + ;; val A.foo: Int + ;; where + ;; T: A, + ;; T: B, + ;; T: C + ;; get() = aaa + ;; + ;; enum class Foo(x: Int) { + ;; A(1), B(2) {}, + ;; C(3) + ;; } + (let ((parent (kotlin-mode--backward-sexps-until + (append + (remove '\, kotlin-mode--expression-parent-tokens) + '("<" : {})) + (if utrecht-style nil '(\,)) + (if utrecht-style '(\,) nil)))) + (cond + ((equal (kotlin-mode--token-text parent) "<") + (if (save-excursion + (eq (kotlin-mode--token-type (kotlin-mode--forward-token-or-list)) + '<>)) + parent + (kotlin-mode--find-parent-of-list-element utrecht-style))) + + ((eq (kotlin-mode--token-type parent) '{}) + (let* ((brace-type-and-keyword-token (kotlin-mode--curly-brace-type)) + (keyword-token (cdr brace-type-and-keyword-token))) + (goto-char (kotlin-mode--token-start keyword-token))) + (kotlin-mode--find-parent-of-list-element utrecht-style)) + + ((eq (kotlin-mode--token-type parent) ':) + (if (kotlin-mode--colon-before-delegation-specifiers-p parent) + parent + (kotlin-mode--find-parent-of-list-element utrecht-style))) + + (t + parent)))) + +(defun kotlin-mode--find-parent-of-expression () + "Move point backward to the parent token of the expression under the point." + ;; TODO Unify with kotlin-mode--find-parent-of-list-element + (let ((parent (kotlin-mode--backward-sexps-until + (append kotlin-mode--expression-parent-tokens '("<" : {}))))) + (cond + ((equal (kotlin-mode--token-text parent) "<") + (if (save-excursion + (eq (kotlin-mode--token-type (kotlin-mode--forward-token-or-list)) + '<>)) + parent + (kotlin-mode--find-parent-of-expression))) + + ((eq (kotlin-mode--token-type parent) '{}) + (let* ((brace-type-and-keyword-token (kotlin-mode--curly-brace-type)) + (keyword-token (cdr brace-type-and-keyword-token))) + (goto-char (kotlin-mode--token-start keyword-token))) + (kotlin-mode--find-parent-of-expression)) + + ((eq (kotlin-mode--token-type parent) ':) + (if (kotlin-mode--colon-before-delegation-specifiers-p parent) + parent + (kotlin-mode--find-parent-of-expression))) + + (t + parent)))) + +(defun kotlin-mode--colon-before-delegation-specifiers-p (token) + "Return non-nil if TOKEN is a colon before delegationSpecifiers." + (and + (eq (kotlin-mode--token-type token) ':) + (save-excursion + (goto-char (kotlin-mode--token-start token)) + (let (previous-token) + ;; Try backward primaryConstructor. + (forward-comment (- (point))) + (when (eq (char-before) ?\)) + (kotlin-mode--backward-token-or-list) + (setq previous-token (kotlin-mode--backward-token)) + (if (equal (kotlin-mode--token-text previous-token) "constructor") + (kotlin-mode--try-backward-modifiers) + (goto-char (kotlin-mode--token-end previous-token)))) + (kotlin-mode--try-backward-type-parameters) + (setq previous-token (kotlin-mode--backward-token-or-list)) + (or + (equal (kotlin-mode--token-text previous-token) "object") + (and + (eq (kotlin-mode--token-type previous-token) 'atom) + (member (kotlin-mode--token-text (kotlin-mode--backward-token-or-list)) + '("class" "interface" "object")))))))) + +(defun kotlin-mode--calculate-indent-after-beginning-of-template-expression + (offset) + "Return indentation after the beginning of a template expression. + +It has offset specified with OFFSET. + +Assuming the point is before the string chunk." + (let ((pos (point))) + (kotlin-mode--forward-string-chunk) + (if (< pos (line-beginning-position)) + ;; The chunk has multiple lines. Align with this line with offset. + (progn + (back-to-indentation) + (make-instance 'kotlin-mode--indentation + :position (point) + :offset offset)) + ;; The chunk is single line. Indent like a expression. + (goto-char pos) + (kotlin-mode--calculate-indent-of-expression offset offset)))) + +(defun kotlin-mode--calculate-indent-after-semicolon () + "Return indentation after semicolon. + +Assuming the point is after the semicolon." + (while (save-excursion + (memq (kotlin-mode--token-type (kotlin-mode--backward-token)) + '(\; implicit-\;))) + (kotlin-mode--backward-token)) + (kotlin-mode--find-parent-and-align-with-next + (cl-remove-if + (lambda (e) + (member e '(\; implicit-\; bare-else + \(\)-before-control-structure-body when-expression-arrow))) + kotlin-mode--statement-parent-tokens) + 0 + '(\; implicit-\;))) + +(defun kotlin-mode--calculate-indent-before-in () + "Return indentation after \"in\" token. + +Assuming the point is before the token. +It is also assumed that the point is not just after \"{\" or \"<\"." + (let* ((type-and-parent (kotlin-mode--find-parent-of-in)) + (type (car type-and-parent)) + (parent (cdr type-and-parent))) + (if (memq type '(for when <>)) + (kotlin-mode--align-with-next-token parent) + (kotlin-mode--calculate-indent-after-semicolon)))) + +(defun kotlin-mode--find-parent-of-in () + "Return parent token of \"in\" token. + +Return a cons (TYPE . PARENT) where TYPE is one of symbol `for', +`when', `<>', or `other' and PARENT is the parent token, one of +`;', `implicit-;', `(', `{', or \"<\". + +Assuming the point is before the token." + ;; Examples: + ;; for ( + ;; x + ;; in // Align with token after "(" + ;; xs + ;; ) {} + ;; + ;; for (x + ;; in + ;; xs) {[]} + ;; + ;; when (x) { + ;; in xs -> 1 + ;; in ys -> 1 + ;; } + ;; + ;; Foo< + ;; in X, + ;; @AAA + ;; in X + ;; > + ;; + ;; val x = 1 // Line breaks are prohibited before infix "in" operator. + ;; in array + (let ((parent (save-excursion (kotlin-mode--backward-sexps-until + '(\; implicit-\; \( { "<"))))) + (cond + ;; Inside "for ()" + ((and + (eq (kotlin-mode--token-type parent) '\() + (save-excursion + (goto-char (kotlin-mode--token-start parent)) + (equal (kotlin-mode--token-text (kotlin-mode--backward-token)) "for"))) + (cons 'for parent)) + + ;; Inside "when () {}" + ((and + (eq (kotlin-mode--token-type parent) '{) + (save-excursion + (goto-char (kotlin-mode--token-start parent)) + (eq (kotlin-mode--token-type (kotlin-mode--backward-token-or-list)) + '\(\)) + (equal (kotlin-mode--token-text (kotlin-mode--backward-token)) + "when"))) + (cons 'when parent)) + + ;; Inside type parameters + ((equal (kotlin-mode--token-text parent) "<") + (if (save-excursion + (goto-char (kotlin-mode--token-start parent)) + (eq (kotlin-mode--token-type + (kotlin-mode--forward-token-or-list)) + '<>)) + (cons '<> parent) + (save-excursion + (goto-char (kotlin-mode--token-start parent)) + (kotlin-mode--find-parent-of-in)))) + + (t + (cons 'other parent))))) + +(defun kotlin-mode--backward-sexps-until (token-types + &optional + stop-at-eol-token-types + stop-at-bol-token-types) + "Backward sexps until a token with one of given token types appears. + +Return the token. +When this function returns, the point is at the start of the token. + +TOKEN-TYPES is a list of guard token types. This function backs to a +token with one of those token types. +STOP-AT-EOL-TOKEN-TYPES is a list of token types that if we skipped +the end of a line just after a token with one of given token type, the +function returns. Typically, this is a list of token types that +separates list elements (e.g. ',', ';'). If STOP-AT-EOL-TOKEN-TYPES +is the symbol `any', it matches all tokens. +STOP-AT-BOL-TOKEN-TYPES is a list of token types that if we hit the +beginning of a line just before a token with one of given token types, +the function returns. Typically, this is a list of token types that +starts list element . If STOP-AT-BOL-TOKEN-TYPES is the symbol `any', +it matches all tokens." + (let* + ((parent (kotlin-mode--backward-token-or-list)) + (type (kotlin-mode--token-type parent)) + (text (kotlin-mode--token-text parent))) + (while (not + ;; Stop loop when... + (or + ;; Hit a guard token. + (memq type token-types) + (member text token-types) + + ;; Beginning of the buffer. + (eq type 'outside-of-buffer) + + ;; When this function is called on "," token before position (1), + ;; this function stops just before the "," token after "Foo". + ;; + ;; Foo, + ;; Bar, Baz, // (1) + ;; AAA + (and stop-at-eol-token-types + (or (eq stop-at-eol-token-types 'any) + (member type stop-at-eol-token-types) + (member text stop-at-eol-token-types)) + (save-excursion + (kotlin-mode--forward-token-or-list) + (forward-comment (- (point))) + (kotlin-mode--eol-other-than-comments-p))) + + ;; When this function is called on "," token before position + ;; (1), this function stops just before ", Bar". + ;; + ;; , Foo + ;; , Bar, Baz: + ;; , AAA // (1) + (and stop-at-bol-token-types + (and + (or + (eq stop-at-bol-token-types 'any) + (member type stop-at-bol-token-types) + (member text stop-at-bol-token-types)) + (kotlin-mode--bol-other-than-comments-p))))) + (setq parent (kotlin-mode--backward-token-or-list)) + (setq type (kotlin-mode--token-type parent)) + (setq text (kotlin-mode--token-text parent))) + parent)) + +(defun kotlin-mode--align-with-next-token (parent &optional offset) + "Return indentation with the next token of PARENT with OFFSET. + +Example: + +Suppose indenting \"B\": + +foo { + /* */ A() + B() +} + +The parent is \"{\". We align with the comment before \"A\"." + (let ((parent-end (kotlin-mode--token-end parent))) + (goto-char parent-end) + (forward-comment (point-max)) + (kotlin-mode--goto-non-comment-bol) + (when (< (point) parent-end) + (goto-char parent-end)) + (kotlin-mode--skip-whitespaces) + (make-instance 'kotlin-mode--indentation + :position (point) + :offset (or offset 0)))) + +(defun kotlin-mode--align-with-token (token &optional offset) + "Return indentation with the TOKEN with OFFSET. + +If the token is preceded by comments on the same line, align with that +comments instead. + +Example: + +Suppose indenting \"B\": + +foo { + /* */ A() + B() +} + +We align with the comment before \"A\"." + (goto-char (kotlin-mode--token-start token)) + (forward-comment (- (point))) + (kotlin-mode--align-with-next-token (kotlin-mode--backward-token) offset)) + +(defun kotlin-mode--align-with-current-line (&optional offset) + "Return indentation of the current line with OFFSET." + (kotlin-mode--goto-non-comment-bol) + (kotlin-mode--skip-whitespaces) + (make-instance 'kotlin-mode--indentation + :position (point) + :offset (or offset 0))) + +(defun kotlin-mode--goto-non-comment-bol-with-same-nesting-level + (&optional use-backward-token-simple) + "Back to the beginning of line that is not inside a comment. + +See `kotlin-mode--backward-token-or-list' for USE-BACKWARD-TOKEN-SIMPLE." + (while (not (kotlin-mode--bol-other-than-comments-p)) + (kotlin-mode--backward-token-or-list use-backward-token-simple))) + + +;;; indent-new-comment-line + +(defun kotlin-mode--indent-new-comment-line (&optional soft) + "Break the line at the point and indent the new line. + +If the point is inside a comment, continue the comment. If the comment is a +multiline comment, close the previous comment and start new one if +`comment-multi-line' is nil. +See `indent-new-comment-line' for SOFT." + (interactive) + (let* ((chunk (kotlin-mode--chunk-after)) + (comment-beginning-position (kotlin-mode--chunk-start chunk))) + (if soft (insert-and-inherit ?\n) (newline 1)) + (delete-horizontal-space) + + ;; Insert a prefix and indent the line. + (cond + ((not (kotlin-mode--chunk-comment-p chunk)) + (indent-according-to-mode)) + + ((kotlin-mode--chunk-single-line-comment-p chunk) + (insert-and-inherit + (save-excursion + (goto-char comment-beginning-position) + (looking-at "/+\\s *") + (match-string-no-properties 0))) + (indent-according-to-mode)) + + ((not comment-multi-line) + (insert-and-inherit + (save-excursion + (goto-char comment-beginning-position) + (looking-at "/\\*+\\s *") + (match-string-no-properties 0))) + ;; Clean up and close the previous line. + (save-excursion + (beginning-of-line) + (backward-char) + (delete-horizontal-space) + (insert-and-inherit " */")) + (indent-according-to-mode)) + + (t + (kotlin-mode--format-multiline-comment-line-after-newline chunk soft))) + ;; Clean up the previous line. + (save-excursion + (beginning-of-line) + (backward-char) + (delete-horizontal-space)))) + +(defun kotlin-mode--format-multiline-comment-line-after-newline (chunk soft) + "Insert prefix and indent current line in multiline comment. + +The point is assumed inside multiline comment and just after newline. + +The closing delimiter is also inserted and/or formatted depending on custom +variables `kotlin-mode--auto-close-multiline-comment' and +`kotlin-mode--break-line-before-comment-close'. + +CHUNK is the comment chunk. + +See `indent-new-comment-line' for SOFT." + (let ((comment-beginning-position (kotlin-mode--chunk-start chunk))) + (cond + ((save-excursion + (forward-line -1) + (<= (point) comment-beginning-position)) + ;; The point was on the 2nd line of the comment. + + ;; If the comment have only one line, delete a space after asterisk. + ;; + ;; Example: + ;; /** aaa */ + ;; ↓ + ;; /** + ;; * aaa + ;; */ + ;; + ;; /** aaa */ + ;; ↓ + ;; /** + ;; * aaa + ;; */ + ;; + ;; + ;; If the comment spans several lines, keep spaces. + ;; + ;; /** aaa + ;; */ + ;; ↓ + ;; /** + ;; * aaa + ;; */ + (when (= (line-beginning-position) + (save-excursion + (goto-char comment-beginning-position) + (forward-comment 1) + (line-beginning-position))) + (save-excursion + (goto-char comment-beginning-position) + (forward-char) + (skip-chars-forward "*") + (when (looking-at " [ \t]*$") + (delete-char 1)))) + + ;; If the point is just before the closing delimiter, break the line. + (when (and kotlin-mode--break-line-before-comment-close + (= (point) + (save-excursion + (goto-char comment-beginning-position) + (if (forward-comment 1) + (progn + (backward-char) + (skip-chars-backward "*") + (point)) + -1)))) + (save-excursion + (if soft (insert-and-inherit ?\n) (newline 1)) + (indent-according-to-mode))) + + ;; Invoke `kotlin-mode--indent-line`. + (indent-according-to-mode) + + ;; Insert or replace a space to an asterisk. + (when kotlin-mode--prepend-asterisk-to-comment-line + (let ((columns-from-end (- (line-end-position) (point)))) + (move-to-column + (save-excursion + (goto-char comment-beginning-position) + (forward-char) + (current-column))) + (insert-and-inherit "*") + (when (eq (char-after) ?\s) + (delete-char 1)) + (when (and + kotlin-mode--insert-space-after-asterisk-in-comment + (not (eq (char-after) ?\s))) + (insert-and-inherit " ")) + (goto-char (- (line-end-position) columns-from-end))))) + + ;; The point was on the 3nd or following lines of + ;; the comment. + ;; Use the prefix of the previous line. + + ((and + kotlin-mode--prepend-asterisk-to-comment-line + (save-excursion + (forward-line -1) + (looking-at "\\s *\\(\\*+\\s *\\)"))) + ;; The previous line has a prefix. Use it. + (insert-and-inherit (match-string-no-properties 1)) + (indent-according-to-mode)) + + (t + ;; Use the default indentation. + (indent-according-to-mode))) + + ;; Close incomplete multiline comment. + (when (and kotlin-mode--auto-close-multiline-comment + (kotlin-mode--incomplete-comment-p chunk)) + (save-excursion + (end-of-line) + (when comment-multi-line + (if soft (insert-and-inherit ?\n) (newline 1))) + (insert-and-inherit "*/") + (indent-according-to-mode))) + + ;; Make sure the closing delimiter is on its own line. + (when kotlin-mode--break-line-before-comment-close + (save-excursion + (goto-char comment-beginning-position) + (when (forward-comment 1) + (backward-char) + (skip-chars-backward "*") + (skip-syntax-backward " ") + (when (not (bolp)) + (if soft (insert-and-inherit ?\n) (newline 1)) + (indent-according-to-mode))))))) + +(defun kotlin-mode--post-self-insert () + "Miscellaneous logic for electric indentation." + (cond + ;; Indent electrically and insert a space when "*" is inserted at the + ;; beginning of a line inside a multiline comment. + ((and + kotlin-mode--prepend-asterisk-to-comment-line + (eq last-command-event ?*) + (kotlin-mode--chunk-comment-p (kotlin-mode--chunk-after)) + (save-excursion (backward-char) (skip-syntax-backward " ") (bolp))) + (when kotlin-mode--insert-space-after-asterisk-in-comment + (insert-and-inherit " ")) + (when electric-indent-mode + (indent-according-to-mode))) + + ;; Fixe "* /" at the end of a multiline comment to "*/". + ((and + kotlin-mode--fix-comment-close + (eq last-command-event ?/) + (let ((chunk (kotlin-mode--chunk-after)) + (pos (point))) + (and + (kotlin-mode--chunk-comment-p chunk) + (save-excursion + (beginning-of-line) + (and + (looking-at "^\\s *\\*\\s +/") + (eq (match-end 0) pos) + (kotlin-mode--incomplete-comment-p chunk)))))) + (backward-char) + (delete-horizontal-space) + (forward-char)) + + ;; Indent electrically when "}" is inserted at bol as the end of a string + ;; interpolation. + ((and + electric-indent-mode + (eq last-command-event ?\}) + (save-excursion (backward-char) (skip-syntax-backward " ") (bolp)) + (eq (kotlin-mode--chunk-start (kotlin-mode--chunk-after)) (1- (point)))) + (indent-according-to-mode)) + + ;; Indent electrically after newline inside strings and comments. + ;; Unlike `electric-indent-mode', the previous line is not indented. + ((and + electric-indent-mode + (eq last-command-event ?\n)) + (let ((chunk (kotlin-mode--chunk-after))) + (if (kotlin-mode--chunk-multiline-comment-p chunk) + (progn + (delete-horizontal-space) + (kotlin-mode--format-multiline-comment-line-after-newline + chunk + (not use-hard-newlines))) + (indent-according-to-mode))) + (save-excursion + (beginning-of-line) + (backward-char) + (delete-horizontal-space))))) + +(defun kotlin-mode--highlight-anchor (indentation) + "Highlight the anchor point of the INDENTATION." + (move-overlay + kotlin-mode--anchor-overlay + (kotlin-mode--indentation-position indentation) + (1+ (kotlin-mode--indentation-position indentation))) + + (overlay-put kotlin-mode--anchor-overlay 'face 'highlight) + + (when kotlin-mode--anchor-overlay-timer + (cancel-timer kotlin-mode--anchor-overlay-timer)) + + (let ((buffer (current-buffer))) + (setq kotlin-mode--anchor-overlay-timer + (run-at-time + "1 sec" + nil + (lambda () + (when (buffer-live-p buffer) + (with-current-buffer buffer + (delete-overlay kotlin-mode--anchor-overlay) + (setq kotlin-mode--anchor-overlay-timer nil)))))))) + +(provide 'kotlin-mode-indent) + +;;; kotlin-mode-indent.el ends here diff --git a/kotlin-mode-lexer.el b/kotlin-mode-lexer.el index 29576be..c19e999 100644 --- a/kotlin-mode-lexer.el +++ b/kotlin-mode-lexer.el @@ -1,8 +1,10 @@ ;;; kotlin-mode-lexer.el --- Major mode for kotlin, lexer -*- lexical-binding: t; -*- +;; Copyright © 2015 Shodai Yokoyama ;; Copyright © 2019 taku0 -;; Author: taku0 (http://github.com/taku0) +;; Author: Shodai Yokoyama (quantumcars@gmail.com) +;; taku0 (http://github.com/taku0) ;; Keywords: languages ;; Package-Requires: ((emacs "24.3")) @@ -51,6 +53,41 @@ ;; ;; This is not a official term; used only in kotlin-mode. +;;; Brackets + +(defvar-local kotlin-mode--bracket-positions '() + "List of position of brackets. + +Element of the list is a cons (TYPE . POSITION) where TYPE is one of +\(, ), {, }, [, or ], and POSITION is the position of the token. + +Elements are sorted by the position in descending order.") + +(defun kotlin-mode--clear-bracket-positions-after (position) + "Remove bracket positions after or equal to POSITION from cache." + (while (and kotlin-mode--bracket-positions + (<= position (cdar kotlin-mode--bracket-positions))) + (pop kotlin-mode--bracket-positions))) + +(defun kotlin-mode--find-containing-brackets (position) + "Return start position of innermost brackets containing POSITION. + +Return a cons (TYPE . POSITION) where TYPE is one of (, ), {, }, [, ], +or outside-of-buffer, and POSITION is the position of the token." + (let ((brackets kotlin-mode--bracket-positions) + (nesting-level 1)) + (while (and brackets (<= position (cdar brackets))) + (pop brackets)) + (while (and brackets (not (zerop nesting-level))) + (if (memq (caar brackets) '(\( \[ {)) + (setq nesting-level (1- nesting-level)) + (setq nesting-level (1+ nesting-level))) + (unless (zerop nesting-level) + (pop brackets))) + (if brackets + (car brackets) + (cons 'outside-of-buffer (point-min))))) + ;;; Text properties ;; See also doc/string_properties.png. @@ -93,7 +130,9 @@ Mark the beginning of and the end of single-line/multiline strings, character literals, backquoted identifiers between the position START and END as general string delimiters. -Intended for `syntax-propertize-function'." +Intended for `syntax-propertize-function'. + +Also track position of brackets in `kotlin-mode--bracket-positions'." (remove-text-properties start end '(syntax-table nil @@ -103,6 +142,7 @@ Intended for `syntax-propertize-function'." nil kotlin-property--interpolation nil)) + (kotlin-mode--clear-bracket-positions-after start) (let* ((chunk (kotlin-mode--chunk-after (syntax-ppss start)))) (cond ((kotlin-mode--chunk-multiline-string-p chunk) @@ -130,50 +170,70 @@ Mark the beginning of and the end of single-line/multiline strings and character literals between the current position and END as general string delimiters. -Assuming the cursor is not on strings, character-literal, +Assuming the point is not on strings, character-literal, backquoted identifier, nor comments. If NESTING-LEVEL is non-zero, nesting of brackets are tracked and the scan stops where the level becomes zero." (let ((found-matching-bracket nil) - (pattern (rx (or "\"\"\"" "\"" "//" "/*" "{" "}" "'" "`")))) + (pattern + (rx (or "\"\"\"" "\"" "//" "/*" "{" "}" "(" ")" "[" "]" "'" "`"))) + match-string + start) (while (and (not found-matching-bracket) (< (point) end) (search-forward-regexp pattern end t)) + (setq match-string (match-string-no-properties 0)) + (setq start (match-beginning 0)) (cond - ((member (match-string-no-properties 0) '("\"\"\"" "\"" "'" "`")) - (let ((start (match-beginning 0)) - (quotation (match-string-no-properties 0))) - (put-text-property start (1+ start) + ;; Quotes + ((member match-string '("\"\"\"" "\"" "'" "`")) + ;; Mark the start of the quotes as a generic string delimiter. + (put-text-property start (1+ start) + 'syntax-table + (string-to-syntax "|")) + ;; Scan until end of the string. + (kotlin-mode--syntax-propertize-end-of-string end match-string) + ;; Mark whole string, including template expressions, + ;; with `syntax-multiline'. The property is used by + ;; `kotlin-mode--syntax-propertize-extend-region'. + (put-text-property start (point) 'syntax-multiline t) + + (when (equal match-string "`") + ;; Backquotes cannot be escaped. So declare the backslashes in + ;; the identifier are normal characters, not escape-syntax characters. + (put-text-property (1+ start) (1- (point)) 'syntax-table - (string-to-syntax "|")) - (kotlin-mode--syntax-propertize-end-of-string end quotation) - (put-text-property start (point) 'syntax-multiline t) - - (when (equal quotation "`") - ;; Backquotes cannot be escaped. So declares the backslashes in - ;; the identifier are not a escape-syntax characters. - (put-text-property (1+ start) (1- (point)) - 'syntax-table - (string-to-syntax "w"))))) + (string-to-syntax "w")))) - ((equal "//" (match-string-no-properties 0)) - (goto-char (match-beginning 0)) + ;; Single-line comment + ((equal "//" match-string) + (goto-char start) (forward-comment (point-max))) - ((equal "/*" (match-string-no-properties 0)) - (goto-char (match-beginning 0)) + ;; Multiline comment + ((equal "/*" match-string) + (goto-char start) (forward-comment (point-max))) - ((and (equal "{" (match-string-no-properties 0)) + ;; Open curly bracket + ((and (equal "{" match-string) (/= nesting-level 0)) + (push (cons (intern match-string) start) kotlin-mode--bracket-positions) (setq nesting-level (1+ nesting-level))) - ((and (equal "}" (match-string-no-properties 0)) + ;; Close curly bracket + ((and (equal "}" match-string) (/= nesting-level 0)) + (push (cons (intern match-string) start) kotlin-mode--bracket-positions) (setq nesting-level (1- nesting-level)) (when (= nesting-level 0) - (setq found-matching-bracket t))))) + (setq found-matching-bracket t))) + + ;; Other brackets + ((member match-string '("(" ")" "[" "]" "{" "}")) + (push (cons (intern match-string) start) + kotlin-mode--bracket-positions)))) (unless found-matching-bracket (goto-char end)) found-matching-bracket)) @@ -181,7 +241,7 @@ the scan stops where the level becomes zero." (defun kotlin-mode--syntax-propertize-end-of-string (end quotation) "Move point to the end of single-line/multiline string. -Assuming the cursor is on a string, a character literal, or a backquoted +Assuming the point is on a string, a character literal, or a backquoted identifier. If the string go beyond END, stop there. The string should be terminated with QUOTATION." @@ -195,12 +255,15 @@ The string should be terminated with QUOTATION." (and "`" (+ (not (any "`\n"))) "`"))))) end t)) (cond + ;; End of the string ((and (equal quotation (match-string-no-properties 0)) (or (equal quotation "`") ; backquotes cannot be escaped (not (kotlin-mode--escaped-p (match-beginning 0))))) (put-text-property (1- (point)) (point) 'syntax-table (string-to-syntax "|"))) + + ;; Start of a template expression ((and (equal "${" (match-string-no-properties 0)) (member quotation '("\"\"\"" "\"")) ;; Dollar signs cannot be escaped, so we don't need to check it. @@ -214,30 +277,39 @@ The string should be terminated with QUOTATION." (backward-char) ;; { (backward-char) ;; $ (point)))) - ;; Declares the open bracket is a generic string delimiter. + ;; Keep the position of the open bracket. + (push (cons '{ (1+ start)) kotlin-mode--bracket-positions) + ;; Declare the open bracket is a generic string delimiter. (put-text-property (1- pos-after-open-bracket) pos-after-open-bracket 'syntax-table (string-to-syntax "|")) + ;; Try to skip (when (kotlin-mode--syntax-propertize-scan end 1) ;; Found the matching bracket. Going further. - ;; Declares the close bracket is a generic string delimiter. + ;; Declare the close bracket is a generic string delimiter. (put-text-property (1- (point)) (point) 'syntax-table (string-to-syntax "|")) - ;; Records the positions. + ;; Record the positions. (put-text-property (1- (point)) (point) 'kotlin-property--matching-bracket start) (put-text-property start pos-after-open-bracket 'kotlin-property--matching-bracket (1- (point))) + ;; Proceed. (kotlin-mode--syntax-propertize-end-of-string end quotation)))) - ((match-string-no-properties 0) + + ;; Template expression without braces + ((and (eq (aref (match-string-no-properties 0) 0) ?$) + (not (eq (aref (match-string-no-properties 0) 1) ?{))) (put-text-property (match-beginning 0) (1+ (match-beginning 0)) 'kotlin-property--interpolation (match-data)) (kotlin-mode--syntax-propertize-end-of-string end quotation)) + + ;; Others (t (kotlin-mode--syntax-propertize-end-of-string end quotation))) (goto-char end))) @@ -260,7 +332,6 @@ Return nil otherwise." (defclass kotlin-mode--chunk () ((type :initarg :type :type symbol - :accessor kotlin-mode--chunk-type :documentation "The type of the chunk. Valid values: @@ -272,12 +343,19 @@ Valid values: - backquoted-identifier") (start :initarg :start :type number - :accessor kotlin-mode--chunk-start :documentation "The start position of the chunk.")) "String-chunks, comments, character literals, or backquoted identifiers. It have the type and the start position.") +(defun kotlin-mode--chunk-type (chunk) + "Return the type of the CHUNK." + (and chunk (oref chunk type))) + +(defun kotlin-mode--chunk-start (chunk) + "Return the start position of the CHUNK." + (and chunk (oref chunk start))) + (defun kotlin-mode--chunk-comment-p (chunk) "Return non-nil if the CHUNK is a comment." (and chunk @@ -314,10 +392,19 @@ It have the type and the start position.") "Return non-nil if the CHUNK is a backquoted identifier." (and chunk (eq (kotlin-mode--chunk-type chunk) 'backquoted-identifier))) +(defun kotlin-mode--incomplete-comment-p (chunk) + "Return t if the CHUNK is incomplete comment. + +Return nil otherwise." + (and (kotlin-mode--chunk-comment-p chunk) + (save-excursion + (goto-char (kotlin-mode--chunk-start chunk)) + (not (forward-comment 1))))) + (defun kotlin-mode--chunk-after (&optional parser-state) - "Return the chunk at the cursor. + "Return the chunk at the point. -If the cursor is outside of strings and comments, return nil. +If the point is outside of strings and comments, return nil. If PARSER-STATE is given, it is used instead of (syntax-ppss)." (save-excursion @@ -330,14 +417,27 @@ If PARSER-STATE is given, it is used instead of (syntax-ppss)." ;; Syntax category "|" is attached to both single-line and multiline ;; string delimiters. So (nth 3 parser-state) may be t even for ;; single-line string delimiters. - (if (save-excursion (goto-char (nth 8 parser-state)) - (looking-at "\"\"\"")) + (save-excursion + (goto-char (nth 8 parser-state)) + (forward-char) + (kotlin-mode--beginning-of-string) + (cond + ((looking-at "\"\"\"") (make-instance 'kotlin-mode--chunk :type 'multiline-string - :start (nth 8 parser-state)) - (make-instance 'kotlin-mode--chunk - :type 'single-line-string - :start (nth 8 parser-state)))) + :start (nth 8 parser-state))) + ((looking-at "`") + (make-instance 'kotlin-mode--chunk + :type 'backquoted-identifier + :start (nth 8 parser-state))) + ((looking-at "'") + (make-instance 'kotlin-mode--chunk + :type 'character-literal + :start (nth 8 parser-state))) + (t + (make-instance 'kotlin-mode--chunk + :type 'single-line-string + :start (nth 8 parser-state)))))) ((eq (nth 4 parser-state) t) (make-instance 'kotlin-mode--chunk :type 'single-line-comment @@ -357,6 +457,2135 @@ If PARSER-STATE is given, it is used instead of (syntax-ppss)." (t nil)))) +;; Syntax table + +(defvar kotlin-mode-syntax-table + (let ((st (make-syntax-table))) + + ;; Strings + (modify-syntax-entry ?\" "\"" st) + (modify-syntax-entry ?\' "\"" st) + (modify-syntax-entry ?` "\"" st) + + ;; `_' and `@' as being a valid part of a symbol + (modify-syntax-entry ?_ "_" st) + (modify-syntax-entry ?@ "_" st) + + ;; b-style comment + (modify-syntax-entry ?/ ". 124b" st) + (modify-syntax-entry ?* ". 23n" st) + (modify-syntax-entry ?\n "> b" st) + (modify-syntax-entry ?\r "> b" st) + st)) + +;; Token + +(defclass kotlin-mode--token () + ((type :initarg :type + :type symbol + :documentation "The type of the token. + +Token types is one of the following symbols: + +- operator (including as, as?, is, !is, in, !in, ., and ->) +- annotation + (e.g. @ABC, @ABC(def), @file:ABC, @[ABC DEF(ghi)], or @file:[ABC DEF(ghi)]) +- atom (including keywords, numbers, strings, and unknown tokens) +- label +- return (including return@identifier) +- continue (including continue@identifier) +- beak (including beak@identifier) +- this (including this@identifier) +- super (including super@identifier) +- [ +- ] +- { +- } +- ( +- ) +- )-before-control-structure-body +- , +- ; +- implicit-; +- < (as an angle bracket) +- > (as an angle bracket) +- : +- string-chunk-after-template-expression (part of a string ending with \"}\") +- string-chunk-before-template-expression (part of a string ending with \"${\") +- bare-else (\"else\" not followed by \"if\" on the same line, \"->\", or \"{\") +- outside-of-buffer +- anonymous-function-parameter-arrow +- when-expression-arrow + +Additionally, `kotlin-mode--backward-token-or-list' may return a parenthesized +expression as a token with one of the following types: +- () +- ()-before-control-structure-body +- [] +- {} +- <>" ) + (text :initarg :text + :type string + :documentation "The text of the token.") + (start :initarg :start + :type integer + :documentation "The start position of the token.") + (end :initarg :end + :type integer + :documentation "The point after the token.")) + "Token of Kotlin.") + +(defun kotlin-mode--token-type (token) + "Return the type of TOKEN." + (and token (oref token type))) + +(defun kotlin-mode--token-text (token) + "Return the text of TOKEN." + (and token (oref token text))) + +(defun kotlin-mode--token-start (token) + "Return the start position of TOKEN." + (and token (oref token start))) + +(defun kotlin-mode--token-end (token) + "Return the end position of TOKEN." + (and token (oref token end))) + +;; Token level movements and predicates. + +(defconst kotlin-mode--inheritance-modifier-keywords + '("open" "final" "abstract")) + +(defconst kotlin-mode--visibility-modifier-keywords + '("public" "private" "internal" "protected")) + +(defconst kotlin-mode--member-modifier-keywords + '("lateinit" "override")) + +(defconst kotlin-mode--companion-modifier-keywords + '("companion")) + +(defconst kotlin-mode--class-modifier-keywords + '("enum" "sealed" "annotation" "data" "inner")) + +(defconst kotlin-mode--property-modifier-keywords + '("const")) + +(defconst kotlin-mode--platform-modifier-keywords + '("expect" "actual")) + +(defconst kotlin-mode--parameter-modifier-keywords + '("vararg" "crossinline" "noinline")) + +(defconst kotlin-mode--function-modifier-keywords + '("tailrec" "operator" "infix" "inline" "value" "external" "suspend")) + +(defconst kotlin-mode--modifier-keywords + (append kotlin-mode--inheritance-modifier-keywords + kotlin-mode--visibility-modifier-keywords + kotlin-mode--member-modifier-keywords + kotlin-mode--class-modifier-keywords + kotlin-mode--property-modifier-keywords + kotlin-mode--platform-modifier-keywords + kotlin-mode--parameter-modifier-keywords + kotlin-mode--function-modifier-keywords)) + +(defun kotlin-mode--implicit-semi-p () + "Return non-nil if the point is after the end of a statement." + (let ((previous-token (save-excursion + (kotlin-mode--extend-annotation-token-backward + (kotlin-mode--backward-token-simple)))) + (next-token (save-excursion + (kotlin-mode--extend-annotation-token-forward + (kotlin-mode--forward-token-simple))))) + ;; If the point is on the empty line, pretend an identifier is on the line. + (when (and + (< (kotlin-mode--token-end previous-token) (line-beginning-position)) + (< (line-end-position) (kotlin-mode--token-start next-token))) + (setq next-token (make-instance 'kotlin-mode--token + :type 'atom + :text "" + :start (point) + :end (point)))) + (cond + ;; Tokens that end a statement + ((memq (kotlin-mode--token-text previous-token) + '("return" "continue" "break")) + t) + + ;; .* in import declarations end a statement. + ((and (equal (kotlin-mode--token-text previous-token) "*") + (equal (kotlin-mode--token-text + (save-excursion + (goto-char (kotlin-mode--token-start previous-token)) + (kotlin-mode--backward-token-simple))) + ".")) + t) + + ;; Suppress implicit semicolon after "if ()", "while ()", "for ()" + ((kotlin-mode--close-parenthesis-before-control-structure-body-p + previous-token) + nil) + + ;; Annotations and modifiers, that cannot end a statement/declaration + ((or + ;; Annotations + (and + (eq (kotlin-mode--token-type previous-token) 'annotation) + ;; Exclude super@label. Note that this@label is handled by + ;; `kotlin-mode--backward-token-simple' + (not (save-excursion + (goto-char (kotlin-mode--token-start previous-token)) + (and (eq (char-before) ?>) + (kotlin-mode--try-backward-type-parameters) + (equal (kotlin-mode--token-text + (kotlin-mode--backward-token-simple)) + "super"))))) + ;; Labels + (eq (kotlin-mode--token-type previous-token) 'label) + ;; Modifiers + (and (member (kotlin-mode--token-text previous-token) + kotlin-mode--modifier-keywords) + (not (equal (kotlin-mode--token-text previous-token) "value")))) + nil) + + ;; Tokens that cannot end a statement + ;; + ;; TODO prefix ++ and -- + ;; TODO infix function call + ;; TODO prefix !, especially !! + ;; + ;; Example: + ;; var shl = 1 + ;; val x = shl shl shl + ;; shl < 100 && foo() // this is not a continuation of the previous line. + ;; + ;; var shl = 1 + ;; val x = shl shl + ;; shl < 100 && foo() // this is a continuation of the previous line. + ;; + ;; var shl = 1 + ;; val x = shl shl shl ++ + ;; shl < 100 && foo() // this is not a continuation of the previous line. + ;; + ;; var shl = 1 + ;; val x = shl shl ++ + ;; shl < 100 && foo() // this is a continuation of the previous line. + ;; + ;; val x = foo()!! + ;; foo() // this is not a continuation of the previous line. + ;; + ;; val x = !! + ;; foo() // this is a continuation of the previous line. + ((or + (memq + (kotlin-mode--token-type previous-token) + '(implicit-\; string-chunk-before-template-expression)) + (member + (kotlin-mode--token-text previous-token) + '("(" "{" "[" "*" "%" "/" "+" "-" "&&" "||" ":" "&" + "=" "+=" "-=" "*=" "/=" "%=" + "->" "." ".." "..<" "::" "?:" "?." "<=" ">=" "!=" "!==" "==" "===" + "as" "as?" "is" "!is" "in" "!in" "," ";" "{" "[" "(" + ;; "class" will be handled later. + "package" "import" "interface" "fun" "object" + "val" "var" "typealias" "constructor" "by" "companion" "init" + "where" "if" "else" "when" "try" "catch" "finally" "for" "do" + "while" "throw" "out" "reified")) + ;; Inequality operator cannot end a statement/declaration. + (and (eq (kotlin-mode--token-type previous-token) 'operator) + (member (kotlin-mode--token-text previous-token) '("<" ">")))) + nil) + + ;; "class" cannot end a statement unless preceded by "::". + ;; + ;; Example: + ;; + ;; class // No implicit semicolon here + ;; Foo { + ;; } + ;; + ;; val x = Foo :: + ;; class // Implicit semicolon here + ;; foo() + ((and (equal (kotlin-mode--token-text previous-token) "class") + (save-excursion + (goto-char (kotlin-mode--token-start previous-token)) + (not (equal (kotlin-mode--token-text + (kotlin-mode--backward-token-simple)) + "::")))) + nil) + + ;; Annotations, labels, and modifiers, that start a statement/declaration, + ;; except for getter and setter. + ;; + ;; "suspend" is excluded because it can be used as type modifier. + ((or + (memq (kotlin-mode--token-type next-token) '(annotation label)) + (member (kotlin-mode--token-text next-token) + (remove "suspend" kotlin-mode--modifier-keywords))) + (not (save-excursion + (kotlin-mode--try-forward-modifiers) + (member (kotlin-mode--token-text + (kotlin-mode--forward-token-simple)) + '("get" "set"))))) + + ;; Tokens that start a statement/declaration + ((member + (kotlin-mode--token-text next-token) + ;; "class" will be handled later. + '("package" "import" "interface" "val" "var" "typealias" + "constructor" "companion" "init" "for" "do" "is" + ;; While we should insert a semicolon before "in" in a "when" + ;; expression or beginning of a expression, we should + ;; suppress a semicolon before "in" in "for", type + ;; parameters, or type arguments: + ;; + ;; when (x) { + ;; 1 -> 1 // implicit semicolon here + ;; in xs -> 2 + ;; } + ;; + ;; // line breaks are prohibited before infix "in" operator. + ;; val x = 1 // implicit semicolon here + ;; in xs // Though this is an invalid expression anyway. + ;; + ;; for ( + ;; x // no implicit semicolons here + ;; in + ;; xs + ;; ) {} + ;; + ;; Foo< // no implicit semicolons here + ;; in X + ;; > + ;; + ;; Because detecting the context is hard at lexer level, we + ;; omit semicolon for now, and handle it later in the + ;; indentation code. + )) + t) + + ;; While or do-while + ((equal (kotlin-mode--token-text next-token) "while") + ;; insert semicolon unless it is a part of do-while. + (save-excursion (not (kotlin-mode--find-do-for-while)))) + + ;; "class" starts a new declaration unless preceded by "::". + ;; + ;; Example: + ;; + ;; class Foo { // This start a class declaration. + ;; } + ;; + ;; Foo :: + ;; class // This does not start a class declaration. + ((and (equal (kotlin-mode--token-text next-token) "class") + (not (equal (kotlin-mode--token-text previous-token) "::"))) + t) + + ;; Tokens that cannot start a statement/declaration. + ((or (memq + (kotlin-mode--token-type next-token) + '(implicit-\; string-chunk-after-template-expression)) + (member + (kotlin-mode--token-text next-token) + '("*" "%" "/" "&" "&&" "||" ":" "=" "+=" "-=" "*=" "/=" "%=" + "->" "." ".." "..<" "::" "?:" "?." "?" "<" ">" "<=" ">=" + "!=" "!==" "==" "===" + "," ";" ")" "]" "}" + "as" "as?" "get" "set" "by" "where" "else" "catch" "finally" + ;; Because detecting the context is hard at lexer level, + ;; we omit semicolon before "in" for now, and handle it + ;; later in the indentation code. + "in" + ;; Strictly speaking, open curly bracket may start a statement + ;; as a part of lambda expression, it is rare case, so we + ;; suppress semicolon before it. + "{"))) + nil) + + ;; Open square bracket start a new statement unless preceded by a + ;; colon (already handled above). + ((eq (kotlin-mode--token-type next-token) '\[) + t) + + ;; Open parenthesis start a new statement unless preceded by: + ;; - a colon (already handled above), + ;; - a comma (already handled above), + ;; - a dot (already handled above), + ;; - an equal sign (already handled above), + ;; - "->" (already handled above), + ;; - "is" (already handled above), + ;; - "!is" (already handled above), + ;; - "as" (already handled above), + ;; - "as?" (already handled above), + ;; - "if" (already handled above), + ;; - "while" (already handled above), + ;; - "for" (already handled above), + ;; - "when" (already handled above), + ;; - "catch" (already handled above), + ;; or is a part of: + ;; - secondary constructor declaration, + ;; - class declaration, + ;; - function declaration, + ;; - variable declaration, + ;; - getter declaration, + ;; - setter declaration, + ;; - constructor declaration call (example: constructor(): this(1)), + ;; + ;; Examples: + ;; + ;; public constructor() {} + ;; + ;; public class Foo() {} + ;; public class Foo () {} + ;; public class Foo public constructor () + ;; public class Foo @AAA constructor () + ;; + ;; fun foo() {} + ;; fun foo() {} + ;; fun (A).foo() {} + ;; fun A.B.C.foo() {} + ;; fun @AAA (A).foo() {} + ;; fun (A).foo() {} + ;; fun @AAA (A).foo() {} + ;; fun @AAA A.B.C.foo() {} + ;; + ;; fun () {} + ;; fun (A).() {} + ;; fun A.B.C.() {} + ;; + ;; val (x, y) = foo() + ;; var (x, y) = foo() + ;; val (x, y) = foo() + ;; val (A).(x, y) = foo() + ;; val (A).(x, y) = foo() + ;; val @AAA A.(x, y) = foo() + ;; val @AAA A.B.C.(x, y) = foo() + ;; + ;; get() {} + ;; set(value) {} + ;; + ;; constructor (): this(1) + ;; constructor (): super(1) + ;; + ;; Note that function argument cannot preceded by newline: + ;; + ;; val x = foo + ;; (bar()) // This is not a continuation of the previous line. + ((eq (kotlin-mode--token-type next-token) '\() + (and + (not (kotlin-mode--constructor-parameter-clause-p)) + (not (kotlin-mode--function-parameter-clause-p)) + (not (kotlin-mode--multi-variable-declaration-p)) + (not (kotlin-mode--constructor-delegation-call-p)))) + + ;; Suppress implicit semicolon after the beginning of an interpolated + ;; expression. + ((eq (kotlin-mode--token-type previous-token) + 'string-chunk-before-interpolated-expression) + nil) + + ;; Otherwise, inserts implicit semicolon. + (t t)))) + +(defun kotlin-mode--close-parenthesis-before-control-structure-body-p (token) + "Return t if TOKEN is close parenthesis before control structure body. + +Return t if TOKEN is the close-parenthesis of \"if (...)\", +\"while(...)\", or \"for (...)\", but not followed by \"{\". + +Return nil otherwise." + (and + (eq (kotlin-mode--token-type token) '\)) + (save-excursion + (goto-char (kotlin-mode--token-end token)) + (not (eq (kotlin-mode--token-type (kotlin-mode--forward-token-simple)) + '\{))) + (save-excursion + (goto-char (kotlin-mode--token-end token)) + (condition-case nil + (progn + (backward-list) + (let ((previous-token (kotlin-mode--backward-token-simple))) + (or + (member (kotlin-mode--token-text previous-token) '("if" "for")) + (and + (equal (kotlin-mode--token-text previous-token) "while") + (not (save-excursion (kotlin-mode--find-do-for-while))))))) + (scan-error + nil))))) + +(defun kotlin-mode--find-do-for-while () + "Return \"do\" token for \"while\" token. + +If \"while\" is not part of do-while, return nil. + +Assuming the point is before \"while\" token." + ;; Example: + ;; + ;; do + ;; while (false) + ;; print(1) + ;; while (false) + ;; print(2) + ;; + ;; is parsed as three statements: + ;; + ;; do while (false); + ;; print(1); + ;; while (false) print(2); + ;; + ;; So we don't need to worry about bare while-statement in body of do-while. + (let ((nesting-level 1) + previous-token) + (while (< 0 nesting-level) + (setq previous-token (kotlin-mode--backward-token-or-list t)) + (cond + ((equal (kotlin-mode--token-text previous-token) "while") + (setq nesting-level (1+ nesting-level))) + + ((equal (kotlin-mode--token-text previous-token) "do") + (setq nesting-level (1- nesting-level))) + + ((memq (kotlin-mode--token-type previous-token) '(outside-of-buffer {)) + ;; Unmatched + (setq nesting-level 0)))) + (and (equal (kotlin-mode--token-text previous-token) "do") + previous-token))) + +(defun kotlin-mode--constructor-parameter-clause-p () + "Return non-nil if the point is before parameters of constructor declaration." + ;; Examples: + ;; + ;; public constructor() {} + ;; + ;; public class Foo() {} + ;; public class Foo () {} + ;; public class Foo public constructor () + ;; public class Foo @AAA constructor () + (let ((previous-token (save-excursion (kotlin-mode--backward-token-simple)))) + (or + (equal (kotlin-mode--token-text previous-token) "constructor") + (save-excursion + (kotlin-mode--try-backward-type-parameters) + (and + (eq (kotlin-mode--token-type (kotlin-mode--backward-token-simple)) + 'atom) + (equal (kotlin-mode--token-text (kotlin-mode--backward-token-simple)) + "class")))))) + +(defun kotlin-mode--function-parameter-clause-p () + "Return non-nil if the point is before parameters of function declaration." + ;; Examples: + ;; + ;; fun foo() {} + ;; fun foo() {} + ;; fun (A).foo() {} + ;; fun A.B.C.foo() {} + ;; fun @AAA (A).foo() {} + ;; fun (A).foo() {} + ;; fun @AAA (A).foo() {} + ;; fun @AAA A.B.C.foo() {} + ;; + ;; fun () {} + ;; fun (A).() {} + ;; fun A.B.C.() {} + (let ((previous-token (save-excursion (kotlin-mode--backward-token-simple)))) + (cond + ;; fun () {} + ;; fun (A).() {} // for first parenthesis + ;; fun (A).foo() {} // for first parenthesis + ((equal (kotlin-mode--token-text previous-token) "fun") + t) + + ;; fun (A).() {} + ;; fun A.B.C.() {} + ((equal (kotlin-mode--token-text previous-token) ".") + (save-excursion + (and (kotlin-mode--try-backward-receiver-type) + (equal (kotlin-mode--token-text + (kotlin-mode--backward-token-simple)) + "fun")))) + + ;; fun @AAA (A).foo() {} // for first parenthesis + ;; fun (A).foo() {} // for first parenthesis + ;; fun @AAA (A).foo() {} // for first parenthesis + ((save-excursion + (kotlin-mode--try-backward-type-modifiers) + (kotlin-mode--try-backward-type-parameters) + (equal (kotlin-mode--token-text + (kotlin-mode--backward-token-simple)) + "fun")) + t) + + ;; fun foo() {} + ;; fun foo() {} + ;; fun (A).foo() {} // for second parenthesis + ;; fun A.B.C.foo() {} + ;; fun @AAA (A).foo() {} // for second parenthesis + ;; fun (A).foo() {} // for second parenthesis + ;; fun @AAA (A).foo() {} // for second parenthesis + ;; fun @AAA A.B.C.foo() {} + ((eq (kotlin-mode--token-type previous-token) 'atom) + (save-excursion + (goto-char (kotlin-mode--token-start previous-token)) + (kotlin-mode--try-backward-receiver-type) + (kotlin-mode--try-backward-type-parameters) + (equal (kotlin-mode--token-text + (kotlin-mode--backward-token-simple)) + "fun")))))) + +(defun kotlin-mode--try-backward-receiver-type () + "Try backward dot, type, and type modifiers. + +Return non-nil if succeeds. Keep position and return nil otherwise." + (let ((pos (point)) + (previous-token (kotlin-mode--backward-token-simple))) + (if (and (equal (kotlin-mode--token-text previous-token) ".") + (kotlin-mode--try-backward-type)) + (progn + (kotlin-mode--try-backward-type-modifiers) + t) + (goto-char pos) + nil))) + +(defun kotlin-mode--try-backward-type () + "Try backward type but not type modifiers. + +Return non-nil if succeeds. Keep position and return nil otherwise." + ;; type + ;; : typeModifiers? + ;; ( parenthesizedType + ;; | nullableType + ;; | typeReference + ;; | functionType) + ;; ; + ;; + ;; typeReference + ;; : userType + ;; | DYNAMIC + ;; ; + ;; + ;; userType + ;; : simpleUserType (NL* DOT NL* simpleUserType)* + ;; ; + ;; + ;; simpleUserType + ;; : simpleIdentifier (NL* typeArguments)? + ;; ; + ;; + ;; functionType + ;; : (receiverType NL* DOT NL*)? functionTypeParameters NL* ARROW NL* type + ;; ; + (let ((pos (point)) + (previous-token (kotlin-mode--backward-token-simple)) + result) + (setq + result + (cond + ;; parenthesizedType + ((equal (kotlin-mode--token-text previous-token) ")") + (goto-char (kotlin-mode--token-end previous-token)) + (condition-case nil + (progn + (backward-list) + t) + (scan-error + (goto-char pos) + nil))) + + ;; nullableType + ((equal (kotlin-mode--token-text previous-token) "?") + (kotlin-mode--try-backward-type)) + + ;; typeReference + (t + (goto-char (kotlin-mode--token-end previous-token)) + (kotlin-mode--try-backward-type-reference)))) + ;; functionType + (while (and result + (equal (kotlin-mode--token-text + (save-excursion (kotlin-mode--backward-token-simple))) + "->")) + ;; Skip "->" + (kotlin-mode--backward-token-simple) + ;; Skip parameters and a receiver type if any. + (setq result (condition-case nil + (progn + (backward-list) + (kotlin-mode--try-backward-receiver-type) + t) + (scan-error + nil)))) + (unless result + (goto-char pos)) + result)) + +(defun kotlin-mode--try-forward-type-reference () + "Try forward type reference. + +Return non-nil if succeeds. Keep position and return nil otherwise." + ;; typeReference + ;; : userType + ;; | DYNAMIC + ;; ; + ;; userType + ;; : simpleUserType (NL* DOT NL* simpleUserType)* + ;; ; + ;; + ;; simpleUserType + ;; : simpleIdentifier (NL* typeArguments)? + ;; ; + (let ((pos (point)) + result) + (setq result (kotlin-mode--forward-simple-user-type)) + (while (and result + (equal (kotlin-mode--token-text + (save-excursion (kotlin-mode--forward-token-simple))) + ".")) + ;; Skip "." + (kotlin-mode--forward-token-simple) + (setq result (kotlin-mode--forward-simple-user-type))) + (unless result + (goto-char pos)) + result)) + +(defun kotlin-mode--forward-simple-user-type () + "Forward simple user type. + +Return non-nil if succeeds. Otherwise, return nil and the point is undefined." + ;; simpleUserType + ;; : simpleIdentifier (NL* typeArguments)? + ;; ; + (let ((next-token (save-excursion (kotlin-mode--forward-token-simple)))) + (if (eq (kotlin-mode--token-type next-token) 'atom) + ;; Found simpleIdentifier + (progn + (goto-char (kotlin-mode--token-end next-token)) + (when (equal (kotlin-mode--token-text + (save-excursion (kotlin-mode--forward-token-simple))) + "<") + ;; Maybe type paramters + (kotlin-mode--try-forward-type-parameters)) + t) + ;; Not a simpleUserType + nil))) + +(defun kotlin-mode--try-backward-type-reference () + "Try backward type reference. + +Return non-nil if succeeds. Keep position and return nil otherwise." + ;; typeReference + ;; : userType + ;; | DYNAMIC + ;; ; + ;; userType + ;; : simpleUserType (NL* DOT NL* simpleUserType)* + ;; ; + ;; + ;; simpleUserType + ;; : simpleIdentifier (NL* typeArguments)? + ;; ; + (let ((pos (point)) + result) + (setq result (kotlin-mode--backward-simple-user-type)) + (while (and result + (equal (kotlin-mode--token-text + (save-excursion (kotlin-mode--backward-token-simple))) + ".")) + ;; Skip "." + (kotlin-mode--backward-token-simple) + (setq result (kotlin-mode--backward-simple-user-type))) + (unless result + (goto-char pos)) + result)) + +(defun kotlin-mode--backward-simple-user-type () + "Backward simple user type. + +Return non-nil if succeeds. Otherwise, return nil and the point is undefined." + ;; simpleUserType + ;; : simpleIdentifier (NL* typeArguments)? + ;; ; + (let ((pos (point)) + (previous-token (save-excursion (kotlin-mode--backward-token-simple)))) + (cond + ;; simpleUserType with type arguments + ((equal (kotlin-mode--token-text previous-token) ">") + (if (and (kotlin-mode--try-backward-type-parameters) + (eq (kotlin-mode--token-type + (kotlin-mode--backward-token-simple)) + 'atom)) + t + (goto-char pos) + nil)) + + ;; Start of an annotation + ((eq (kotlin-mode--token-type previous-token) 'annotation) + (goto-char (kotlin-mode--token-start previous-token)) + (forward-char) + t) + + ;; Other simpleUserType + ((eq (kotlin-mode--token-type previous-token) 'atom) + (goto-char (kotlin-mode--token-start previous-token)) + t) + + ;; Not a simpleUserType + (t + nil)))) + +(defun kotlin-mode--try-backward-type-modifiers () + "Try backward type modifiers. + +Keep position if failed." + (let ((previous-token (save-excursion (kotlin-mode--backward-token-simple)))) + (while (or + (eq (kotlin-mode--token-type previous-token) 'annotation) + (equal (kotlin-mode--token-text previous-token) "suspend")) + (goto-char (kotlin-mode--token-start previous-token)) + (setq previous-token + (save-excursion (kotlin-mode--backward-token-simple)))))) + +(defun kotlin-mode--multi-variable-declaration-p () + "Return non-nil if the point is before multi variable declaration." + ;; Example: + ;; + ;; val (x, y) = foo() + ;; var (x, y) = foo() + ;; val (x, y) = foo() + ;; val (A).(x, y) = foo() + ;; val (A).(x, y) = foo() + ;; val @AAA A.(x, y) = foo() + ;; val @AAA A.B.C.(x, y) = foo() + (save-excursion + (kotlin-mode--try-backward-receiver-type) + (kotlin-mode--try-backward-type-parameters) + (member (kotlin-mode--token-text + (kotlin-mode--backward-token-simple)) + '("val" "var")))) + +(defun kotlin-mode--constructor-delegation-call-p () + "Return non-nil if the point is before constructor delegation call arguments." + ;; Example: + ;; + ;; constructor (): this(1) + ;; constructor (): super(1) + (save-excursion + (and (member (kotlin-mode--token-text (kotlin-mode--backward-token-simple)) + '("this" "super")) + (eq (kotlin-mode--token-type (kotlin-mode--backward-token-simple)) + ':)))) + +(defun kotlin-mode--try-forward-modifiers () + "Try forward modifiers. + +Keep position if failed." + (let ((next-token (save-excursion (kotlin-mode--forward-token-simple)))) + (while (or + (eq (kotlin-mode--token-type next-token) 'annotation) + (member (kotlin-mode--token-text next-token) + kotlin-mode--modifier-keywords)) + (goto-char (kotlin-mode--token-end next-token)) + (setq next-token (save-excursion (kotlin-mode--forward-token-simple)))))) + +(defun kotlin-mode--try-backward-modifiers () + "Try forward modifiers. + +Keep position if failed." + (let ((previous-token (save-excursion (kotlin-mode--backward-token-simple)))) + (while (or + (eq (kotlin-mode--token-type previous-token) 'annotation) + (member (kotlin-mode--token-text previous-token) + kotlin-mode--modifier-keywords)) + (goto-char (kotlin-mode--token-start previous-token)) + (setq previous-token + (save-excursion (kotlin-mode--backward-token-simple)))))) + +(defun kotlin-mode--backward-token-or-list (&optional use-backward-token-simple) + "Move point to the start position of the previous token or list. + +Return the token skipped. + +If USE-BACKWARD-TOKEN-SIMPLE is non-nil, use +`kotlin-mode--backward-token-simple' instead of +`kotlin-mode--backward-token'." + (let* ((previous-token (if use-backward-token-simple + (kotlin-mode--backward-token-simple) + (kotlin-mode--backward-token))) + (previous-type (kotlin-mode--token-type previous-token)) + (previous-text (kotlin-mode--token-text previous-token)) + (previous-start (kotlin-mode--token-start previous-token)) + (previous-end (kotlin-mode--token-end previous-token))) + (cond + ;; Maybe list + ((memq previous-type '(} \) \] \)-before-control-structure-body)) + (goto-char previous-end) + (condition-case nil + (progn + (backward-list) + (make-instance + 'kotlin-mode--token + :type (assoc-default + previous-type + '((} . {}) + (\) . \(\)) + (\)-before-control-structure-body . + \(\)-before-control-structure-body) + (\] . \[\]))) + :text (buffer-substring-no-properties (point) previous-end) + :start (point) + :end previous-end)) + (scan-error + (goto-char previous-start) + previous-token))) + + ;; Maybe type parameter list + ((equal previous-text ">") + (goto-char previous-end) + (if (kotlin-mode--try-backward-type-parameters) + (make-instance + 'kotlin-mode--token + :type '<> + :text (buffer-substring-no-properties (point) previous-end) + :start (point) + :end previous-end) + (goto-char previous-start) + previous-token)) + + ;; Other token + (t previous-token)))) + +(defun kotlin-mode--forward-token-or-list (&optional use-forward-token-simple) + "Move point to the end position of the next token or list. + +Return the token skipped. + +If USE-FORWARD-TOKEN-SIMPLE is non-nil, use +`kotlin-mode--forward-token-simple' instead of +`kotlin-mode--forward-token'." + (let* ((next-token (if use-forward-token-simple + (kotlin-mode--forward-token-simple) + (kotlin-mode--forward-token))) + (next-type (kotlin-mode--token-type next-token)) + (next-text (kotlin-mode--token-text next-token)) + (next-start (kotlin-mode--token-start next-token)) + (next-end (kotlin-mode--token-end next-token)) + result-type) + (cond + ;; Maybe list + ((memq next-type '({ \( \[)) + (goto-char next-start) + (condition-case nil + (progn + (forward-list) + (setq + result-type + (if (kotlin-mode--close-parenthesis-before-control-structure-body-p + (save-excursion (kotlin-mode--backward-token-simple))) + '\(\)-before-control-structure-body + (assoc-default next-type '(({ . {}) + (\( . \(\)) + (\[ . \[\]))))) + (make-instance + 'kotlin-mode--token + :type result-type + :text (buffer-substring-no-properties next-start (point)) + :start next-start + :end (point))) + (scan-error + (goto-char next-end) + next-token))) + + ;; Maybe type parameter list + ((equal next-text "<") + (goto-char next-start) + (if (kotlin-mode--try-forward-type-parameters) + (make-instance + 'kotlin-mode--token + :type '<> + :text (buffer-substring-no-properties next-start (point)) + :start next-start + :end (point)) + (goto-char next-end) + next-token)) + + ;; Other token + (t next-token)))) + +(defun kotlin-mode--try-backward-type-parameters () + "Move point to the start of the type parameter list. + +Return non-nil if succeeds. Keep position and return nil otherwise. + +It is a type parameter list if: +- it has matching angle brackets, and +- it does not have tokens that cannot appears in a type parameter list." + (let ((pos (point))) + (if (and + (equal (kotlin-mode--token-text (kotlin-mode--backward-token-simple)) + ">") + (kotlin-mode--try-skip-type-parameters + (lambda () (kotlin-mode--backward-token-or-list t)) + "<" ">")) + t + (goto-char pos) + nil))) + +(defun kotlin-mode--try-forward-type-parameters () + "Move point to the end of the type parameter list. + +Return non-nil if succeeds. Keep position and return nil otherwise. + +It is a type parameter list if: +- it has matching angle brackets, and +- it does not have tokens that cannot appears in a type parameter list." + (let ((pos (point))) + (if (and + (equal (kotlin-mode--token-text (kotlin-mode--forward-token-simple)) + "<") + (kotlin-mode--try-skip-type-parameters + (lambda () (kotlin-mode--forward-token-or-list t)) + ">" "<")) + t + (goto-char pos) + nil))) + +(defconst kotlin-mode--tokens-not-in-type-parameter-list + ;; Whitelisting tend to be fragile. So we list tokens that are + ;; unlikely to appear in type parameter lists in the current + ;; version and future ones. + ;; + ;; Example of type parameters: + ;; < + ;; A: B, + ;; @AAA(aaa) reified in C: @AAA suspend D.(x: Int) -> (X)? + ;; > + ;; + ;; Example of type arguments + ;; < + ;; A, + ;; @AAA(aaa) in *, + ;; suspend D.(x: Int) -> (X)? + ;; > + ;; + ;; We don't need to consider the contents of inner brackets because + ;; they are skipped by `kotlin-mode--backward-token-or-list'. + ;; + ;; String literals and numbers are also excluded by + ;; `kotlin-mode--try-skip-type-parameters'. + `(outside-of-buffer + \; + { } \( \) \[ \] + "%" "/" "+" "-" + "++" "--" + "&&" "||" + "!==" "!=" + ;; exclude "in" + "is" "!is" "!in" "as" "as?" + "!" + "=" "+=" "-=" "*=" "/=" "%=" + ".." "..<" "::" "?:" "?." "<=" ">=" "==" "===" + "package" "import" "class" "interface" "fun" "object" "val" "var" + "typealias" "constructor" "by" "companion" "init" "if" "else" "when" + "try" "catch" "finally" "for" "do" "while" "throw" + return continue break "true" "false" "null" + ,@(remove "suspend" kotlin-mode--modifier-keywords))) + +(defun kotlin-mode--try-skip-type-parameters + (skip-token-or-list-function matching-bracket-text unmatching-bracket-text) + "Skip type parameters if the point is just before/after one. + +Return non-nil if succeeds. Keep position and return nil otherwise. + +Assuming open/close bracket is already skipped. + +SKIP-TOKEN-OR-LIST-FUNCTION is a function skipping forward/backward a +token or a list. +MATCHING-BRACKET-TEXT is a text of the matching bracket. +UNMATCHING-BRACKET-TEXT is a text of the current bracket." + (let ((pos (point)) + result + (prohibited-tokens (cons + unmatching-bracket-text + kotlin-mode--tokens-not-in-type-parameter-list)) + (next-token (funcall skip-token-or-list-function))) + (while + (cond + ;; Prohibited tokens + ((or + (memq (kotlin-mode--token-type next-token) prohibited-tokens) + (member (kotlin-mode--token-text next-token) prohibited-tokens) + (string-match-p "^[\"'0-9]" (kotlin-mode--token-text next-token))) + ;; Not a type parameter list. + ;; Return to the initial position and stop the loop. + (goto-char pos) + (setq result nil) + nil) + + ;; Matching bracket + ((equal (kotlin-mode--token-text next-token) matching-bracket-text) + ;; Found the matching open angle bracket. Stop the loop. + (setq result t) + nil) + + ;; Otherwise, keep scanning + (t t)) + (setq next-token (funcall skip-token-or-list-function))) + result)) + +(defun kotlin-mode--forward-token () + "Move point forward to the next position of the end of a token. + +Return the token object. If no more tokens available, return a token with +type `outside-of-buffer'." + (let ((pos (point))) + ;; Skip comments and whitespaces. + (let ((chunk (kotlin-mode--chunk-after))) + ;; If point is inside a comment, go back to the beginning of the + ;; comment before skipping it. + (when (kotlin-mode--chunk-comment-p chunk) + (goto-char (kotlin-mode--chunk-start chunk)))) + (forward-comment (point-max)) + (cond + ;; Outside of buffer + ((eobp) + (make-instance 'kotlin-mode--token + :type 'outside-of-buffer + :text "" + :start (point) + :end (point))) + + ;; Implicit semicolon + ((and + ;; Check (forward-comment (point-max)) skipped a newline. + (< pos + (save-excursion + (kotlin-mode--goto-non-comment-bol) + (point))) + (save-excursion (goto-char pos) (kotlin-mode--implicit-semi-p))) + (make-instance 'kotlin-mode--token + :type 'implicit-\; + :text (buffer-substring-no-properties pos (point)) + :start pos + :end (point))) + + (t + (let ((token (kotlin-mode--extend-annotation-token-forward + (kotlin-mode--forward-token-simple)))) + (cond + ;; Close parenthesis of "if ()", "while ()", or "for ()" + ((kotlin-mode--close-parenthesis-before-control-structure-body-p token) + (make-instance 'kotlin-mode--token + :type '\)-before-control-structure-body + :text (kotlin-mode--token-text token) + :start (kotlin-mode--token-start token) + :end (kotlin-mode--token-end token))) + + ;; "else" not followed by "if" on the same line or "{" + ((kotlin-mode--bare-else-p token) + (make-instance 'kotlin-mode--token + :type 'bare-else + :text (kotlin-mode--token-text token) + :start (kotlin-mode--token-start token) + :end (kotlin-mode--token-end token))) + + ;; super@label + ((save-excursion + (and (equal (kotlin-mode--token-text token) "super") + (eq (char-after) ?<) + (kotlin-mode--try-forward-type-parameters) + (eq (char-after) ?@))) + (kotlin-mode--try-forward-type-parameters) + (forward-char) + (skip-syntax-forward "w") + (make-instance 'kotlin-mode--token + :type 'super + :text (buffer-substring-no-properties + (kotlin-mode--token-start token) + (point)) + :start (kotlin-mode--token-start token) + :end (point))) + + ;; "->" of lambda parameters + ((kotlin-mode--anonymous-function-parameter-arrow-p token) + (make-instance 'kotlin-mode--token + :type 'anonymous-function-parameter-arrow + :text (kotlin-mode--token-text token) + :start (kotlin-mode--token-start token) + :end (kotlin-mode--token-end token))) + + ;; "->" of when expression + ((kotlin-mode--when-expression-arrow-p token) + (make-instance 'kotlin-mode--token + :type 'when-expression-arrow + :text (kotlin-mode--token-text token) + :start (kotlin-mode--token-start token) + :end (kotlin-mode--token-end token))) + + (t token))))))) + +(defun kotlin-mode--bare-else-p (token) + "Return non-nil if TOKEN is bare \"else\" token. + +A bare \"else\" token is a \"else\" token not followed by \"if\" on +the same line, \"->\", or \"{\"." + (let ((next-token (save-excursion + (goto-char (kotlin-mode--token-end token)) + (kotlin-mode--forward-token-simple)))) + (and (equal (kotlin-mode--token-text token) "else") + (not (eq (kotlin-mode--token-type next-token) '{)) + (not (eq (kotlin-mode--token-text next-token) "->")) + (not (and + (equal (kotlin-mode--token-text next-token) "if") + (save-excursion + (= (progn + (goto-char (kotlin-mode--token-start next-token)) + (kotlin-mode--goto-non-comment-bol) + (point)) + (progn + (goto-char (kotlin-mode--token-start token)) + (kotlin-mode--goto-non-comment-bol) + (point))))))))) + +(defun kotlin-mode--anonymous-function-parameter-arrow-p (token) + "Return non-nil if TOKEN is an arrow of lambda parameters." + ;; Examples: + ;; + ;; { -> 1} + ;; { x -> 1 } + ;; { f: (Int) -> Int, x: Int -> f(x) } + ;; { f: (Int) -> Int -> f(1) } + ;; { f: (Int) -> (Int) -> Int -> f(1) } + ;; { (x: Int, y: Int) -> x + y } + ;; + ;; If an arrow is not preceded by a close parenthesis, it is an end + ;; of parameters. + ;; + ;; If an arrow is preceded by a close parenthesis and the + ;; parentheses are preceded by an open curly bracket, the arrow is + ;; an end of parameters. + ;; + ;; Otherwise, it is an arrow in function types. + ;; + ;; Furthermore, the curly open bracket should not be preceded by + ;; "when ()". + (and (equal (kotlin-mode--token-text token) "->") + (save-excursion + (goto-char (kotlin-mode--token-start token)) + (forward-comment (- (point))) + (or + ;; The arrow is not preceded by a close parenthesis. + ;; + ;; Examples: + ;; { -> 1} + ;; { x -> 1 } + ;; { f: (Int) -> Int, x: Int -> f(x) } // last arrow + ;; { f: (Int) -> Int -> f(1) } // last arrow + ;; { f: (Int) -> (Int) -> Int -> f(1) } // last arrow + (not (eq (char-before) ?\))) + ;; The parentheses are preceded by an open curly bracket. + ;; + ;; Example: + ;; { (x: Int, y: Int) -> x + y } + (condition-case nil + (progn (backward-list) + (forward-comment (- (point))) + (eq (char-before) ?{)) + (scan-error nil)))) + (save-excursion + ;; The token is inside a curly brackets but not inside a + ;; when-expression. + (goto-char (kotlin-mode--token-start token)) + (let ((containing-bracket + (kotlin-mode--find-containing-brackets (point)))) + (and (eq (car containing-bracket) '{) + (not (kotlin-mode--inside-when-expression-p + containing-bracket))))))) + +(defun kotlin-mode--when-expression-arrow-p (token) + "Return non-nil if TOKEN is an arrow of when expression." + ;; This function does not distinguish arrows of when expression and + ;; ones of function types. + (and (equal (kotlin-mode--token-text token) "->") + (save-excursion + (goto-char (kotlin-mode--token-start token)) + (kotlin-mode--inside-when-expression-p)))) + +(defun kotlin-mode--inside-when-expression-p (&optional containing-bracket) + "Return non-nil if point is inside a when expression. + +CONTAINING-BRACKET is a return value of +`kotlin-mode--find-containing-brackets'. If ommitted, +\(kotlin-mode--find-containing-brackets (point)) is used." + (unless containing-bracket + (setq containing-bracket (kotlin-mode--find-containing-brackets (point)))) + (goto-char (cdr containing-bracket)) + (and (eq (car containing-bracket) '{) + (eq (kotlin-mode--token-type (kotlin-mode--backward-token-or-list t)) + '\(\)) + (equal (kotlin-mode--token-text (kotlin-mode--backward-token-simple)) + "when"))) + +(defun kotlin-mode--forward-token-simple () + "Like `kotlin-mode--forward-token' without recursion. + +This function does not return `implicit-;'." + (forward-comment (point-max)) + (cond + ;; Outside of buffer + ((eobp) + (make-instance 'kotlin-mode--token + :type 'outside-of-buffer + :text "" + :start (point) + :end (point))) + + ;; End of template expression + ((and (eq (char-after) ?\}) + (equal (get-text-property (point) 'syntax-table) + (string-to-syntax "|"))) + (let ((pos-after-comment (point))) + (kotlin-mode--forward-string-chunk) + (make-instance + 'kotlin-mode--token + :type 'string-chunk-after-template-expression + :text (buffer-substring-no-properties pos-after-comment (point)) + :start pos-after-comment + :end (point)))) + + ;; :: + ((looking-at "::") + (forward-char 2) + (make-instance 'kotlin-mode--token + :type 'operator + :text "::" + :start (- (point) 2) + :end (point))) + + ;; Separators and parentheses + ((memq (char-after) '(?, ?\; ?\{ ?\} ?\[ ?\] ?\( ?\) ?:)) + (forward-char) + (make-instance 'kotlin-mode--token + :type (intern (string (char-before))) + :text (string (char-before)) + :start (1- (point)) + :end (point))) + + ;; Open angle bracket for type parameters or type arguments + ;; + ;; We use a heuristic: spaces are inserted around inequality sign, but not + ;; for angle bracket, and a type parameter starts with upper case + ;; characters, parentheses, asterisk, keyword 'reified', keyword 'in', + ;; keyword 'out', keyword 'suspend', or annotations. + ((and (eq (char-after) ?<) + (looking-at + (rx (seq "<" (or (any "_(@[*" upper) + (seq (or "reified" "in" "out" "suspend") + word-end)))))) + (forward-char) + (make-instance 'kotlin-mode--token + :type '< + :text "<" + :start (1- (point)) + :end (point))) + + ;; Close angle bracket for type parameters or type arguments + ;; + ;; Close angle bracket follows identifiers, parentheses, question symbols, + ;; or other angle brackets (e.g. Foo>) + ((and (eq (char-after) ?>) + (save-excursion + ;; You know that regular languages can be reversed. Thus you may + ;; think that `looking-back' reverses the given regexp and scans + ;; chars backwards. Nevertheless, `looking-back' function does not + ;; do that. It just repeats `looking-at' with decrementing start + ;; position until it succeeds. The document says that it is not + ;; recommended to use. So we use combination of + ;; `skip-chars-backward', `skip-syntax-backward', and + ;; `looking-at' here. + (skip-chars-backward "?)>") + (skip-syntax-backward "w") + (looking-at "[[:upper:]_]"))) + (forward-char) + (make-instance 'kotlin-mode--token + :type '> + :text ">" + :start (1- (point)) + :end (point))) + + ;; Operator (other than as, in, or is) + ((looking-at + (rx + (or + "++" "--" + "&&" "||" + "!==" "!=" + (seq "!is" word-end) (seq "!in" word-end) "as?" + "!" + "->" + "..<" ".." "." "?:" "?." "?" "<" ">" "<=" ">=" "===" "==" + "=" "+=" "-=" "*=" "/=" "%=" + "*" "%" "/" "+" "-" "&"))) + (let ((text (match-string-no-properties 0)) + (start (match-beginning 0)) + (end (match-end 0))) + (goto-char end) + (make-instance 'kotlin-mode--token + :type 'operator + :text text + :start start + :end end))) + + ;; Backquoted identifier or character literal + ((memq (char-after) '(?` ?')) + (let ((pos-after-comment (point))) + (kotlin-mode--forward-string-chunk) + (make-instance + 'kotlin-mode--token + :type 'atom + :text (buffer-substring-no-properties pos-after-comment (point)) + :start pos-after-comment + :end (point)))) + + ;; String + ((looking-at "\"") + (let ((pos-after-comment (point))) + (forward-char) + (kotlin-mode--end-of-string) + (make-instance + 'kotlin-mode--token + :type 'atom + :text (buffer-substring-no-properties pos-after-comment (point)) + :start pos-after-comment + :end (point)))) + + ;; Part of annotations. Only @ and first identifier. + ;; + ;; This will be augmented by + ;; `kotlin-mode--extend-annotation-token-forward' later. + ((eq (char-after) ?@) + (let ((pos-after-comment (point))) + (forward-char) + (if (eq (char-after) ?`) + (kotlin-mode--forward-string-chunk) + (skip-syntax-forward "w")) + ;; Strictly speaking, single at sign is not an annotation, but + ;; we treat it as an annotation. + (make-instance + 'kotlin-mode--token + :type 'annotation + :text (buffer-substring-no-properties pos-after-comment (point)) + :start pos-after-comment + :end (point)))) + + ;; Other tokens including identifiers, keywords, labels, and numbers. + ;; + ;; Note that we parse 123.456e+2 as "123" "." "456e" "+" "2". + (t + (let* ((pos-after-comment (point)) + (text + (cond + ;; return@label, continue@label, break@label, this@label, + ;; or super@label. + ((looking-at (rx (or "return@" + "continue@" + "break@" + "this@" + "super@"))) + (skip-syntax-forward "w") + (forward-char) + (skip-syntax-forward "w") + (buffer-substring-no-properties pos-after-comment (point))) + + ;; Identifiers, keywords, labels, or numbers + ((eq (syntax-class (syntax-after (point))) + (syntax-class (string-to-syntax "w"))) + (skip-syntax-forward "w") + ;; Skip an at sign if exists for label. + (when (eq (char-after) ?@) + (forward-char)) + (buffer-substring-no-properties pos-after-comment (point))) + ;; Unknown character type. Treats as a single-letter token. + (t + (forward-char) + (string (char-before))))) + (type (cond + ((member text '("is" "in" "as")) + ;; Note that "as?" is already handled. + 'operator) + + ((eq (aref text (1- (length text))) ?@) + 'label) + + ((string-match "^return\\>" text) + 'return) + + ((string-match "^continue\\>" text) + 'return) + + ((string-match "^break\\>" text) + 'break) + + ((string-match "^this\\>" text) + 'this) + + ((string-match "^super\\>" text) + 'super) + + (t + 'atom)))) + (make-instance 'kotlin-mode--token + :type type + :text text + :start (- (point) (length text)) + :end (point)))))) + +(defun kotlin-mode--extend-annotation-token-forward (token) + "Return annotation token if TOKEN is the start of an annotation. + +TOKEN is assumed to be a return value of `kotlin-mode--forward-token-simple'." + (if (eq (kotlin-mode--token-type token) 'annotation) + (let ((pos (point))) + (cond + ;; Example: @file:Abc(def) + ;; Example: @file:[Abc Def Ghi] + ((save-excursion + (forward-comment (point-max)) + (eq (char-after) ?:)) + (forward-comment (point-max)) + (forward-char) + (unless (or (kotlin-mode--try-forward-multi-annotation-bracket) + (kotlin-mode--try-forward-unescaped-annotation)) + (goto-char pos))) + + ;; Example: @[Abc Def Ghi] + ((equal (kotlin-mode--token-text token) "@") + (kotlin-mode--try-forward-multi-annotation-bracket)) + + ;; Example: @Abc(def) + (t + (goto-char (1+ (kotlin-mode--token-start token))) + (kotlin-mode--try-forward-unescaped-annotation))) + (make-instance 'kotlin-mode--token + :type 'annotation + :text (buffer-substring-no-properties + (kotlin-mode--token-start token) + (point)) + :start (kotlin-mode--token-start token) + :end (point))) + token)) + +(defun kotlin-mode--try-forward-multi-annotation-bracket () + "Move point forward to the end of square brackets. + +Return non-nil if succeeds. Keep position and return nil otherwise." + (let ((pos (point))) + (when (save-excursion + (forward-comment (point-max)) + (eq (char-after) ?\[)) + (condition-case nil + (progn + (forward-list) + t) + (scan-error + (goto-char pos) + nil))))) + +(defun kotlin-mode--try-forward-unescaped-annotation () + "Move point forward to the end of unescapedAnnotation. + +Skip userType and constructor arguments if exists. + +Return non-nil if succeeds. Keep position and return nil otherwise." + (if (kotlin-mode--try-forward-type-reference) + (let ((pos (point))) + (when (save-excursion + ;; Line breaks are prohibited before bracket. + (kotlin-mode--forward-comments-but-not-line-breaks) + (eq (char-after) ?\()) + (condition-case nil + (forward-list) + (scan-error + (goto-char pos)))) + t) + nil)) + +(defun kotlin-mode--backward-token () + "Move point backward to the previous position of the end of a token. + +Return the token object. If no more tokens available, return a token with +type `outside-of-buffer'." + (let ((pos (point))) + ;; Skip comments and whitespaces. + (let ((chunk (kotlin-mode--chunk-after))) + (when (kotlin-mode--chunk-comment-p chunk) + (goto-char (kotlin-mode--chunk-start chunk)))) + (forward-comment (- (point))) + (cond + ;; Outside of buffer + ((bobp) + (make-instance 'kotlin-mode--token + :type 'outside-of-buffer + :text "" + :start (point) + :end (point))) + + ;; Implicit semicolon + ((and + ;; Check (forward-comment (- (point))) skipped a newline. + (< (save-excursion + (kotlin-mode--goto-non-comment-eol) + (point)) + pos) + (save-excursion (goto-char pos) (kotlin-mode--implicit-semi-p))) + (make-instance 'kotlin-mode--token + :type 'implicit-\; + :text (buffer-substring-no-properties (point) pos) + :start (point) + :end pos)) + + (t + (let ((token (kotlin-mode--extend-annotation-token-backward + (kotlin-mode--backward-token-simple)))) + (cond + ;; Close parenthesis of "if ()", "while ()", or "for ()" + ((kotlin-mode--close-parenthesis-before-control-structure-body-p token) + (make-instance 'kotlin-mode--token + :type '\)-before-control-structure-body + :text (kotlin-mode--token-text token) + :start (kotlin-mode--token-start token) + :end (kotlin-mode--token-end token))) + + ;; "else" not followed by "if" on the same line or "{" + ((kotlin-mode--bare-else-p token) + (make-instance 'kotlin-mode--token + :type 'bare-else + :text (kotlin-mode--token-text token) + :start (kotlin-mode--token-start token) + :end (kotlin-mode--token-end token))) + + ;; super@label + ((save-excursion + (and (eq (kotlin-mode--token-type token) 'annotation) + (eq (char-before) ?>) + (kotlin-mode--try-backward-type-parameters) + (equal (kotlin-mode--token-text + (kotlin-mode--backward-token-simple)) + "super"))) + (kotlin-mode--try-backward-type-parameters) + (kotlin-mode--backward-token-simple) + (make-instance 'kotlin-mode--token + :type 'super + :text (buffer-substring-no-properties + (point) + (kotlin-mode--token-end token)) + :start (point) + :end (kotlin-mode--token-end token))) + + ;; "->" of lambda parameters + ((kotlin-mode--anonymous-function-parameter-arrow-p token) + (make-instance 'kotlin-mode--token + :type 'anonymous-function-parameter-arrow + :text (kotlin-mode--token-text token) + :start (kotlin-mode--token-start token) + :end (kotlin-mode--token-end token))) + + ;; "->" of when expression + ((kotlin-mode--when-expression-arrow-p token) + (make-instance 'kotlin-mode--token + :type 'when-expression-arrow + :text (kotlin-mode--token-text token) + :start (kotlin-mode--token-start token) + :end (kotlin-mode--token-end token))) + + (t token))))))) + +(defun kotlin-mode--backward-token-simple () + "Like `kotlin-mode--backward-token' without recursion. + +This function does not return `implicit-;'" + (forward-comment (- (point))) + (cond + ;; Outside of buffer + ((bobp) + (make-instance 'kotlin-mode--token + :type 'outside-of-buffer + :text "" + :start (point) + :end (point))) + + ;; Beginning of template expression + ((and (eq (char-before) ?\{) + (equal (get-text-property (1- (point)) 'syntax-table) + (string-to-syntax "|"))) + (let ((pos-before-comment (point))) + (kotlin-mode--backward-string-chunk) + (make-instance + 'kotlin-mode--token + :type 'string-chunk-before-template-expression + :text (buffer-substring-no-properties (point) pos-before-comment) + :start (point) + :end pos-before-comment))) + + ;; ::, ?: + ((member (buffer-substring-no-properties + (max (point-min) (- (point) 2)) + (point)) + '("::" "?:")) + (backward-char 2) + (make-instance 'kotlin-mode--token + :type 'operator + :text (buffer-substring-no-properties (point) (+ 2 (point))) + :start (point) + :end (+ 2 (point)))) + + ;; Separators and parentheses + ((memq (char-before) '(?, ?\; ?\{ ?\} ?\[ ?\] ?\( ?\) ?:)) + (backward-char) + (make-instance 'kotlin-mode--token + :type (intern (string (char-after))) + :text (string (char-after)) + :start (point) + :end (1+ (point)))) + + ;; Operator (3 letters) + ((member (buffer-substring-no-properties + (max (point-min) (- (point) 3)) + (point)) + '("===" "!==" "!is" "!in" "..<")) + (backward-char 3) + (make-instance 'kotlin-mode--token + :type 'operator + :text (buffer-substring-no-properties (point) (+ 3 (point))) + :start (point) + :end (+ 3 (point)))) + + ;; Operator (2 letters, other than as, in, or is) + ((member (buffer-substring-no-properties + (max (point-min) (- (point) 2)) + (point)) + '("++" "--" + "&&" "||" + "+=" "-=" "*=" "/=" "%=" + ".." "?." + "<=" ">=" "!=" "==" + "->")) + (backward-char 2) + (make-instance 'kotlin-mode--token + :type 'operator + :text (buffer-substring-no-properties (point) (+ 2 (point))) + :start (point) + :end (+ 2 (point)))) + + ;; Open angle bracket for type parameters + ;; + ;; We use a heuristic: spaces are inserted around inequality sign, + ;; but not for angle bracket, and a type parameter starts with an + ;; upper case characters, parenthesis, or keyword 'reified', + ;; keyword 'in', keyword 'out', keyword 'suspend', or annotations. + ((and (eq (char-before) ?<) + (looking-at + (rx (or (any "_(@[*" upper) + (seq (or "reified" "in" "out") + word-end))))) + (backward-char) + (make-instance 'kotlin-mode--token + :type '< + :text "<" + :start (point) + :end (1+ (point)))) + + ;; Close angle bracket for type parameters + ;; + ;; Close angle bracket follows identifier, parentheses, question symbols, + ;; or other angle brackets (e.g. Foo>) + ((and (eq (char-before) ?>) + (save-excursion + (skip-chars-backward "?)>") + (skip-syntax-backward "w") + (looking-at "[[:upper:]_]"))) + (backward-char) + (make-instance 'kotlin-mode--token + :type '> + :text ">" + :start (point) + :end (1+ (point)))) + + ;; ? or as? + ((eq (char-before) ??) + (let ((pos-before-comment (point))) + (backward-char) + (skip-syntax-backward "w") + (unless (looking-at (rx "as?")) + (goto-char pos-before-comment) + (backward-char)) + (make-instance + 'kotlin-mode--token + :type 'operator + :text (buffer-substring-no-properties (point) pos-before-comment) + :start (point) + :end pos-before-comment))) + + ;; Operator (1 letter) + ((member (buffer-substring-no-properties + (max (point-min) (- (point) 1)) + (point)) + '("*" "%" "/" "+" "-" "!" "=" "." "?" "<" ">" "&")) + (backward-char) + (make-instance 'kotlin-mode--token + :type 'operator + :text (buffer-substring-no-properties (point) (1+ (point))) + :start (point) + :end (1+ (point)))) + + ;; Backquoted identifier + ((eq (char-before) ?`) + (let ((pos-before-comment (point))) + (kotlin-mode--backward-string-chunk) + (when (eq (char-before) ?@) + (backward-char)) + (make-instance + 'kotlin-mode--token + :type (if (eq (char-after) ?@) 'annotation 'atom) + :text (buffer-substring-no-properties (point) pos-before-comment) + :start (point) + :end pos-before-comment))) + + ;; character literal + ((eq (char-before) ?') + (let ((pos-before-comment (point))) + (kotlin-mode--backward-string-chunk) + (make-instance + 'kotlin-mode--token + :type 'atom + :text (buffer-substring-no-properties (point) pos-before-comment) + :start (point) + :end pos-before-comment))) + + ;; String + ((eq (char-before) ?\") + (let ((pos-before-comment (point))) + (backward-char) + (kotlin-mode--beginning-of-string) + (make-instance + 'kotlin-mode--token + :type 'atom + :text (buffer-substring-no-properties (point) pos-before-comment) + :start (point) + :end pos-before-comment))) + + ;; Other tokens including identifiers, keywords, labels, numbers, + ;; and annotations without bracket. + ;; + ;; Note that we parse 123.456e+2 as "123" "." "456e" "+" "2". + (t + (let* ((pos-before-comment (point)) + (text + (cond + ;; Labels (and single at sign) + ((eq (char-before) ?@) + (backward-char) + (skip-syntax-backward "w") + (buffer-substring-no-properties (point) pos-before-comment)) + + ;; Identifiers, keywords, numbers, (part of) annotations + ((eq (syntax-class (syntax-after (1- (point)))) + (syntax-class (string-to-syntax "w"))) + (skip-syntax-backward "w") + (when (eq (char-before) ?@) + ;; Annotation, return@ or something like it. + (backward-char) + (let ((pos (point))) + (skip-syntax-backward "w") + (unless (looking-at + (rx (or "return" + "continue" + "break" + "this" + "super"))) + (goto-char pos)))) + (buffer-substring-no-properties (point) pos-before-comment)) + + ;; Unknown character type. Treats as a single-letter token. + (t (backward-char) (string (char-after))))) + (type (cond + ((member text '("as" "in" "is")) + 'operator) + + ((string-prefix-p "@" text) + ;; Strictly speaking, single at sign is not an + ;; annotation, but we treat it as an annotation. + 'annotation) + + ((string-match "^return\\>" text) + 'return) + + ((string-match "^continue\\>" text) + 'return) + + ((string-match "^break\\>" text) + 'break) + + ((string-match "^this\\>" text) + 'this) + + ((string-match "^super\\>" text) + 'super) + + ((eq (aref text (1- (length text))) ?@) + 'label) + + (t + 'atom)))) + (make-instance 'kotlin-mode--token + :type type + :text text + :start (point) + :end (+ (point) (length text))))))) + +(defun kotlin-mode--extend-annotation-token-backward (token) + "Return annotation token if TOKEN is the end of an annotation. + +TOKEN is assumed to be a return value of `kotlin-mode--backward-token-simple'." + (let ((pos (point))) + (goto-char (kotlin-mode--token-end token)) + (cond + ;; Example: @[Abc Def Ghi] + ;; Example: @file:[Abc Def Ghi] + ((eq (char-before) ?\]) + (condition-case nil + (progn + (backward-list) + (forward-comment (- (point))) + (if (or (and (eq (char-before) ?@) (prog1 t (backward-char))) + (kotlin-mode--try-backward-annotation-use-site-target)) + (make-instance 'kotlin-mode--token + :type 'annotation + :text (buffer-substring-no-properties + (point) + (kotlin-mode--token-end token)) + :start (point) + :end (kotlin-mode--token-end token)) + ;; Not an annotation. + (goto-char pos) + token)) + (scan-error + ;; Not an annotation. + (goto-char pos) + token))) + + ;; Example: @Abc(def) + ;; Example: @file:Abc(def) + ;; Example: @Abc + ;; Example: @file:Abc + ;; Example: @Abc + ((or (memq (char-before) '(?\) ?` ?>)) + (eq (syntax-class (syntax-after (1- (point)))) + (syntax-class (string-to-syntax "w")))) + (when (eq (char-before) ?\)) + (condition-case nil + (progn + (backward-list) + ;; Line breaks are prohibited before bracket. + (kotlin-mode--backward-comments-but-not-line-breaks) + (unless (or (memq (char-before) '(?` ?>)) + (eq (syntax-class (syntax-after (1- (point)))) + (syntax-class (string-to-syntax "w")))) + (goto-char pos))) + (scan-error + (goto-char pos)))) + (if (and + (kotlin-mode--try-backward-type-reference) + (or (and (eq (char-before) ?@) (prog1 t (backward-char))) + (kotlin-mode--try-backward-annotation-use-site-target))) + (make-instance 'kotlin-mode--token + :type 'annotation + :text (buffer-substring-no-properties + (point) + (kotlin-mode--token-end token)) + :start (point) + :end (kotlin-mode--token-end token)) + ;; Not an annotation. + (goto-char pos) + token)) + + ;; Not an annotation. + (t + (goto-char pos) + token)))) + +(defun kotlin-mode--try-backward-annotation-use-site-target () + "Try backward annotationUseSiteTarget. + +Try to skip colon, word, and at sign. + +Return non-nil if succeeds. Keep position and return nil otherwise." + (let ((pos (point))) + (forward-comment (- (point))) + (if (eq (char-before) ?:) + (progn + (backward-char) + (forward-comment (- (point))) + (if (and (< (skip-syntax-backward "w") 0) + (eq (char-before) ?@)) + (progn + (backward-char) + t) + (goto-char pos) + nil)) + (goto-char pos) + nil))) + +(defun kotlin-mode--forward-string-chunk () + "Skip forward a string chunk. + +A string chunk is a part of single-line/multiline string delimited with +quotation marks or template expressions." + (condition-case nil + (goto-char (scan-sexps (point) 1)) + (scan-error (goto-char (point-max))))) + +(defun kotlin-mode--backward-string-chunk () + "Skip backward a string chunk. + +A string chunk is a part of single-line/multiline string delimited with +quotation marks or template expressions." + (condition-case nil + (goto-char (scan-sexps (point) -1)) + (scan-error (goto-char (point-min))))) + +(defun kotlin-mode--beginning-of-string () + "Move point to the beginning of single-line/multiline string. + +Return the point of the beginning. + +Assuming the point is on a string." + (goto-char (or (nth 8 (syntax-ppss)) (point))) + (let (matching-bracket) + (while (and + (setq matching-bracket + (get-text-property + (point) + 'kotlin-property--matching-bracket)) + (< (point-min) matching-bracket)) + (goto-char matching-bracket) + (goto-char (nth 8 (syntax-ppss)))) + (point))) + +(defun kotlin-mode--end-of-string () + "Move point to the end of single-line/multiline string. + +Assuming the point is on a string." + (goto-char (or (nth 8 (syntax-ppss)) (point))) + (let (matching-bracket) + (kotlin-mode--forward-string-chunk) + (while (and (setq matching-bracket + (get-text-property + (1- (point)) + 'kotlin-property--matching-bracket)) + (< matching-bracket (point-max))) + (goto-char matching-bracket) + (kotlin-mode--forward-string-chunk))) + (point)) + +(defun kotlin-mode--goto-non-comment-bol () + "Back to the beginning of line that is not inside a comment." + (beginning-of-line) + (let (chunk) + (while (progn + (setq chunk (kotlin-mode--chunk-after)) + (kotlin-mode--chunk-comment-p chunk)) + ;; The point is in a comment. Backs to the beginning of the comment. + (goto-char (kotlin-mode--chunk-start chunk)) + (beginning-of-line)))) + +(defun kotlin-mode--goto-non-comment-eol () + "Proceed to the end of line that is not inside a comment. + +If this line ends with a single-line comment, goto just before the comment." + (end-of-line) + (let (chunk) + (while (progn + (setq chunk (kotlin-mode--chunk-after)) + (kotlin-mode--chunk-comment-p chunk)) + ;; The point is in a comment. + (if (kotlin-mode--chunk-single-line-comment-p chunk) + ;; This is a single-line comment + ;; Back to the beginning of the comment. + (goto-char (kotlin-mode--chunk-start chunk)) + ;; This is a multiline comment + ;; Proceed to the end of the comment. + (goto-char (kotlin-mode--chunk-start chunk)) + (forward-comment 1) + (end-of-line) + ;; If the comment is incomplete, back to the beginning of the comment. + (when (and (eobp) (kotlin-mode--chunk-after)) + (goto-char (kotlin-mode--chunk-start (kotlin-mode--chunk-after)))))))) + +(defun kotlin-mode--bol-other-than-comments-p () + "Return t if there is nothing other than comments in the front of this line. + +Return nil otherwise. +Newlines inside comments are ignored." + ;; Foo // ← bol + ;; /* */ Foo // ← bol + ;; X /* */ Foo // ← not bol + ;; + ;; /* + ;; */ /* */ /* + ;; */ Foo // ← bol + ;; + ;; X /* + ;; */ /* */ /* + ;; */ Foo // ← not bol + ;; + ;; X + ;; /* */ /* + ;; */ Foo // ← bol + (save-excursion + (let ((pos (point))) + (kotlin-mode--goto-non-comment-bol) + (forward-comment (point-max)) + (<= pos (point))))) + +(defun kotlin-mode--eol-other-than-comments-p () + "Return t if there is nothing other than comments until the end of this line. + +Return nil otherwise. +Newlines inside comments are ignored." + (save-excursion + (let ((pos (point))) + (kotlin-mode--goto-non-comment-eol) + (forward-comment (- (point))) + (<= (point) pos)))) + +(defun kotlin-mode--skip-whitespaces () + "Skip forward whitespaces and newlines." + (skip-syntax-forward " >")) + +(defun kotlin-mode--forward-comments-but-not-line-breaks () + "Skip forward comments but not line breaks." + (goto-char (min + (save-excursion + (kotlin-mode--goto-non-comment-eol) + (point)) + (save-excursion + (forward-comment (point-max)) + (point))))) + +(defun kotlin-mode--backward-comments-but-not-line-breaks () + "Skip backward comments but not line breaks." + (goto-char (max + (save-excursion + (kotlin-mode--goto-non-comment-bol) + (point)) + (save-excursion + (forward-comment (- (point))) + (point))))) + (provide 'kotlin-mode-lexer) ;;; kotlin-mode-lexer.el ends here diff --git a/kotlin-mode.el b/kotlin-mode.el index 6b715a5..eedbb35 100644 --- a/kotlin-mode.el +++ b/kotlin-mode.el @@ -1,11 +1,12 @@ ;;; kotlin-mode.el --- Major mode for kotlin -*- lexical-binding: t; -*- -;; Copyright © 2015 Shodai Yokoyama +;; Copyright © 2015 Shodai Yokoyama ;; Author: Shodai Yokoyama (quantumcars@gmail.com) ;; Version: 2.0.0 ;; Keywords: languages ;; Package-Requires: ((emacs "24.3")) +;; URL: https://github.com/Emacs-Kotlin-Mode-Maintainers/kotlin-mode ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by @@ -22,7 +23,7 @@ ;;; Commentary: -;; +;; Major-mode for Kotlin programming language. ;;; Code: @@ -33,6 +34,7 @@ (require 'eieio) (require 'kotlin-mode-lexer) +(require 'kotlin-mode-indent) (defgroup kotlin nil "A Kotlin major mode." @@ -59,18 +61,25 @@ :type 'string :group 'kotlin) +;; REPL + (defun kotlin-do-and-repl-focus (f &rest args) + "Call function F with ARGS and pop to the REPL buffer." (apply f args) (pop-to-buffer kotlin-repl-buffer)) (defun kotlin-send-region (start end) - "Send current region to Kotlin interpreter." + "Send current region to Kotlin interpreter. + +START and END define region within current buffer." (interactive "r") (comint-send-region kotlin-repl-buffer start end) (comint-send-string kotlin-repl-buffer "\n")) (defun kotlin-send-region-and-focus (start end) - "Send current region to Kotlin interpreter and switch to it." + "Send current region to Kotlin interpreter and switch to it. + +START and END define region within current buffer." (interactive "r") (kotlin-do-and-repl-focus 'kotlin-send-region start end)) @@ -97,6 +106,7 @@ (kotlin-do-and-repl-focus 'kotlin-send-block)) (defun kotlin-send-line () + "Send current line to Kotlin interpreter." (interactive) (kotlin-send-region (line-beginning-position) @@ -121,11 +131,13 @@ kotlin-args-repl)) (set (make-local-variable 'comint-preoutput-filter-functions) - (cons (lambda (string) - (replace-regexp-in-string "\x1b\\[.[GJK]" "" string)) nil))) + (list (lambda (string) + (replace-regexp-in-string "\x1b\\[.[GJK]" "" string))))) (pop-to-buffer kotlin-repl-buffer)) +;; Keymap + (defvar kotlin-mode-map (let ((map (make-keymap))) (define-key map (kbd "C-c C-z") 'kotlin-repl) @@ -133,37 +145,18 @@ (define-key map (kbd "C-c C-r") 'kotlin-send-region) (define-key map (kbd "C-c C-c") 'kotlin-send-block) (define-key map (kbd "C-c C-b") 'kotlin-send-buffer) + (define-key map [remap indent-new-comment-line] + #'kotlin-mode--indent-new-comment-line) map) - "Keymap for kotlin-mode") - -(defvar kotlin-mode-syntax-table - (let ((st (make-syntax-table))) - - ;; Strings - (modify-syntax-entry ?\" "\"" st) - (modify-syntax-entry ?\' "\"" st) - (modify-syntax-entry ?` "\"" st) - - ;; `_' and `@' as being a valid part of a symbol - (modify-syntax-entry ?_ "_" st) - (modify-syntax-entry ?@ "_" st) - - ;; b-style comment - (modify-syntax-entry ?/ ". 124b" st) - (modify-syntax-entry ?* ". 23n" st) - (modify-syntax-entry ?\n "> b" st) - (modify-syntax-entry ?\r "> b" st) - st)) - -(defconst kotlin-mode--closing-brackets '(?} ?\) ?\])) + "Keymap for `kotlin-mode'.") ;;; Font Lock -(defconst kotlin-mode--misc-keywords +(defconst kotlin-mode--package-keywords '("package" "import")) (defconst kotlin-mode--type-decl-keywords - '("sealed" "inner" "data" "class" "interface" "trait" "typealias" "enum" "object")) + '("class" "interface" "typealias" "object")) (defconst kotlin-mode--fun-decl-keywords '("fun")) @@ -187,8 +180,8 @@ (defconst kotlin-mode--generic-type-parameter-keywords '("where")) -(defvar kotlin-mode--keywords - (append kotlin-mode--misc-keywords +(defvar kotlin-mode--misc-keywords + (append kotlin-mode--package-keywords kotlin-mode--type-decl-keywords kotlin-mode--fun-decl-keywords kotlin-mode--val-decl-keywords @@ -200,15 +193,14 @@ (defconst kotlin-mode--constants-keywords '("null" "true" "false")) -(defconst kotlin-mode--modifier-keywords - '("open" "private" "protected" "public" "lateinit" - "override" "abstract" "final" "companion" - "annotation" "internal" "const" "in" "out" - "actual" "expect" "crossinline" "inline" "noinline" "external" - "infix" "operator" "reified" "suspend" "tailrec" "vararg")) +(defconst kotlin-mode--variance-modifier-keywords + '("in" "out")) + +(defconst kotlin-mode--reification-modifier-keywords + '("reified")) (defconst kotlin-mode--property-keywords - '("by" "get" "set")) ;; "by" "get" "set" + '("by" "get" "set")) (defconst kotlin-mode--initializer-keywords '("init" "constructor")) @@ -223,13 +215,13 @@ (defvar kotlin-mode--font-lock-keywords `(;; Keywords (,(rx-to-string - `(and bow (group (or ,@kotlin-mode--keywords)) eow) + `(and bow (group (or ,@kotlin-mode--misc-keywords)) eow) t) 1 font-lock-keyword-face) ;; Package names (,(rx-to-string - `(and (or ,@kotlin-mode--misc-keywords) (+ space) + `(and (or ,@kotlin-mode--package-keywords) (+ space) (group (+ (any word ?.)))) t) 1 font-lock-string-face) @@ -273,11 +265,14 @@ t) 1 font-lock-function-name-face) - ;; Access modifier - ;; Access modifier is valid identifier being used as variable + ;; Modifiers + ;; Modifier is valid identifier being used as variable ;; TODO: Highlight only modifiers in the front of class/fun (,(rx-to-string - `(and bow (group (or ,@kotlin-mode--modifier-keywords)) + `(and bow (group (or ,@kotlin-mode--modifier-keywords + ,@kotlin-mode--companion-modifier-keywords + ,@kotlin-mode--variance-modifier-keywords + ,@kotlin-mode--reification-modifier-keywords)) eow) t) 1 font-lock-keyword-face) @@ -312,9 +307,14 @@ ;; String interpolation (kotlin-mode--match-interpolation 0 font-lock-variable-name-face t)) - "Default highlighting expression for `kotlin-mode'") + "Default highlighting expression for `kotlin-mode'.") (defun kotlin-mode--match-interpolation (limit) + "Find template expression before LIMIT. + +Template expressions must be propertized by `kotlin-mode--syntax-propertize'. +If a template expression is found, move to that point, set `match-data', +and return non-nil. Return nil otherwise." (let ((pos (next-single-char-property-change (point) 'kotlin-property--interpolation @@ -328,233 +328,56 @@ t) (kotlin-mode--match-interpolation limit)))))) -(defun kotlin-mode--prev-line () - "Moves up to the nearest non-empty line" - (if (not (bobp)) - (progn - (forward-line -1) - (while (and (looking-at "^[ \t]*$") (not (bobp))) - (forward-line -1))))) - -(defun kotlin-mode--prev-line-begins (pattern) - "Return whether the previous line begins with the given pattern" - (save-excursion - (kotlin-mode--prev-line) - (looking-at (format "^[ \t]*%s" pattern)))) - -(defun kotlin-mode--prev-line-ends (pattern) - "Return whether the previous line ends with the given pattern" - (save-excursion - (kotlin-mode--prev-line) - (looking-at (format ".*%s[ \t]*$" pattern)))) - -(defun kotlin-mode--line-begins (pattern) - "Return whether the current line begins with the given pattern" - (save-excursion - (beginning-of-line) - (looking-at (format "^[ \t]*%s" pattern)))) - -(defun kotlin-mode--line-ends (pattern) - "Return whether the current line ends with the given pattern" - (save-excursion - (beginning-of-line) - (looking-at (format ".*%s[ \t]*$" pattern)))) - -(defun kotlin-mode--line-contains (pattern) - "Return whether the current line contains the given pattern" - (save-excursion - (beginning-of-line) - (looking-at (format ".*%s.*" pattern)))) - -(defun kotlin-mode--line-continuation() - "Return whether this line continues a statement in the previous line" - (or - (and (kotlin-mode--prev-line-begins "\\(if\\|for\\|while\\)[ \t]+(") - (kotlin-mode--prev-line-ends ")[[:space:]]*\\(\/\/.*\\|\\/\\*.*\\)?")) - (and (kotlin-mode--prev-line-begins "else[ \t]*") - (not (kotlin-mode--prev-line-begins "else [ \t]*->")) - (not (kotlin-mode--prev-line-ends "{.*"))) - (or - (kotlin-mode--line-begins "\\([.=:]\\|->\\|\\(\\(private\\|public\\|protected\\|internal\\)[ \t]*\\)?[sg]et\\b\\)")))) - -(defun kotlin-mode--in-comment-block () - "Return whether the cursor is within a standard comment block structure - of the following format: - /** - * Description here - */" - (save-excursion - (let ((in-comment-block nil) - (keep-going (and - (not (kotlin-mode--line-begins "\\*\\*+/")) - (not (kotlin-mode--line-begins "/\\*")) - (nth 4 (syntax-ppss))))) - (while keep-going - (kotlin-mode--prev-line) - (cond - ((kotlin-mode--line-begins "/\\*") - (setq keep-going nil) - (setq in-comment-block t)) - ((bobp) - (setq keep-going nil)) - ((kotlin-mode--line-contains "\\*/") - (setq keep-going nil)))) - in-comment-block))) - -(defun kotlin-mode--first-line-p () - "Determine if point is on the first line." - (save-excursion - (beginning-of-line) - (bobp) - ) - ) - -(defun kotlin-mode--line-closes-block-p () - "Return whether or not the start of the line closes its containing block." - (save-excursion - (back-to-indentation) - (memq (following-char) kotlin-mode--closing-brackets) - )) - -(defun kotlin-mode--get-opening-char-indentation (parser-state-index) - "Determine the indentation of the line that starts the current block. - -Caller must pass in PARSER-STATE-INDEX, which refers to the index -of the list returned by `syntax-ppss'. - -If it does not exist, will return nil." - (save-excursion - (back-to-indentation) - (let ((opening-pos (nth parser-state-index (syntax-ppss)))) - (when opening-pos - (goto-char opening-pos) - (current-indentation))) - ) - ) - -(defun kotlin-mode--indent-for-continuation () - "Return the expected indentation for a continuation." - (kotlin-mode--prev-line) - (if (kotlin-mode--line-continuation) - (kotlin-mode--indent-for-continuation) - (+ kotlin-tab-width (current-indentation))) - ) - -(defun kotlin-mode--indent-for-code () - "Return the level that this line of code should be indented to." - (let ((indent-opening-block (kotlin-mode--get-opening-char-indentation 1))) - (cond - ((kotlin-mode--line-continuation) (save-excursion (kotlin-mode--indent-for-continuation))) - ((booleanp indent-opening-block) 0) - ((kotlin-mode--line-closes-block-p) indent-opening-block) - (t (+ indent-opening-block kotlin-tab-width))) - )) - -(defun kotlin-mode--indent-for-comment () - "Return the level that this line of comment should be indented to." - (let ((opening-indentation (kotlin-mode--get-opening-char-indentation 8))) - (if opening-indentation - (1+ opening-indentation) - 0) - )) - -(defun kotlin-mode--indent-line () - "Indent the current line of Kotlin code." - (interactive) - (let ((follow-indentation-p - (and (<= (line-beginning-position) (point)) - (>= (+ (line-beginning-position) - (current-indentation)) - (point))))) - (save-excursion - (beginning-of-line) - (if (bobp) ; 1.) - (progn - (kotlin-mode--beginning-of-buffer-indent)) - (let ((not-indented t) cur-indent) - (cond ((looking-at "^[ \t]*\\.") ; line starts with . - (save-excursion - (kotlin-mode--prev-line) - (cond ((looking-at "^[ \t]*\\.") - (setq cur-indent (current-indentation))) - - (t - (setq cur-indent (+ (current-indentation) (* 2 kotlin-tab-width))))) - (if (< cur-indent 0) - (setq cur-indent 0)))) - - ((looking-at "^[ \t]*}") ; line starts with } - (save-excursion - (kotlin-mode--prev-line) - (while (and (or (looking-at "^[ \t]*$") (looking-at "^[ \t]*\\.")) (not (bobp))) - (kotlin-mode--prev-line)) - (cond ((or (looking-at ".*{[ \t]*$") (looking-at ".*{.*->[ \t]*$")) - (setq cur-indent (current-indentation))) - (t - (setq cur-indent (- (current-indentation) kotlin-tab-width))))) - (if (< cur-indent 0) - (setq cur-indent 0))) - - ((looking-at "^[ \t]*)") ; line starts with ) - (save-excursion - (kotlin-mode--prev-line) - (setq cur-indent (- (current-indentation) kotlin-tab-width))) - (if (< cur-indent 0) - (setq cur-indent 0))) - - (t - (save-excursion - (while not-indented - (kotlin-mode--prev-line) - (cond ((looking-at ".*{[ \t]*$") ; line ends with { - (setq cur-indent (+ (current-indentation) kotlin-tab-width)) - (setq not-indented nil)) - - ((looking-at "^[ \t]*}") ; line starts with } - (setq cur-indent (current-indentation)) - (setq not-indented nil)) - - ((looking-at ".*{.*->[ \t]*$") ; line ends with -> - (setq cur-indent (+ (current-indentation) kotlin-tab-width)) - (setq not-indented nil)) - - ((looking-at ".*([ \t]*$") ; line ends with ( - (setq cur-indent (+ (current-indentation) kotlin-tab-width)) - (setq not-indented nil)) - - ((looking-at "^[ \t]*).*$") ; line starts with ) - (setq cur-indent (current-indentation)) - (setq not-indented nil)) - - ((bobp) ; 5.) - (setq not-indented nil))))))) - (if cur-indent - (indent-line-to cur-indent) - (indent-line-to 0))))) - - (when follow-indentation-p - (back-to-indentation)))) - - -(defun kotlin-mode--beginning-of-buffer-indent () - (indent-line-to 0)) + +;; the Kotlin mode ;;;###autoload (define-derived-mode kotlin-mode prog-mode "Kotlin" "Major mode for editing Kotlin." (setq font-lock-defaults '((kotlin-mode--font-lock-keywords) nil nil)) + (setq-local parse-sexp-lookup-properties t) (add-hook 'syntax-propertize-extend-region-functions #'kotlin-mode--syntax-propertize-extend-region nil t) (setq-local syntax-propertize-function #'kotlin-mode--syntax-propertize) - (set (make-local-variable 'comment-start) "//") - (set (make-local-variable 'comment-padding) 1) - (set (make-local-variable 'comment-start-skip) "\\(//+\\|/\\*+\\)\\s *") - (set (make-local-variable 'comment-end) "") - (set (make-local-variable 'indent-line-function) 'kotlin-mode--indent-line) - (setq-local adaptive-fill-regexp comment-start-skip) + + (setq-local comment-start "//") + (setq-local comment-end "") + (setq-local comment-padding 1) + (setq-local comment-start-skip + (rx (seq (zero-or-more (syntax whitespace)) + (or + ;; Single-line comment + (seq "/" (one-or-more "/")) + ;; Multi-line comment + (seq "/" (one-or-more "*")) + ;; Middle of multi-line-comment + (seq (one-or-more "*") " ")) + (zero-or-more (syntax whitespace))))) + (setq-local adaptive-fill-regexp + (rx (seq (zero-or-more (syntax whitespace)) + (or + ;; Single-line comment + (seq "/" (one-or-more "/")) + ;; Middle of multi-line-comment + (seq (one-or-more "*") " ")) + (zero-or-more (syntax whitespace))))) + (setq-local fill-indent-according-to-mode t) + (setq-local comment-multi-line t) + + (setq-local indent-line-function 'kotlin-mode--indent-line) + + (setq-local electric-indent-chars + (append "{}()[]:;,." electric-indent-chars)) + + (add-hook 'post-self-insert-hook #'kotlin-mode--post-self-insert nil t) + + (setq-local kotlin-mode--anchor-overlay + (make-overlay (point-min) (point-min) nil t)) + + (delete-overlay kotlin-mode--anchor-overlay) :group 'kotlin :syntax-table kotlin-mode-syntax-table) diff --git a/test/kotlin-mode-test.el b/test/kotlin-mode-test.el index 9749006..b6f1ebf 100644 --- a/test/kotlin-mode-test.el +++ b/test/kotlin-mode-test.el @@ -76,7 +76,7 @@ println(arg) (should (equal (buffer-string) "fun test(args: Array) { args.forEach(arg -> - println(arg) + println(arg) ) ")) ))) @@ -180,26 +180,94 @@ println(arg) (while (and (looking-at "^[ \t]*$") (not (eobp))) (forward-line))) -(ert-deftest kotlin-mode--sample-test () - (with-temp-buffer - (insert-file-contents "test/sample.kt") - (goto-char (point-min)) - (kotlin-mode) - (setq-local indent-tabs-mode nil) - (setq-local tab-width 4) - (setq-local kotlin-tab-width 4) - (while (not (eobp)) - (let ((expected-line (thing-at-point 'line))) +(defvar kotlin-mode--test-keep-going nil + "If non-nil, do not stop at error in `kotlin-mode--sample-test'.") - ;; Remove existing indentation - (beginning-of-line) - (delete-region (point) (progn (skip-chars-forward " \t") (point))) +(ert-deftest kotlin-mode--sample-test () + (dolist (filename '("test/sample.kt" "test/pathological.kt")) + (with-temp-buffer + (insert-file-contents filename) + (goto-char (point-min)) + (kotlin-mode) + (setq-local indent-tabs-mode nil) + (setq-local tab-width 4) + (setq-local kotlin-tab-width 4) - ;; Indent the line - (kotlin-mode--indent-line) + (while (not (eobp)) + (kotlin-mode--test-current-line filename nil) - ;; Check that the correct indentation is re-applied - (should (equal expected-line (thing-at-point 'line))) + ;; Indent without following lines + (narrow-to-region (point-min) (line-end-position)) + (kotlin-mode--test-current-line filename t) + (widen) ;; Go to the next non-empty line (next-non-empty-line))))) + +(defun kotlin-mode--test-current-line (filename truncated) + (back-to-indentation) + (let (;; (thing-at-point 'line) returns string with property, so + ;; we use `buffer-substring-no-properties'. + (expected-line (buffer-substring-no-properties + (line-beginning-position) + (line-end-position))) + actual-line + (original-indent (current-column)) + ;; Known bug when the following lines are not present. + (known-bug-when-truncated + (looking-at ".*//.*KNOWN_BUG_WHEN_TRUNCATED")) + (known-bug (looking-at ".*//.*KNOWN_BUG\\($\\| \\)"))) + ;; Remove existing indentation, or indent to column 1 if expected + ;; indentation is column 0. + ;; Keep indent inside multiline string. + (unless (kotlin-mode--chunk-multiline-string-p + (kotlin-mode--chunk-after)) + (if (= original-indent 0) + (indent-line-to 1) + (delete-horizontal-space))) + + ;; Indent the line + (kotlin-mode--indent-line) + + (setq actual-line (buffer-substring-no-properties + (line-beginning-position) + (line-end-position))) + + ;; Check that the correct indentation is re-applied + (unless (and truncated known-bug) + (if (or known-bug (and truncated known-bug-when-truncated)) + (if (equal expected-line actual-line) + (message "%s:%s:info: %s is fixed somehow" + filename + (line-number-at-pos) + (if truncated "KNOWN_BUG_WHEN_TRUNCATED" "KNOWN_BUG")) + (back-to-indentation) + (message "%s:%s:warn: (known bug) %sexpected indentation to column %d but %d" + filename + (line-number-at-pos) + (if truncated "(when code is truncated) " "") + original-indent + (current-column))) + (if (and kotlin-mode--test-keep-going + (not (equal expected-line actual-line))) + (message "%s:%s:err: %sexpected indentation to column %d but %d" + filename + (line-number-at-pos) + (if truncated "(when code is truncated) " "") + original-indent + (current-column)) + (should + (equal + (format "%s:%s: %s%s" + filename + (line-number-at-pos) + (if truncated "(when code is truncated) " "") + expected-line) + (format "%s:%s: %s%s" + filename + (line-number-at-pos) + (if truncated "(when code is truncated) " "") + actual-line)))))) + + ;; Restore to original indentation for KNOWN_BUG line. + (indent-line-to original-indent))) diff --git a/test/pathological.kt b/test/pathological.kt new file mode 100644 index 0000000..2c2624a --- /dev/null +++ b/test/pathological.kt @@ -0,0 +1,2150 @@ +#!/usr/bin/kotlinc -script + +// fileAnnotation +@file + : + [ + A(a = 1) // No Comma here + B + . + C + . + D // No Comma here + E + ] + +@file + : + Foo + . + Bar< + A // KNOWN_BUG + > // KNOWN_BUG + . + Baz + +@file + : + Foo(aaa) + +// packageHeader +package foo // Line breaks are prohibited after "package". + .bar // Line breaks are prohibited after dot. + .baz + +// importList, importHeader +import a // Line breaks are prohibited after "import". + .b // Line breaks are prohibited after dot. + .c + .* + +// importAlias +import a + . b + . c + as d + +// typeAlias +@A +public + typealias + A + < + B, // KNOWN_BUG_WHEN_TRUNCATED + C // KNOWN_BUG_WHEN_TRUNCATED + > + = + D + +// classDeclaration + +@A +public + sealed + class + A + < + A, // KNOWN_BUG_WHEN_TRUNCATED + B // KNOWN_BUG_WHEN_TRUNCATED + > + // primaryConstructor + public + constructor + ( + // classParameters + val + x + : + Foo + = + foo, + var + y + : + Foo + = + foo + ) + : + // delegationSepcifiers + Base + by + x + + x, + B, + C, + D + . + (Int) + -> + Int + where + @A + A + : + A, + B + : + B { + + public + interface + A { + fun a(): Int + fun b(): Int + } + + // typeParameters + data + class + Bar< + @A // KNOWN_BUG_WHEN_TRUNCATED + out + A // KNOWN_BUG_WHEN_TRUNCATED + : + A, + @A // KNOWN_BUG_WHEN_TRUNCATED + in // KNOWN_BUG_WHEN_TRUNCATED + A + : + A + > + { // brace on its own line + fun a() { + } + } + + inner + enum + class + A(val x: Int) { + // enumClassBody + A(1), B(1), + C(1) { + override fun a() = 2 + }, D(1) { + override fun a() = 3 + }, + E(1); + + fun a(): Int = x + } + + // typeConstraints + public + class + Foo< + A // KNOWN_BUG_WHEN_TRUNCATED + > + : + X, + Y + where + A + : + B, + A + : + C { + } + + public class Foo: X, Y + where A + : + B, + A + : + C { + } + + public class Foo: X, Y where + A + : + B, + A + : + C { + } + + public class Foo: X, Y where A + : + B, + A + : + C { + } + + fun < + A + > Foo.foo() + : A + where + A + : + B, + A + : + C { + } + + fun < + A + > Foo.foo() + : A + where + A + : + B, + A + : + C + = + A() + + val + < + A // KNOWN_BUG_WHEN_TRUNCATED + > + A + . + x + where + A + : + B, + A + : + C + = + 1 + + val f = fun A.(): A + where + A: + B, + A: + C { + } + + class Foo where T: A + , T: B + , T: C + + // anonymousInitializer + init { + a() + } + + // companionObject + public + companion + object + A + : + B { + fun foo() { + } + } + + // functionDeclaration + public + fun + < + A // KNOWN_BUG_WHEN_TRUNCATED + > + A + . + foo + // functionValueParameters // KNOWN_BUG + ( + a: Int = 1 + ) + : + A + where + A : A { + a() + } + + fun + foo(x) + = + x + 1 + + // propertyDeclaration + public + val + < + A // KNOWN_BUG_WHEN_TRUNCATED + > + A + . + @A + a + : + A + where + A + : + A + = + a + public get() = 1 + public set(value) { + foo(value) + } + + // delegated property + var + x : Int + by + foo + + // multiVariableDeclaration + public + var + ( + x: Int, + y: Int + ) + + // getter/setter with default implementation + var x = 1 + @A get + @A set + + // objectDeclaration + public + object + Foo + : + Bar { + fun foo() { + bar() + } + } + + // secondaryConstructor + public + constructor + ( + ) + : + this + ( + 1 + ) { + a() + } + + public + constructor + ( + ) + : + super + ( + 1 + ) + + // dynamic type + var x: dynamic = 1 + + // nullableType + var + x + : + A + < + X, // KNOWN_BUG_WHEN_TRUNCATED + *, // KNOWN_BUG_WHEN_TRUNCATED + out + Y, + in + Z + > + . + B + . + C + < + X // KNOWN_BUG_WHEN_TRUNCATED + > + ? + ? + ? + = + null + + var + x + : + ( + Int + ) + ? + = + null + + // functionType + var + f + : + // parehthesized nullable receiver type + ( + A + ) + ? + . + ( + Int + ) + -> + ( + Int + ) + -> + C + + // definitelyNonNullableType + fun foo( + x: + A + & + Any + ) { + } + + // value class + + value + class Foo { // KNOWN_BUG + } // KNOWN_BUG +} + + +// statements +fun foo() { + //explicit semicolons + /* aaa */ a(); b(); + c(); + + // annotation + @A + @B + .D(aaa) @C + @[ + A + B + C + ] + // label + aaa@ + a() + + // forStatement + for ( + @A + a + : + A + in + aaa + ) { + a() + } + + for + ( + @A + (x, y) + in + aaa + ) + { + a() + } + + for ( + a in aaa + ) + a() + + // whileStatement + while ( + a() + ) { + a() + } + + while + ( + a() + ) + { + a() + } + + while ( + a() + ) + a() + + while ( + a() + ) + ; + + // doWhileStatement + do { + a() + } while ( + a() + ) + + do + { + a() + } + while + ( + a() + ) + + do + a() + + b() + while (a()) + + do + while (a()) + + while (a()) + do + do + while(a()) + while(a()) + + while(a()) + do + while(a()) + + // assignment + (x, y) = // Line breaks are prohibited before assignment operators. + a() + + aaa[x] = + 1 + + a + . + b + ?. + c + . + d = + e + + a + . + b += + 1 + + a + . + b -= + 1 + + a + . + b *= + 1 + + a + . + b %= + 1 + + // expression + // various operators + val x = + a + || + b + && + c + + val x = + a == // Line breaks are prohibited before equality operators + b + + val x = + a != + b + + val x = + a === + b + + val x = + a !== + g + + val x = + a < // Line breaks are prohibited before comparison operators + b + + val x = + a > + b + + val x = + a <= + b + + val x = + a >= + b + + val x = + foo(a) in // Line breaks are prohibited before in/is operators + b + + val x = + a !in + b + + when (a()) { + 1 -> a() + // Line breaks are prohibited before in/is operators, So the following + // line should not be indented. + in aaa -> a() + !in aaa -> a() + } + + val x = + a is + b + + val x = + a !is + b + + when (a()) { + 1 -> a() + // Line breaks are prohibited before in/is operators, So the following + // line should not be indented. + is X -> a() + !is X -> a() + } + + val x = + a + ?: + b + + // infixFunctionCall + val x = + a shl // Line breaks are allowed after infix function. + b // KNOWN_BUG + val y = 1 // KNOWN_BUG + + var shl = 1 + val x = shl shl shl + shl < 100 && foo() // this is not a continuation of the previous line. + + var shl = 1 + val x = shl shl + shl < 100 && foo() // this is a continuation of the previous line. // KNOWN_BUG + val y = 1 // KNOWN_BUG + + // This is not a infix function call; line breaks are + // prohibited before infix function. + val x = + a + f (b) // So this line should not indented. + + val x = + a .. // Line breaks are prohibited before range operator. + b + + val x = + a ..< // Line breaks are prohibited before closed range operator. + b + + val x = + a + // Line breaks are prohibited before additive operators. + b - + c + + a() + +a() // So this line should not be indented. + -a() // Nor should this line. + + val x = + a * // Line breaks are prohibited before multiplicative operators. + b / + c % + d + + val x = + a + as + A + as? + B + + // prefixUnaryExpression + val x = + @a + a@ // label + + + - + ++ + a // KNOWN_BUG + + val x = // KNOWN_BUG + -- + a // KNOWN_BUG + + val x = // KNOWN_BUG + ! + a // KNOWN_BUG + + // postfixUnaryExpression // KNOWN_BUG + val x = // KNOWN_BUG + a++ // Line breaks are prohibited before postfix operators. + + val x = + a + ++ a // So this line should not be indented. + + val x = + a-- + + val x = + a + -- a // This line too. + + var shl = 1 + val x = shl shl shl ++ + shl < 100 && foo() // this is not a continuation of the previous line. + + var shl = 1 + val x = shl shl ++ + shl < 100 && foo() // this is a continuation of the previous line. // KNOWN_BUG + + val x = // KNOWN_BUG + a!! + + val x = foo()!! + foo() // this is not a continuation of the previous line. + + val x = !! + foo() // this is a continuation of the previous line. // KNOWN_BUG + + val x = // KNOWN_BUG + f< // Line breaks are prohibited before type arguments. + A // KNOWN_BUG_WHEN_TRUNCATED + >( // Line breaks are prohibited before function arguments. + x + )[ // Line breaks are prohibited before subscript. + x + ] + . + a + . + b + + // lambda arguments + val x = f() + { + a() + } + + val x = f() + @A + a@ + { + a() + } + + val x = f() @A a@ { + a() + } + + val x = f + { + a() + } + + val x = f + @A + a@ + { + a() + } + + val x = f @A a@ { + a() + } + + val x = x + .foo { + a + } + .bar { + a + } + + val x = x.foo { + a + }.bar { + a + } + + val x = + f // Line breaks are prohibited before function arguments. + (1 + 1).also { print(it) } // So this line should not be indented. + + val x = + a // Line breaks are prohibited before function arguments. + [g()].forEach { print(it) } // So this line should not be indented. + + // parenthesizedExpression + val x = ( + a() + ) + + // collectionLiteral + @A(x = [ + 1, 2, 3, + /* aaa */ 4, 5, 6, + 7, 8, 9 + ]) + a() + + // CharacterLiteral + val x = + 'a' + + val x = + '\'' + + val x = + '"' + + // stringLiteral + val x = "abc\"def${ + foo("abc") + }ghi${ + "aaa\${ // dollar sign cannot be escaped + 1 + }bbb" + }jkl" + + val x = """a + "b" + c${ + foo("""a + b + c + """) + }d + e + f${ + """aaa\${ + 1 + }bbb""" + }ghi""" + + val x = + a("'") + + // lambdaLiteral + val f: () -> Unit = { + a() + a() + a() + } + + val f: () -> Unit = { -> + a() + a() + a() + } + + val f: () -> Unit = { + -> + a() + a() + a() + } + + val f: () -> Unit = { x, + @A + y + : + Int, + ( + z1, + z2 + ) + : + AAA + -> + a() + a() + a() + } + + val f: () -> Unit = { + x, + y, + z + -> + a() + a() + a() + } + + val f: () -> Unit = { + (x, + y, + z) + -> + a() + a() + a() + } + + // functionLiteral + + val f = + fun + A + . + ( + x: Int + ) + : + AAA + where + A + : + A, + B + : + B { + a() // KNOWN_BUG + } // KNOWN_BUG + + val f = fun + { + a() + } + + // objectLiteral + val object: A by a, B by b { + fun foo() { + } + } + + val x = + object + : + A, + B + by + b, + C { + fun foo() { + } + } + + val x = object + { + fun foo() { + } + } + + // thisExpression + val x = + this + + val x = + this@Foo + + // superExpression + val x = + super + + val x = + super@Foo + + val x = + super< + Int // KNOWN_BUG_WHEN_TRUNCATED + >@Foo + + // ifExpression + val x = if ( + a + + 1 + ) { + a() + } + + val x = if + ( + a + + 1 + ) + { + a() + } + + val x = if ( + a + ) + a() + + val x = if ( + a + ) + ; + + val x = if ( + a + ) { + a() + } else { + a() + } + + val x = if + ( + a + ) + { + a() + } + else + { + a() + } + + val x = if ( + a + ) + a() + else + a() + + val x = if ( + a + ) + ; + else + ; + + val x = if ( + a + ) + else + ; + + val x = + if ( + a + ) { + a() + } else { + a() + } + + val x = if (foo) 1 + else 2 // should be indented? + + val x = + if (foo) 1 + else 2 + + val x = if (foo) 1 + + 1 + else 2 + + 1 + + val x = if (foo) + if (bar) + aaa() + + 1 + else + if (baz) + if (aaa) aaa else bbb + + 1 + else + if (aaa) aaa else + bbb + + 1 + else + if (bar) + if (aaa) + if (bbb) + ccc + + 1 + + 2 + else + ccc + + 1 + + 2 + else + ccc + + 1 + + 2 + else + aaa() + + 1 + + val x = if (foo) + while (false) { + aaa() + + 1 + } + else + while (false) { + aaa() + + 1 + } + + val x = if (a) + foo() + + 1 + else if (e) + foo() + + 1 + else + foo() + + 1 + + val x = if (a) { + foo() + + 1 + } else if (e) { + foo() + + 1 + } else { + foo() + + 1 + } + + val x = if (a) { + foo() + + 1 + } else + if (e) { + foo() + + 1 + } else { + foo() + + 1 + } + + val x = if (a) + { + foo() + + 1 + } + else if (e) + { + foo() + + 1 + } + else + { + foo() + + 1 + } + + val x = if (a) if (a) 1 + + 1 else 1 else + if (a) if (a) 1 + + 1 else 1 else + 1 + + // whenExpression + + val x = when ( + @A + val + a + = + a() + ) { + a(), b(), + c(), d() + -> + { + a() + } + + a() -> { + a() + } + + in + a() + -> + { + a () + } + + is + A + -> + { + } + + a() + -> + if (x) { + 1 + } else { + 2 + } + + if (x) + 1 + else + 2 + -> + if (x) + 1 + else + 2 + + else + -> + { + a() + } + } + + val x = when + ( + a() + ) + { + a -> 1 + } + + // tryExpression + val x = try { + a() + } catch(@A e: E) { + a() + } catch(@A e: E) { + a() + } finally { + a() + } + + val x = + try { + a() + } catch(@A e: E) { + a() + } catch(@A e: E) { + a() + } finally { + a() + } + + val x = try + { + a() + } + catch + ( + @A e: E + ) + { + a() + } + finally + { + a() + } + + val x = try { + a() + } finally { + a() + } + + val x = try + { + a() + } + finally + { + a() + } + + // jumpExpression + val x = + throw + a() + + val x = + return a() // Line breaks are prohibited after return. + + val x = + return // Line breaks are prohibited after return. + a() // So this line should not be indented. + + val x = + return@A a() // Line breaks are prohibited after return. + + val x = + return@A // Line breaks are prohibited after return. + a() // So this line should not be indented. + + val x = + continue + + val x = + continue@A + + val x = + break + + val x = + break@A + + // callableReference + val x = + Foo + :: + foo + + val x = + Foo + :: + class + + // typeModifier + val f: + suspend + () -> Unit + = + suspend + { + a() + } +} + +class Foo: Base { + // memberModifier + public + override + fun f() { + } + + public + lateinit + var x: Int + + + // visibilityModifier + override + public + fun f() { + } + + override + private + fun f() { + } + + override + internal + fun f() { + } + + override + protected + fun f() { + } + + // functionModifier + public + tailrec + infix + inline + fun A.f(): A { + return a() + } + + public + operator + fun A.unaryPlus(): A { + return a() + } + + public + suspend + fun foo() { + a() + } +} + +public + external + fun foo() { + a() +} + +class Foo { + // propertyModifier + public + const + val + x = 1 +} + +// inheritanceModifier +public + abstract + class Foo { + fun foo() { + bar() + } +} + +public + final + class Foo { + fun foo() { + bar() + } +} + +public + open + class Foo { + fun foo() { + } +} + +class Foo { + // parameterModifier + fun foo( + @A + crossinline + body: () -> Unit + ) { + } + + fun foo( + @A + noinline + body: () -> Unit + ) { + } + + fun foo( + @A + vararg + a: A + ) { + } + + // reificationModifier + inline fun < + @A + reified + T // KNOWN_BUG_WHEN_TRUNCATED + > foo() { + a() + } +} + +// platformModifier +public + expect + class Foo { + fun foo() +} + +public + actual + class Foo { + fun foo() +} + + +// Ambiguous commas, colons, curly brackets, and objects. + +class C: A by object: B1, + B2 { + fun foo() {} + }, + B by object: B3, + B4 { + fun foo() {} + } { + fun foo(x: T): Int { + return when (x) { + object: B1 by object: B1 { + fun foo() {} + }, + B2 { + fun foo() {} + }, + object: B3, + B4 { + fun foo() {} + } -> + 1 + + else -> + 2 + } + } +} + +// Curly braces may appar at: +// classBody +// class (optional) +// interface (optional) +// companionObject (optional) +// objectDeclaration (optional) +// enumEntry (optional) +// objectLiteral +// block +// init +// functionBody +// fun (optional) +// getter +// setter +// anonymousFunction +// constructor (optional) +// controlStructureBody (optional) +// for +// while +// do +// if +// else +// whenEntry +// try +// catch +// finally +// lambda +// when + +class C: + A by foo bar { // infix function call + aaa() // this is not a class body // KNOWN_BUG + } { // KNOWN_BUG + // this is the class body + + fun foo() { + aaa() + } +} + +// Ambiguous arrows + +val f = { g: + (Int) -> + (Int) -> + Int -> + g(1, 2) +} + +when (x) { + 1 -> + f as + (Int) -> + Int // KNOWN_BUG + f as (Int) -> + Int -> + f as + (Int) -> + Int // KNOWN_BUG + f(1) -> + f as + (Int) -> + Int // KNOWN_BUG + (f) -> + f as + (Int) -> + Int // KNOWN_BUG + is (Int) -> + Int -> + f as + (Int) -> + Int // KNOWN_BUG + is Foo<(Int) -> + Int, // KNOWN_BUG + (Int) -> // KNOWN_BUG + Int> -> // KNOWN_BUG + f as + (Int) -> + Int // KNOWN_BUG + 1 < (2), (f) -> + f as + (Int) -> + Int // KNOWN_BUG + else -> + f as + (Int) -> + Int // KNOWN_BUG +} + +// various identifiers + +val `abc` = + 1 + +val `abc def 'ghi "aaa ${ aaa \` = // backquotes cannot be escaped + a() + +// UNICODE_CLASS_ND +val _0123456789٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹߀߁߂߃߄߅߆߇߈߉०१२३४५६७८९০১২৩৪৫৬৭৮৯੦੧੨੩੪੫੬੭੮੯૦૧૨૩૪૫૬૭૮૯୦୧୨୩୪୫୬୭୮୯௦௧௨௩௪௫௬௭௮௯౦౧౨౩౪౫౬౭౮౯೦೧೨೩೪೫೬೭೮೯൦൧൨൩൪൫൬൭൮൯෦෧෨෩෪෫෬෭෮෯๐๑๒๓๔๕๖๗๘๙໐໑໒໓໔໕໖໗໘໙༠༡༢༣༤༥༦༧༨༩၀၁၂၃၄၅၆၇၈၉႐႑႒႓႔႕႖႗႘႙០១២៣៤៥៦៧៨៩᠐᠑᠒᠓᠔᠕᠖᠗᠘᠙᥆᥇᥈᥉᥊᥋᥌᥍᥎᥏᧐᧑᧒᧓᧔᧕᧖᧗᧘᧙᪀᪁᪂᪃᪄᪅᪆᪇᪈᪉᪐᪑᪒᪓᪔᪕᪖᪗᪘᪙᭐᭑᭒᭓᭔᭕᭖᭗᭘᭙᮰᮱᮲᮳᮴᮵᮶᮷᮸᮹᱀᱁᱂᱃᱄᱅᱆᱇᱈᱉᱐᱑᱒᱓᱔᱕᱖᱗᱘᱙꘠꘡꘢꘣꘤꘥꘦꘧꘨꘩꣐꣑꣒꣓꣔꣕꣖꣗꣘꣙꤀꤁꤂꤃꤄꤅꤆꤇꤈꤉꧐꧑꧒꧓꧔꧕꧖꧗꧘꧙꧰꧱꧲꧳꧴꧵꧶꧷꧸꧹꩐꩑꩒꩓꩔꩕꩖꩗꩘꩙꯰꯱꯲꯳꯴꯵꯶꯷꯸꯹0123456789𐒠𐒡𐒢𐒣𐒤𐒥𐒦𐒧𐒨𐒩 = + 1 + +// Some characters are not supported while they are in the unicode category Nd. +// val _𐴰 = +// 1 +// val _𐴱 = +// 1 +// val _𐴲 = +// 1 +// val _𐴳 = +// 1 +// val _𐴴 = +// 1 +// val _𐴵 = +// 1 +// val _𐴶 = +// 1 +// val _𐴷 = +// 1 +// val _𐴸 = +// 1 +// val _𐴹 = +// 1 +val _𑁦 = + 1 +val _𑁧 = + 1 +val _𑁨 = + 1 +val _𑁩 = + 1 +val _𑁪 = + 1 +val _𑁫 = + 1 +val _𑁬 = + 1 +val _𑁭 = + 1 +val _𑁮 = + 1 +val _𑁯 = + 1 +val _𑃰 = + 1 +val _𑃱 = + 1 +val _𑃲 = + 1 +val _𑃳 = + 1 +val _𑃴 = + 1 +val _𑃵 = + 1 +val _𑃶 = + 1 +val _𑃷 = + 1 +val _𑃸 = + 1 +val _𑃹 = + 1 +val _𑄶 = + 1 +val _𑄷 = + 1 +val _𑄸 = + 1 +val _𑄹 = + 1 +val _𑄺 = + 1 +val _𑄻 = + 1 +val _𑄼 = + 1 +val _𑄽 = + 1 +val _𑄾 = + 1 +val _𑄿 = + 1 +val _𑇐 = + 1 +val _𑇑 = + 1 +val _𑇒 = + 1 +val _𑇓 = + 1 +val _𑇔 = + 1 +val _𑇕 = + 1 +val _𑇖 = + 1 +val _𑇗 = + 1 +val _𑇘 = + 1 +val _𑇙 = + 1 +val _𑋰 = + 1 +val _𑋱 = + 1 +val _𑋲 = + 1 +val _𑋳 = + 1 +val _𑋴 = + 1 +val _𑋵 = + 1 +val _𑋶 = + 1 +val _𑋷 = + 1 +val _𑋸 = + 1 +val _𑋹 = + 1 +val _𑑐 = + 1 +val _𑑑 = + 1 +val _𑑒 = + 1 +val _𑑓 = + 1 +val _𑑔 = + 1 +val _𑑕 = + 1 +val _𑑖 = + 1 +val _𑑗 = + 1 +val _𑑘 = + 1 +val _𑑙 = + 1 +val _𑓐 = + 1 +val _𑓑 = + 1 +val _𑓒 = + 1 +val _𑓓 = + 1 +val _𑓔 = + 1 +val _𑓕 = + 1 +val _𑓖 = + 1 +val _𑓗 = + 1 +val _𑓘 = + 1 +val _𑓙 = + 1 +val _𑙐 = + 1 +val _𑙑 = + 1 +val _𑙒 = + 1 +val _𑙓 = + 1 +val _𑙔 = + 1 +val _𑙕 = + 1 +val _𑙖 = + 1 +val _𑙗 = + 1 +val _𑙘 = + 1 +val _𑙙 = + 1 +val _𑛀 = + 1 +val _𑛁 = + 1 +val _𑛂 = + 1 +val _𑛃 = + 1 +val _𑛄 = + 1 +val _𑛅 = + 1 +val _𑛆 = + 1 +val _𑛇 = + 1 +val _𑛈 = + 1 +val _𑛉 = + 1 +val _𑜰 = + 1 +val _𑜱 = + 1 +val _𑜲 = + 1 +val _𑜳 = + 1 +val _𑜴 = + 1 +val _𑜵 = + 1 +val _𑜶 = + 1 +val _𑜷 = + 1 +val _𑜸 = + 1 +val _𑜹 = + 1 +val _𑣠 = + 1 +val _𑣡 = + 1 +val _𑣢 = + 1 +val _𑣣 = + 1 +val _𑣤 = + 1 +val _𑣥 = + 1 +val _𑣦 = + 1 +val _𑣧 = + 1 +val _𑣨 = + 1 +val _𑣩 = + 1 +val _𑱐 = + 1 +val _𑱑 = + 1 +val _𑱒 = + 1 +val _𑱓 = + 1 +val _𑱔 = + 1 +val _𑱕 = + 1 +val _𑱖 = + 1 +val _𑱗 = + 1 +val _𑱘 = + 1 +val _𑱙 = + 1 +// val _𑵐 = +// 1 +// val _𑵑 = +// 1 +// val _𑵒 = +// 1 +// val _𑵓 = +// 1 +// val _𑵔 = +// 1 +// val _𑵕 = +// 1 +// val _𑵖 = +// 1 +// val _𑵗 = +// 1 +// val _𑵘 = +// 1 +// val _𑵙 = +// 1 +// val _𑶠 = +// 1 +// val _𑶡 = +// 1 +// val _𑶢 = +// 1 +// val _𑶣 = +// 1 +// val _𑶤 = +// 1 +// val _𑶥 = +// 1 +// val _𑶦 = +// 1 +// val _𑶧 = +// 1 +// val _𑶨 = +// 1 +// val _𑶩 = +// 1 +val _𖩠 = + 1 +val _𖩡 = + 1 +val _𖩢 = + 1 +val _𖩣 = + 1 +val _𖩤 = + 1 +val _𖩥 = + 1 +val _𖩦 = + 1 +val _𖩧 = + 1 +val _𖩨 = + 1 +val _𖩩 = + 1 +val _𖭐 = + 1 +val _𖭑 = + 1 +val _𖭒 = + 1 +val _𖭓 = + 1 +val _𖭔 = + 1 +val _𖭕 = + 1 +val _𖭖 = + 1 +val _𖭗 = + 1 +val _𖭘 = + 1 +val _𖭙 = + 1 +val _𝟎 = + 1 +val _𝟏 = + 1 +val _𝟐 = + 1 +val _𝟑 = + 1 +val _𝟒 = + 1 +val _𝟓 = + 1 +val _𝟔 = + 1 +val _𝟕 = + 1 +val _𝟖 = + 1 +val _𝟗 = + 1 +val _𝟘 = + 1 +val _𝟙 = + 1 +val _𝟚 = + 1 +val _𝟛 = + 1 +val _𝟜 = + 1 +val _𝟝 = + 1 +val _𝟞 = + 1 +val _𝟟 = + 1 +val _𝟠 = + 1 +val _𝟡 = + 1 +val _𝟢 = + 1 +val _𝟣 = + 1 +val _𝟤 = + 1 +val _𝟥 = + 1 +val _𝟦 = + 1 +val _𝟧 = + 1 +val _𝟨 = + 1 +val _𝟩 = + 1 +val _𝟪 = + 1 +val _𝟫 = + 1 +val _𝟬 = + 1 +val _𝟭 = + 1 +val _𝟮 = + 1 +val _𝟯 = + 1 +val _𝟰 = + 1 +val _𝟱 = + 1 +val _𝟲 = + 1 +val _𝟳 = + 1 +val _𝟴 = + 1 +val _𝟵 = + 1 +val _𝟶 = + 1 +val _𝟷 = + 1 +val _𝟸 = + 1 +val _𝟹 = + 1 +val _𝟺 = + 1 +val _𝟻 = + 1 +val _𝟼 = + 1 +val _𝟽 = + 1 +val _𝟾 = + 1 +val _𝟿 = + 1 +// val _𞅀 = +// 1 +// val _𞅁 = +// 1 +// val _𞅂 = +// 1 +// val _𞅃 = +// 1 +// val _𞅄 = +// 1 +// val _𞅅 = +// 1 +// val _𞅆 = +// 1 +// val _𞅇 = +// 1 +// val _𞅈 = +// 1 +// val _𞅉 = +// 1 +// val _𞋰 = +// 1 +// val _𞋱 = +// 1 +// val _𞋲 = +// 1 +// val _𞋳 = +// 1 +// val _𞋴 = +// 1 +// val _𞋵 = +// 1 +// val _𞋶 = +// 1 +// val _𞋷 = +// 1 +// val _𞋸 = +// 1 +// val _𞋹 = +// 1 +val _𞥐 = + 1 +val _𞥑 = + 1 +val _𞥒 = + 1 +val _𞥓 = + 1 +val _𞥔 = + 1 +val _𞥕 = + 1 +val _𞥖 = + 1 +val _𞥗 = + 1 +val _𞥘 = + 1 +val _𞥙 = + 1 + +// UNICODE_CLASS_LL +// UNICODE_CLASS_LM +// UNICODE_CLASS_LO +// UNICODE_CLASS_LT +// UNICODE_CLASS_LU +val aʰªDžA = + 1 + +// UNICODE_CLASS_NL (rejected for some reason) +// val ᛮ = +// 1 diff --git a/test/sample.kt b/test/sample.kt index 7dd47b5..cb8c56a 100644 --- a/test/sample.kt +++ b/test/sample.kt @@ -13,7 +13,7 @@ import bar.Bar as bBar /**************************************************************** Multiline comment without leading "*" -****************************************************************/ +****************************************************************/ // KNOWN_BUG fun sum(a: Int, b: Int): Int { return a + b @@ -367,7 +367,7 @@ var setterVisibility: String = "abc" private set // the setter is private and has the default implementation var setterWithAnnotation: Any? = null -@Inject set // annotate the setter with Inject + @Inject set // annotate the setter with Inject var counter = 0 // the initializer value is written directly to the backing field set(value) { @@ -555,8 +555,8 @@ fun > sort(list: List) { } fun cloneWhenGreater(list: List, threshold: T): List -where T : Comparable, -T : Cloneable { + where T : Comparable, + T : Cloneable { return list.filter { it > threshold }.map { it.clone() } } @@ -591,8 +591,7 @@ window.addMouseListener( override fun mouseEntered(e: MouseEvent) { // ... } - } -) + }) val adHoc = object { var x: Int = 0 @@ -613,8 +612,7 @@ fun countClicks(window: JComponent) { override fun mouseEntered(e: MouseEvent) { enterCount++ } - } - ) + }) // ... } @@ -683,13 +681,13 @@ val doubled = ints.map { it -> it * 2 } strings.filter { it.length == 5 }.sortBy { it }.map { it.toUpperCase() } max(strings, { a, b -> a.length < b.length -}) + }) fun max(collection: Collection, less: (T, T) -> Boolean): T? { var max: T? = null for (it in collection) if (max == null || less(max, it)) - max = it + max = it return max } @@ -733,8 +731,10 @@ inline fun TreeNode.findParentOfType(): T? { class Test { fun tryAdd(a: Int?, b: Int?) : Int? { var result: Int? = null - a?.let { lhs -> - b?.let { rhs -> + a?.let { + lhs -> + b?.let { + rhs -> result = lhs + rhs } }