branch: elpa/kotlin-mode commit 55eed95033a59d7448a4b2bc11879e62c05e361b Merge: 99499e1500 8d5b1cada0 Author: Gregg Hernandez <greggory...@gmail.com> Commit: GitHub <nore...@github.com>
Merge pull request #73 from bricka/improve-indentation Making a number of improvements to the indentation logic --- kotlin-mode.el | 280 ++++++++++++----------------------------------- test/kotlin-mode-test.el | 28 +++++ test/sample.kt | 65 +++++------ 3 files changed, 129 insertions(+), 244 deletions(-) diff --git a/kotlin-mode.el b/kotlin-mode.el index f494a26c27..201b00691e 100644 --- a/kotlin-mode.el +++ b/kotlin-mode.el @@ -155,6 +155,7 @@ (modify-syntax-entry ?\r "> b" st) st)) +(defconst kotlin-mode--closing-brackets '(?} ?\) ?\])) ;;; Font Lock @@ -368,187 +369,13 @@ (defun kotlin-mode--line-continuation() "Return whether this line continues a statement in the previous line" (or - (and (kotlin-mode--prev-line-begins "\\(if\\|else\\|for\\|while\\)[ \t]*") + (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\\)") - (save-excursion - (kotlin-mode--prev-line) - (kotlin-mode--line-ends "\\([=:]\\|->\\)"))))) - -(defun kotlin-mode--base-indentation () - "Return the indentation level of the current line based on brackets only, - i.e. ignoring 'continuation' indentation." - (cond ((kotlin-mode--line-continuation) - (- (current-indentation) kotlin-tab-width)) - ((kotlin-mode--in-comment-block) - (- (current-indentation) 1)) - (t - (current-indentation)))) - -(defclass kotlin-mode--bracket-counter () - ((count :initarg :count - :initform 0 - :type integer - :documentation "The net bracket count (+1 for open, -1 for close).") - (indent :initarg :indent - :initform 0 - :type integer - :documentation "The indentation based on bracket layout.") - (finished :initarg :finished - :initform nil - :type boolean - :documentation "Whether the counting has finished.") - (use-base :initarg :use-base - :initform t - :type boolean - :documentation "Whether to factor out extra indentations.")) - "A class for counting brackets to find the appropriate bracket-based indent. - The logic here involves keeping track of the net-bracket-count, - defined as the number of open-brackets minus the number of close-brackets. - We scroll backwards until the net-bracket-count is zero, and this point - determines the desired indentation level for the current line.") - -(defun kotlin-mode--count-to-line-start (counter) - "Count the brackets on the current line, starting from the -cursor position, and working backward, incrementing the count +1 -for open-brackets, -1 for close-brackets. - -Mark the COUNTER finished, set indentation, and return as soon as -the overall count exceeds zero. If the counter is zero at the -beginning of the line, Mark the counter finished and set -indentation. If we hit a beginning of line but the counter is -negative, just return without marking finished." - (when (nth 4 (syntax-ppss)) - ;; If the point is inside a comment, goto the beginning of the comment. - (goto-char (nth 8 (syntax-ppss)))) - (save-excursion - (let ((line-beginning-position (line-beginning-position))) - (while (and (<= (oref counter count) 0) (not (bolp))) - (forward-comment (- (point))) - (backward-char) - (when (< (point) line-beginning-position) - (goto-char line-beginning-position)) - (cond ((eq (char-syntax (char-after)) ?\() - (cl-incf (oref counter count))) - ((eq (char-syntax (char-after)) ?\)) - (cl-decf (oref counter count)))))) - ;; We are at the beginning of the line, or just before an - ;; unmatching open bracket. - (cond - ;; If the net-bracket-count is zero, use this indentation - ((= (oref counter count) 0) - (oset counter finished t) - (if (oref counter use-base) - ;; Indenting a line that is neither close bracket nor the - ;; first element of a block or a list. Found the previous - ;; line. So align with the previous line, without effect of - ;; continued expression at the previous line. - (kotlin-mode--add-indent counter (kotlin-mode--base-indentation)) - ;; Indenting close bracket or the first element of a block or - ;; a list. So align with this line, optionally with extra - ;; indentation. - (kotlin-mode--add-indent counter (current-indentation)))) - ;; If we've now counted more open-brackets than close-brackets, - ;; use the indentation of the content immediately following the - ;; final open-bracket. - ;; - ;; Example: - ;; - ;; Suppose indenting "bar2()" in the following example: - ;; - ;; foo( bar1(), - ;; bar2()) - ;; - ;; We are at just before the open bracket of "foo". So skip the - ;; open bracket and spaces, then align "bar2()" with "bar1()". - ((> (oref counter count) 0) - (oset counter finished t) - (forward-char) - (skip-syntax-forward "(") - (skip-syntax-forward "-") - (kotlin-mode--add-indent counter (current-column)))))) - -(defun kotlin-mode--count-leading-close-brackets (counter) - "Adjust COUNTER when indenting close brackets. - -This function should be called at the line being indented. - -Example: -Suppose indenting the closing bracket of \"bar\" in the following example: - -fun foo() { - bar { - baz() - } // Indenting here -} - -This function decrements the counter, so that -`kotlin-mode--count-to-line-start' should not stop at the line -\"baz()\", but goto the line \"bar {\", so that the close bracket -aligned with \"bar {\"." - - (save-excursion - (skip-syntax-forward "-") - (when (looking-at "\\s)") - (oset counter use-base nil) - (kotlin-mode--subtract-count counter (skip-syntax-forward ")"))))) - -(defun kotlin-mode--count-trailing-open-brackets (counter) - "Adjust COUNTER when indenting the first element of a block or list. - -This function should be called before calling -`kotlin-mode--count-to-line-start', with the point at the end of -the previous line of the line being indented. - -If the bracket count is at zero, and there are open-brackets at -the end of the line, do not count them, but add a single -indentation level. If bracket count is at zero, we are not -indenting close brackets. - -Example: - -Suppose indenting \"baz()\" in the following example: - -fun foo() { - bar { - baz() - } -} - -This function is called with the point at the end of the line -\"bar {\". This function skips \"{\" backward and add indentation -amount `kotlin-tab-width', say 4. Then -`kotlin-mode--count-to-line-start' seeks to the -beginning-of-line. So the final indentation is 8, that is the -sum of indentation of bar and extra indentation. - -On the other hand, when indenting \"baz2()\" in the following -line, keep cursor and indentation level as is because -\"bar(baz1(),\" does not end with open brackets. Then -`kotlin-mode--count-to-line-start' stops at the close bracket of -\"bar(\". So \"baz2()\" is aligned with \"baz1()\". - -fun foo() { - bar(baz1(), - baz2()) -}" - (when (and (= (oref counter count) 0) - (not (= (skip-syntax-backward "(") 0))) - (kotlin-mode--add-indent counter kotlin-tab-width) - (oset counter use-base nil))) - -(defun kotlin-mode--add-count (counter val) - (cl-incf (oref counter count) val)) - -(defun kotlin-mode--subtract-count (counter val) - (cl-decf (oref counter count) val)) - -(defun kotlin-mode--add-indent (counter val) - (cl-incf (oref counter indent) val)) - -(defun kotlin-mode--finished (counter) - (oref counter finished)) + (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 @@ -574,46 +401,73 @@ fun foo() { (setq keep-going nil)))) in-comment-block))) -(defun kotlin-mode--indent-line () - "Indent current line as kotlin code." - (interactive) +(defun kotlin-mode--first-line-p () + "Determine if point is on the first line." (save-excursion (beginning-of-line) - (if (bobp) - (kotlin-mode--beginning-of-buffer-indent) - (let* ((bracket-counter (make-instance 'kotlin-mode--bracket-counter)) - ;; Find bracket-based indentation first - (cur-indent - (progn - (kotlin-mode--count-leading-close-brackets bracket-counter) - (save-excursion - (kotlin-mode--prev-line) - (end-of-line) - (kotlin-mode--count-trailing-open-brackets bracket-counter) - (kotlin-mode--count-to-line-start bracket-counter) - (while (and (not (kotlin-mode--finished bracket-counter)) - (not (bobp))) - (kotlin-mode--prev-line) - (end-of-line) - (kotlin-mode--count-to-line-start bracket-counter)) - (oref bracket-counter indent))))) + (bobp) + ) + ) - (cond - ((kotlin-mode--line-continuation) - ;; Add extra indentation if the line continues the previous one - (cl-incf cur-indent kotlin-tab-width)) +(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. - ((kotlin-mode--in-comment-block) - ;; Add one space of extra indentation if inside a comment block - (cl-incf cur-indent))) +Caller must pass in PARSER-STATE-INDEX, which refers to the index +of the list returned by `syntax-ppss'. - (indent-line-to cur-indent)))) - ;; bol < point < indentation-start - (when (<= (point-at-bol) (point) (save-excursion (back-to-indentation) (point))) - (back-to-indentation))) +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--beginning-of-buffer-indent () - (indent-line-to 0)) +(defun kotlin-mode--indent-line () + "Indent the current line of Kotlin code." + (interactive) + (indent-line-to (cond + ((kotlin-mode--first-line-p) 0) + ((kotlin-mode--in-comment-block) (kotlin-mode--indent-for-comment)) + (t (kotlin-mode--indent-for-code)) + )) + (syntax-ppss-flush-cache (point-at-bol)) + ) ;;;###autoload (define-derived-mode kotlin-mode prog-mode "Kotlin" diff --git a/test/kotlin-mode-test.el b/test/kotlin-mode-test.el index fe96b6218a..9749006f2f 100644 --- a/test/kotlin-mode-test.el +++ b/test/kotlin-mode-test.el @@ -53,6 +53,34 @@ return a + b return a + b }"))))) +(ert-deftest kotlin-mode--lambda-body-indent-test () + (with-temp-buffer + (let ((text "fun test(args: Array<String>) { +args.forEach(arg -> +println(arg) +) +")) + (insert text) + (goto-char (point-min)) + (kotlin-mode) + (setq-local indent-tabs-mode nil) + (setq-local tab-width 4) + (setq-local kotlin-tab-width 4) + + (forward-line) + (kotlin-mode--indent-line) + (forward-line) + (kotlin-mode--indent-line) + (forward-line) + (kotlin-mode--indent-line) + + (should (equal (buffer-string) "fun test(args: Array<String>) { + args.forEach(arg -> + println(arg) + ) +")) + ))) + (ert-deftest kotlin-mode--chained-methods () (with-temp-buffer (let ((text "names.filter { it.empty } diff --git a/test/sample.kt b/test/sample.kt index 06a8063dfc..7dd47b5eee 100644 --- a/test/sample.kt +++ b/test/sample.kt @@ -31,10 +31,12 @@ fun printSum(a: Int, b: Int) { print(veryLongResultVariableName) } -fun functionMultiLineArgs(first: Int, - second: Int, - third: Int, - fourth: Int) { +fun functionMultiLineArgs( + first: Int, + second: Int, + third: Int, + fourth: Int +) { print("(${first}, ${second}, ${third}, ${fourth})") } @@ -54,9 +56,9 @@ fun main(args: Array<String>) { fun max(a: Int, b: Int): Int { if (a > b) - return a + return a else - return b + return b } fun max(a: Int, b: Int) = if (a > b) a else b @@ -81,16 +83,16 @@ fun getStringLength(obj: Any): Int? { fun main(args: Array<String>) { for (arg in args) - print(arg) + print(arg) } for (i in args.indices) -print(args[i]) + print(args[i]) fun main(args: Array<String>) { var i = 0 while (i < args.size) - print(args[i++]) + print(args[i++]) } fun cases(obj: Any) { @@ -104,19 +106,19 @@ fun cases(obj: Any) { } if (x in 1..y-1) -print("OK") + print("OK") if (x !in 0..array.lastIndex) -print("Out") + print("Out") for (x in 1..5) -print(x) + print(x) for (name in names) -println(name) + println(name) if (text in names) // names.contains(text) is called -print("Yes") + print("Yes") names.filter { it.startsWith("A") } .sortedBy { it } @@ -225,7 +227,7 @@ inline fun <reified T: Any> Gson.fromJson(json): T = this.fromJson(json, T::clas loop@ for (i in 1..100) { for (j in 1..100) { if (x) - break@loop + break@loop } } @@ -362,7 +364,7 @@ var stringRepresentation: String } var setterVisibility: String = "abc" -private set // the setter is private and has the default implementation + private set // the setter is private and has the default implementation var setterWithAnnotation: Any? = null @Inject set // annotate the setter with Inject @@ -370,7 +372,7 @@ var setterWithAnnotation: Any? = null var counter = 0 // the initializer value is written directly to the backing field set(value) { if (value >= 0) - field = value + field = value } val isEmpty: Boolean @@ -444,7 +446,7 @@ class D : A, B { private fun foo() {} // visible inside example.kt public var bar: Int = 5 // property is visible everywhere -private set // setter is visible only in example.kt + private set // setter is visible only in example.kt internal val baz = 6 // visible inside the same module @@ -589,7 +591,8 @@ window.addMouseListener( override fun mouseEntered(e: MouseEvent) { // ... } -}) + } +) val adHoc = object { var x: Int = 0 @@ -610,7 +613,8 @@ fun countClicks(window: JComponent) { override fun mouseEntered(e: MouseEvent) { enterCount++ } - }) + } + ) // ... } @@ -648,7 +652,7 @@ infix fun Int.shl(x: Int): Int { fun <T> asList(vararg ts: T): List<T> { val result = ArrayList<T>() for (t in ts) // ts is an Array - result.add(t) + result.add(t) return result } @@ -670,7 +674,7 @@ val result = lock(lock, ::toBeSynchronized) fun <T, R> List<T>.map(transform: (T) -> R): List<R> { val result = arrayListOf<R>() for (item in this) - result.add(transform(item)) + result.add(transform(item)) return result } @@ -678,13 +682,14 @@ 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 }) +max(strings, { a, b -> a.length < b.length +}) fun <T> max(collection: Collection<T>, less: (T, T) -> Boolean): T? { var max: T? = null for (it in collection) - if (max == null || less(max, it)) - max = it + if (max == null || less(max, it)) + max = it return max } @@ -728,12 +733,10 @@ inline fun <reified T> TreeNode.findParentOfType(): T? { class Test { fun tryAdd(a: Int?, b: Int?) : Int? { var result: Int? = null - a?.let { - lhs -> - b?.let { - rhs -> - result = lhs + rhs - } + a?.let { lhs -> + b?.let { rhs -> + result = lhs + rhs + } } return result }