Revision a87ef76f85c8c42fa15329ea53adda4793bf9686 authored by Mishal Shah on 27 August 2019, 23:26:43 UTC, committed by GitHub on 27 August 2019, 23:26:43 UTC
2 parent s 148ea44 + 4d66676
Raw File
swift-mode.el
;===--- swift-mode.el ----------------------------------------------------===;
;
; This source file is part of the Swift.org open source project
;
; Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
; Licensed under Apache License v2.0 with Runtime Library Exception
;
; See https://swift.org/LICENSE.txt for license information
; See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
;
;===----------------------------------------------------------------------===;

(require 'compile)
(unless (fboundp 'prog-mode)
  (define-derived-mode prog-mode fundamental-mode "Prog"
    "Base mode for other programming language modes"
    (setq bidi-paragraph-direction 'left-to-right)
    (set
     (make-local-variable 'require-final-newline) mode-require-final-newline)
    (set
     (make-local-variable 'parse-sexp-ignore-comments) t)))

(unless (fboundp 'defvar-local)
  (defmacro defvar-local (var val &optional docstring)
    "Define VAR as a buffer-local variable with default value VAL."
    `(make-variable-buffer-local (defvar ,var ,val ,docstring))))

;; Create mode-specific variables
(defcustom swift-basic-offset 2
  "Default indentation width for Swift source"
  :type 'integer)


;; Create mode-specific tables.
(defvar swift-mode-syntax-table nil
  "Syntax table used while in SWIFT mode.")

(defvar swift-font-lock-keywords
  (list
   ;; Comments
   '("^#!.*" . font-lock-comment-face)
   ;; Variables surrounded with backticks (`)
   '("`[a-zA-Z_][a-zA-Z_0-9]*`" . font-lock-variable-name-face)
   ;; Types
   '("\\b[A-Z][a-zA-Z_0-9]*\\b" . font-lock-type-face)
   ;; Floating point constants
   '("\\b[-+]?[0-9]+\.[0-9]+\\b" . font-lock-preprocessor-face)
   ;; Integer literals
   '("\\b[-]?[0-9]+\\b" . font-lock-preprocessor-face)
   ;; Decl and type keywords
   `(,(regexp-opt '("class" "init" "deinit" "extension" "fileprivate" "func"
                    "import" "let" "protocol" "static" "struct" "subscript"
                    "typealias" "enum" "var" "lazy" "where" "private" "public"
                    "internal" "override" "open" "associatedtype" "inout"
                    "indirect" "final")
                  'words) . font-lock-keyword-face)
   ;; Variable decl keywords
   `("\\b\\(?:[^a-zA-Z_0-9]*\\)\\(get\\|set\\)\\(?:[^a-zA-Z_0-9]*\\)\\b" 1 font-lock-keyword-face)
   `(,(regexp-opt '("willSet" "didSet") 'words) . font-lock-keyword-face)
   ;; Operators
   `("\\b\\(?:\\(?:pre\\|post\\|in\\)fix\\s-+\\)operator\\b" . font-lock-keyword-face)
   ;; Keywords that begin with a number sign
   `("#\\(if\\|endif\\|elseif\\|else\\|available\\|error\\|warning\\)\\b" . font-lock-string-face)
   `("#\\(file\\|line\\|column\\|function\\|selector\\)\\b" . font-lock-keyword-face)
   ;; Infix operator attributes
   `(,(regexp-opt '("precedence" "associativity" "left" "right" "none")
                  'words) . font-lock-keyword-face)
   ;; Statements
   `(,(regexp-opt '("if" "guard" "in" "else" "for" "do" "repeat" "while"
                    "return" "break" "continue" "fallthrough"  "switch" "case"
                    "default" "defer" "catch")
                  'words) . font-lock-keyword-face)
   ;; Decl modifier keywords
   `(,(regexp-opt '("convenience" "dynamic" "mutating" "nonmutating" "optional"
                    "required" "weak" "unowned" "safe" "unsafe")
                  'words) . font-lock-keyword-face)
   ;; Expression keywords: "Any" and "Self" are included in "Types" above
   `(,(regexp-opt '("as" "false" "is" "nil" "rethrows" "super" "self" "throw"
                    "true" "try" "throws")
                  'words) . font-lock-keyword-face)
   ;; Expressions
   `(,(regexp-opt '("new") 'words) . font-lock-keyword-face)
   ;; Variables
   '("[a-zA-Z_][a-zA-Z_0-9]*" . font-lock-variable-name-face)
   ;; Unnamed variables
   '("$[0-9]+" . font-lock-variable-name-face)
   )
  "Syntax highlighting for SWIFT"
  )

;; ---------------------- Syntax table ---------------------------

(if (not swift-mode-syntax-table)
    (progn
      (setq swift-mode-syntax-table (make-syntax-table))
      (mapc (function (lambda (n)
                        (modify-syntax-entry (aref n 0)
                                             (aref n 1)
                                             swift-mode-syntax-table)))
            '(
              ;; whitespace (` ')
              [?\f  " "]
              [?\t  " "]
              [?\   " "]
              ;; word constituents (`w')
              ;; punctuation
              [?< "."]
              [?> "."]
              ;; comments
              [?/  ". 124"]
              [?*  ". 23b"]
              [?\n  ">"]
              [?\^m  ">"]
              ;; symbol constituents (`_')
              [?_ "_"]
              ;; punctuation (`.')
              ;; open paren (`(')
              [?\( "())"]
              [?\[ "(]"]
              [?\{ "(}"]
              ;; close paren (`)')
              [?\) ")("]
              [?\] ")["]
              [?\} "){"]
              ;; string quote ('"')
              [?\" "\""]
              ;; escape-syntax characters ('\\')
              [?\\ "\\"]
              ))))

;; --------------------- Abbrev table -----------------------------

(defvar swift-mode-abbrev-table nil
  "Abbrev table used while in SWIFT mode.")
(define-abbrev-table 'swift-mode-abbrev-table ())

(defvar swift-mode-map
  (let ((keymap (make-sparse-keymap)))
    keymap)
  "Keymap for `swift-mode'.")


;;;###autoload
(define-derived-mode swift-mode prog-mode "Swift"
  "Major mode for editing SWIFT source files.
  \\{swift-mode-map}
  Runs swift-mode-hook on startup."
  :group 'swift

  (require 'electric)
  (set (make-local-variable 'indent-line-function) 'swift-indent-line)
  (set (make-local-variable 'parse-sexp-ignore-comments) t)
  (set (make-local-variable 'comment-use-syntax) nil) ;; don't use the syntax table; use our regexp
  (set (make-local-variable 'comment-start-skip) "\\(?:/\\)\\(?:/[:/]?\\|[*]+\\)[ \t]*")
  (set (make-local-variable 'comment-start) "// ")
  (set (make-local-variable 'comment-end) "")

  (unless (boundp 'electric-indent-chars)
    (defvar electric-indent-chars nil))
  (unless (boundp 'electric-pair-pairs)
    (defvar electric-pair-pairs nil))

  (set (make-local-variable 'electric-indent-chars)
       (append "{}()[]:," electric-indent-chars))
  (set (make-local-variable 'electric-pair-pairs)
       (append '(
                 ;; (?' . ?\') ;; This isn't such a great idea because
                 ;; pairs are detected even in strings and comments,
                 ;; and sometimes an apostrophe is just an apostrophe
                 (?{ . ?})  (?[ . ?]) (?( . ?)) (?` . ?`)) electric-pair-pairs))
  (set (make-local-variable 'electric-layout-rules)
       '((?\{ . after) (?\} . before)))

  (set (make-local-variable 'font-lock-defaults)
       '(swift-font-lock-keywords) ))

(defconst swift-doc-comment-detail-re
  (let* ((just-space "[ \t\n]*")
        (not-just-space "[ \t]*[^ \t\n].*")
        (eol "\\(?:$\\)")
        (continue "\n\\1"))

    (concat "^\\([ \t]*///\\)" not-just-space eol
            "\\(?:" continue not-just-space eol "\\)*"
            "\\(" continue just-space eol
            "\\(?:" continue ".*" eol "\\)*"
            "\\)"))
  "regexp that finds the non-summary part of a swift doc comment as subexpression 2")

(defun swift-hide-doc-comment-detail ()
  "Hide everything but the summary part of doc comments.

Use `M-x hs-show-all' to show them again."
    (interactive)
  (hs-minor-mode)
  (save-excursion
    (save-match-data
      (goto-char (point-min))
      (while (search-forward-regexp swift-doc-comment-detail-re (point-max) :noerror)
        (hs-hide-comment-region (match-beginning 2) (match-end 2))
        (goto-char (match-end 2))))))

(defvar swift-mode-generic-parameter-list-syntax-table
  (let ((s (copy-syntax-table swift-mode-syntax-table)))
    (modify-syntax-entry ?\< "(>" s)
    (modify-syntax-entry ?\> ")<" s)
    s))

(defun swift-skip-comments-and-space ()
  "Skip comments and whitespace, returning t"
  (while (forward-comment 1))
  t)

(defconst swift-identifier-re "\\_<[[:alpha:]_].*?\\_>")

(defun swift-skip-optionality ()
  "Hop over any comments, whitespace, and strings
of `!' or `?', returning t unconditionally."
  (swift-skip-comments-and-space)
  (while (not (zerop (skip-chars-forward "!?")))
    (swift-skip-comments-and-space)))

(defun swift-skip-generic-parameter-list ()
  "Hop over any comments, whitespace, and, if present, a generic
parameter list, returning t if the parameter list was found and
nil otherwise."
  (swift-skip-comments-and-space)
  (when (looking-at "<")
    (with-syntax-table swift-mode-generic-parameter-list-syntax-table
      (ignore-errors (forward-sexp) t))))

(defun swift-skip-re (pattern)
  "Hop over any comments and whitespace; then if PATTERN matches
the next characters skip over them, returning t if so and nil
otherwise."
  (swift-skip-comments-and-space)
  (save-match-data
    (when (looking-at pattern)
      (goto-char (match-end 0))
      t)))

(defun swift-skip-identifier ()
  "Hop over any comments, whitespace, and an identifier if one is
present, returning t if so and nil otherwise."
  (swift-skip-re swift-identifier-re))

(defun swift-skip-simple-type-name ()
  "Hop over a chain of the form identifier
generic-parameter-list? ( `.' identifier generic-parameter-list?
)*, returning t if the initial identifier was found and nil otherwise."
  (when (swift-skip-identifier)
    (swift-skip-generic-parameter-list)
    (when (swift-skip-re "\\.")
      (swift-skip-simple-type-name))
    t))

(defun swift-skip-type-name ()
    "Hop over any comments, whitespace, and the name of a type if
one is present, returning t if so and nil otherwise"
  (swift-skip-comments-and-space)
  (let ((found nil))
    ;; repeatedly
    (while
        (and
         ;; match a tuple or an identifier + optional generic param list
         (cond
          ((looking-at "[[(]")
           (forward-sexp)
           (setq found t))

          ((swift-skip-simple-type-name)
           (setq found t)))

          ;; followed by "->"
         (prog2 (swift-skip-re "\\?+")
             (swift-skip-re "throws\\|rethrows\\|->")
           (swift-skip-re "->") ;; accounts for the throws/rethrows cases on the previous line
           (swift-skip-comments-and-space))))
    found))

(defun swift-skip-constraint ()
    "Hop over a single type constraint if one is present,
returning t if so and nil otherwise"
  (swift-skip-comments-and-space)
  (and (swift-skip-type-name)
       (swift-skip-re ":\\|==")
       (swift-skip-type-name)))

(defun swift-skip-where-clause ()
    "Hop over a where clause if one is present, returning t if so
and nil otherwise"
  (when (swift-skip-re "\\<where\\>")
    (while (and (swift-skip-constraint) (swift-skip-re ",")))
    t))

(defun swift-in-string-or-comment ()
  "Return non-nil if point is in a string or comment."
  (or (nth 3 (syntax-ppss)) (nth 4 (syntax-ppss))))

(defconst swift-body-keyword-re
  "\\_<\\(var\\|func\\|init\\|deinit\\|subscript\\)\\_>")

(defun swift-hide-bodies ()
  "Hide the bodies of methods, functions, and computed properties.

Use `M-x hs-show-all' to show them again."
    (interactive)
  (hs-minor-mode)
  (save-excursion
    (save-match-data
      (goto-char (point-min))
      (while (search-forward-regexp swift-body-keyword-re (point-max) :noerror)
        (when
            (and
             (not (swift-in-string-or-comment))
             (let ((keyword (match-string 0)))
               ;; parse up to the opening brace
               (cond
                ((equal keyword "deinit") t)

                ((equal keyword "var")
                 (and (swift-skip-identifier)
                      (swift-skip-re ":")
                      (swift-skip-type-name)))

                ;; otherwise, there's a parameter list
                (t
                 (and
                  ;; parse the function's base name or operator symbol
                  (if (equal keyword "func") (forward-symbol 1) t)
                  ;; advance to the beginning of the function
                  ;; parameter list
                  (progn
                    (swift-skip-generic-parameter-list)
                    (swift-skip-comments-and-space)
                    (equal (char-after) ?\())
                  ;; parse the parameter list and any return type
                  (prog1
                    (swift-skip-type-name)
                    (swift-skip-where-clause))))))
             (swift-skip-re "{"))
          (hs-hide-block :reposition-at-end))))))

(defun swift-indent-line ()
  (interactive)
  (let (indent-level target-column)
    (save-excursion
      (widen)
      (setq indent-level (car (syntax-ppss (point-at-bol))))

      ;; Look at the first non-whitespace to see if it's a close paren
      (beginning-of-line)
      (skip-syntax-forward " ")
      (setq target-column
            (if (or (equal (char-after) ?\#) (looking-at "//:")) 0
              (* swift-basic-offset
                 (- indent-level
                    (cond ((= (char-syntax (or (char-after) ?\X)) ?\))
                           1)
                          ((save-match-data
                             (looking-at
                              "case \\|default *:\\|[a-zA-Z_][a-zA-Z0-9_]*\\(\\s-\\|\n\\)*:\\(\\s-\\|\n\\)*\\(for\\|do\\|\\while\\|switch\\|repeat\\)\\>"))
                           1)
                          (t 0))))))
      (indent-line-to (max target-column 0)))
    (when (< (current-column) target-column)
      (move-to-column target-column)))
  )

;; Compilation error parsing
(push 'swift0 compilation-error-regexp-alist)
(push 'swift1 compilation-error-regexp-alist)
(push 'swift-fatal compilation-error-regexp-alist)

(push `(swift0
        ,(concat
     "^"
       "[ \t]+" "\\(?:(@\\)?"
       "[A-Z⚠️][A-Za-z0-9_]*@"
     ;; Filename \1
       "\\("
          "[0-9]*[^0-9\n]"
          "\\(?:"
             "[^\n :]" "\\|" " [^/\n]" "\\|" ":[^ \n]"
          "\\)*?"
       "\\)"
       ":"
       ;; Line number (\2)
       "\\([0-9]+\\)"
       ":"
       ;; Column \3
       "\\([0-9]+\\)"
       )
     1 2 3 0)
      compilation-error-regexp-alist-alist)

(push `(swift1
        ,(concat
     "^"
       "[0-9]+[.][ \t]+While .* at \\[?"
     ;; Filename \1
       "\\("
          "[0-9]*[^0-9\n]"
          "\\(?:"
             "[^\n :]" "\\|" " [^/\n]" "\\|" ":[^ \n]"
          "\\)*?"
       "\\)"
       ":"
       ;; Line number (\2)
       "\\([0-9]+\\)"
       ":"
       ;; Column \3
       "\\([0-9]+\\)"
       )
     1 2 3 2)
      compilation-error-regexp-alist-alist)

(push `(swift-fatal
        ,(concat
     "^\\(?:assertion failed\\|fatal error\\): \\(?:.*: \\)?file "
     ;; Filename \1
       "\\("
          "[0-9]*[^0-9\n]"
          "\\(?:"
             "[^\n :]" "\\|" " [^/\n]" "\\|" ":[^ \n]"
          "\\)*?"
       "\\)"
       ", line "
       ;; Line number (\2)
       "\\([0-9]+\\)"
       )
     1 2 nil 2)
      compilation-error-regexp-alist-alist)

;; Flymake support

(require 'flymake)

;; This name doesn't end in "function" to avoid being unconditionally marked as risky.
(defvar-local swift-find-executable-fn 'executable-find
  "Function to find a command executable.
The function is called with one argument, the name of the executable to find.
Might be useful if you want to use a swiftc that you built instead
of the one in your PATH.")
(put 'swift-find-executable-fn 'safe-local-variable 'functionp)

(defvar-local swift-syntax-check-fn 'swift-syntax-check-directory
"Function to create the swift command-line that syntax-checks the current buffer.
The function is called with two arguments, the swiftc executable, and
the name of a temporary file that will contain the contents of the
current buffer.
Set to 'swift-syntax-check-single-file to ignore other files in the current directory.")
(put 'swift-syntax-check-fn 'safe-local-variable 'functionp)

(defvar-local swift-syntax-check-args '("-typecheck")
  "List of arguments to be passed to swiftc for syntax checking.
Elements of this list that are strings are inserted literally
into the command line.  Elements that are S-expressions are
evaluated.  The resulting list is cached in a file-local
variable, `swift-syntax-check-evaluated-args', so if you change
this variable you should set that one to nil.")
(put 'swift-syntax-check-args 'safe-local-variable 'listp)

(defvar-local swift-syntax-check-evaluated-args
  "File-local cache of swift arguments used for syntax checking
variable, `swift-syntax-check-args', so if you change
that variable you should set this one to nil.")

(defun swift-syntax-check-single-file (swiftc temp-file)
  "Return a flymake command-line list for syntax-checking the current buffer in isolation"
  `(,swiftc ("-typecheck" ,temp-file)))

(defun swift-syntax-check-directory (swiftc temp-file)
  "Return a flymake command-line list for syntax-checking the
current buffer along with the other swift files in the same
directory."
  (let* ((sources nil))
    (dolist (x (directory-files (file-name-directory (buffer-file-name))))
      (when (and (string-equal "swift" (file-name-extension x))
                 (not (file-equal-p x (buffer-file-name))))
        (setq sources (cons x sources))))
    `(,swiftc ("-typecheck" ,temp-file ,@sources))))

(defun flymake-swift-init ()
  (let* ((temp-file
          (flymake-init-create-temp-buffer-copy
           (lambda (x y)
             (make-temp-file
              (concat (file-name-nondirectory x) "-" y)
              (not :DIR_FLAG)
              ;; grab *all* the extensions; handles .swift.gyb files, for example
              ;; whereas using file-name-extension would only get ".gyb"
              (replace-regexp-in-string "^\\(?:.*/\\)?[^.]*" "" (buffer-file-name)))))))
    (funcall swift-syntax-check-fn
             (funcall swift-find-executable-fn "swiftc")
             temp-file)))

(add-to-list 'flymake-allowed-file-name-masks '(".+\\.swift$" flymake-swift-init))

(setq flymake-err-line-patterns
      (append
       (flymake-reformat-err-line-patterns-from-compile-el
        (mapcar (lambda (x) (assoc x compilation-error-regexp-alist-alist))
                '(swift0 swift1 swift-fatal)))
       flymake-err-line-patterns))

(defgroup swift nil
  "Major mode for editing swift source files."
  :prefix "swift-")

(provide 'swift-mode)

;; end of swift-mode.el
back to top