;;; kotlin-mode.el --- Major mode for kotlin -*- lexical-binding: t; -*-
;; Copyright © 2015 Shodai Yokoyama
;; Author: Shodai Yokoyama (quantumcars@gmail.com)
;; Keywords: languages
;; Package-Version: 20190116.2055
;; Package-Requires: ((emacs "24.3"))
;; 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:
;;
;;; Code:
(require 'comint)
(require 'rx)
(require 'cc-cmds)
(defgroup kotlin nil
"A Kotlin major mode."
:group 'languages)
(defcustom kotlin-tab-width tab-width
"The tab width to use for indentation."
:type 'integer
:group 'kotlin-mode
:safe 'integerp)
(defcustom kotlin-command "kotlinc"
"The Kotlin command used for evaluating code."
:type 'string
:group 'kotlin)
(defcustom kotlin-args-repl '()
"The arguments to pass to `kotlin-command' to start a REPL."
:type 'list
:group 'kotlin)
(defcustom kotlin-repl-buffer "*KotlinREPL*"
"The name of the KotlinREPL buffer."
:type 'string
:group 'kotlin)
(defun kotlin-do-and-repl-focus (f &rest args)
(apply f args)
(pop-to-buffer kotlin-repl-buffer))
(defun kotlin-send-region (start end)
"Send current region to Kotlin interpreter."
(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."
(interactive "r")
(kotlin-do-and-repl-focus 'kotlin-send-region start end))
(defun kotlin-send-buffer ()
"Send whole buffer to Kotlin interpreter."
(interactive)
(kotlin-send-region (point-min) (point-max)))
(defun kotlin-send-buffer-and-focus ()
"Send whole buffer to Kotlin interpreter and switch to it."
(interactive)
(kotlin-do-and-repl-focus 'kotlin-send-buffer))
(defun kotlin-send-block ()
(interactive)
(let* ((p (point)))
(mark-paragraph)
(kotlin-send-region (region-beginning) (region-end))
(goto-char p)))
(defun kotlin-send-block-and-focus ()
"Send block to Kotlin interpreter and switch to it."
(interactive)
(kotlin-do-and-repl-focus 'kotlin-send-block))
(defun kotlin-send-line ()
(interactive)
(kotlin-send-region
(line-beginning-position)
(line-end-position)))
(defun kotlin-send-line-and-focus ()
"Send current line to Kotlin interpreter and switch to it."
(interactive)
(kotlin-do-and-repl-focus 'kotlin-send-line))
(defun kotlin-repl ()
"Launch a Kotlin REPL using `kotlin-command' as an inferior mode."
(interactive)
(unless (comint-check-proc kotlin-repl-buffer)
(set-buffer
(apply 'make-comint "KotlinREPL"
"env"
nil
"NODE_NO_READLINE=1"
kotlin-command
kotlin-args-repl))
(set (make-local-variable 'comint-preoutput-filter-functions)
(cons (lambda (string)
(replace-regexp-in-string "\x1b\\[.[GJK]" "" string)) nil)))
(pop-to-buffer kotlin-repl-buffer))
(defvar kotlin-mode-map
(let ((map (make-keymap)))
(define-key map (kbd "C-c C-z") 'kotlin-repl)
(define-key map (kbd "C-c C-n") 'kotlin-send-line)
(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 (kbd "") 'c-indent-line-or-region)
map)
"Keymap for kotlin-mode")
(defvar kotlin-mode-syntax-table
(let ((st (make-syntax-table)))
;; Strings
(modify-syntax-entry ?\" "\"" st)
;; `_' as being a valid part of a word
(modify-syntax-entry ?_ "w" st)
;; b-style comment
(modify-syntax-entry ?/ ". 124b" st)
(modify-syntax-entry ?* ". 23" st)
(modify-syntax-entry ?\n "> b" st)
st))
;;; Font Lock
(defconst kotlin-mode--misc-keywords
'("package" "import"))
(defconst kotlin-mode--type-decl-keywords
'("nested" "inner" "data" "class" "interface" "trait" "typealias" "enum" "object"))
(defconst kotlin-mode--fun-decl-keywords
'("fun"))
(defconst kotlin-mode--val-decl-keywords
'("val" "var"))
(defconst kotlin-mode--statement-keywords
'(;; Branching
"if" "else"
;; Exceptions
"try" "catch" "finally" "throw"
;; Loops
"while" "for" "do" "continue" "break"
;; Miscellaneous
"when" "is" "in" "as" "return"))
(defconst kotlin-mode--context-variables-keywords
'("this" "super"))
(defvar kotlin-mode--keywords
(append kotlin-mode--misc-keywords
kotlin-mode--type-decl-keywords
kotlin-mode--fun-decl-keywords
kotlin-mode--val-decl-keywords
kotlin-mode--statement-keywords
kotlin-mode--context-variables-keywords)
"Keywords used in Kotlin language.")
(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")) ;; "in" "out"
(defconst kotlin-mode--property-keywords
'("by")) ;; "by" "get" "set"
(defconst kotlin-mode--initializer-keywords
'("init" "constructor"))
(defvar kotlin-mode--font-lock-keywords
`(;; Keywords
(,(rx-to-string
`(and bow (group (or ,@kotlin-mode--keywords)) eow)
t)
1 font-lock-keyword-face)
;; Package names
(,(rx-to-string
`(and (or ,@kotlin-mode--misc-keywords) (+ space)
(group (+ (any word ?.))))
t)
1 font-lock-string-face)
;; Types
(,(rx-to-string
`(and bow upper (group (* (or word "<" ">" "." "?" "!" "*"))))
t)
0 font-lock-type-face)
;; Classes/Enums
(,(rx-to-string
`(and bow (or ,@kotlin-mode--type-decl-keywords) eow (+ space)
(group (+ word)) eow)
t)
1 font-lock-type-face)
;; Constants
(,(rx-to-string
`(and bow (group (or ,@kotlin-mode--constants-keywords)) eow)
t)
0 font-lock-constant-face)
;; Value bindings
(,(rx-to-string
`(and bow (or ,@kotlin-mode--val-decl-keywords) eow
(+ space)
(group (+ word)) (* space) (\? ":"))
t)
1 font-lock-variable-name-face t)
;; Function names
(,(rx-to-string
`(and (or ,@kotlin-mode--fun-decl-keywords)
(+ space) bow (group (+ (any alnum word))) eow)
t)
1 font-lock-function-name-face)
;; Access modifier
;; Access 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))
eow)
t)
1 font-lock-keyword-face)
;; Properties
;; by/get/set are valid identifiers being used as variable
;; TODO: Highlight keywords in the property declaration statement
;; (,(rx-to-string
;; `(and bow (group (or ,@kotlin-mode--property-keywords)) eow)
;; t)
;; 1 font-lock-keyword-face)
;; Constructor/Initializer blocks
(,(rx-to-string
`(and bow (group (or ,@kotlin-mode--initializer-keywords)) eow)
t)
1 font-lock-keyword-face)
;; String interpolation
(kotlin-mode--match-interpolation 0 font-lock-variable-name-face t))
"Default highlighting expression for `kotlin-mode'")
(defun kotlin-mode--new-font-lock-keywords ()
'(
("package\\|import" . font-lock-keyword-face)
))
(defun kotlin-mode--syntax-propertize-interpolation ()
(let* ((pos (match-beginning 0))
(context (save-excursion
(save-match-data (syntax-ppss pos)))))
(when (nth 3 context)
(put-text-property pos
(1+ pos)
'kotlin-property--interpolation
(match-data)))))
(defun kotlin-mode--syntax-propertize-function (start end)
(let ((case-fold-search))
(goto-char start)
(remove-text-properties start end '(kotlin-property--interpolation))
(funcall
(syntax-propertize-rules
((let ((identifier '(or
(and alpha (* alnum))
(and "`" (+ (not (any "`\n"))) "`"))))
(rx-to-string
`(or (group "${" ,identifier "}")
(group "$" ,identifier))))
(0 (ignore (kotlin-mode--syntax-propertize-interpolation)))))
start end)))
(defun kotlin-mode--match-interpolation (limit)
(let ((pos (next-single-char-property-change (point)
'kotlin-property--interpolation
nil
limit)))
(when (and pos (> pos (point)))
(goto-char pos)
(let ((value (get-text-property pos 'kotlin-property--interpolation)))
(if value
(progn (set-match-data value)
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--indent-line ()
"Indent current line as kotlin code"
(interactive)
(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)))))
(defun kotlin-mode--beginning-of-buffer-indent ()
(indent-line-to 0))
;;;###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 syntax-propertize-function #'kotlin-mode--syntax-propertize-function)
(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)
:group 'kotlin
:syntax-table kotlin-mode-syntax-table)
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.kts?\\'" . kotlin-mode) t)
(provide 'kotlin-mode)
;;; kotlin-mode.el ends here