| ;===--- 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))) |
| |
| ;; 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) |
| ;; 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" "throws" "rethrows" |
| "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\\)\\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" "throw" "defer" "try" "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) |
| ;; Expressions |
| `(,(regexp-opt '("new") 'words) . font-lock-keyword-face) |
| ;; Special Variables |
| '("self" . 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\\)\\>")) |
| 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 |