X-Git-Url: http://g0dil.de/git?a=blobdiff_plain;f=elpa%2Ftypescript-mode-20200430.1232%2Ftypescript-mode.el;fp=elpa%2Ftypescript-mode-20200430.1232%2Ftypescript-mode.el;h=09add955536e0b4178d80249822ee04c90ea062c;hb=28a13bd0b8508bbab5e79108e5d32d2edc357baa;hp=0000000000000000000000000000000000000000;hpb=26914a98eea51a46872bacef7c2dcf8ba96a50de;p=emacs-init.git diff --git a/elpa/typescript-mode-20200430.1232/typescript-mode.el b/elpa/typescript-mode-20200430.1232/typescript-mode.el new file mode 100644 index 0000000..09add95 --- /dev/null +++ b/elpa/typescript-mode-20200430.1232/typescript-mode.el @@ -0,0 +1,2979 @@ +;;; typescript-mode.el --- Major mode for editing typescript + +;; ----------------------------------------------------------------------------------- +;; TypeScript support for Emacs +;; Unmodified original sourve available at http://www.karllandstrom.se/downloads/emacs/javascript.el +;; Copyright (c) 2008 Free Software Foundation +;; Portions Copyright (C) Microsoft Open Technologies, Inc. All rights reserved. +;; +;; 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 . +;; ------------------------------------------------------------------------------------------- + +;; URL: http://github.com/ananthakumaran/typescript.el +;; Version: 0.1 +;; Keywords: typescript languages +;; Package-Requires: ((emacs "24.3")) + +;; This file is not part of GNU Emacs. + +;;; Commentary: + +;; This is based on Karl Landstrom's barebones typescript-mode. This +;; is much more robust and works with cc-mode's comment filling +;; (mostly). +;; The modifications to the original javascript.el mode mainly consisted in +;; replacing "javascript" with "typescript" +;; +;; The main features of this typescript mode are syntactic +;; highlighting (enabled with `font-lock-mode' or +;; `global-font-lock-mode'), automatic indentation and filling of +;; comments. +;; +;; +;; General Remarks: +;; +;; XXX: This mode assumes that block comments are not nested inside block +;; XXX: comments +;; +;; Exported names start with "typescript-"; private names start with +;; "typescript--". + +;;; Code: + +(eval-and-compile + (require 'compile) + (require 'cc-mode) + (require 'font-lock) + (require 'rx) + (require 'newcomment)) + +(eval-when-compile + (require 'cl-lib)) + +;;; Constants + +(defconst typescript--type-name-re "\\(?:[A-Z][A-Za-z0-9]+\\.\\)\\{0,\\}\\(?:[A-Z][A-Za-z0-9]+\\)" + "Regexp matching a conventional TypeScript type-name. Must start with upper-case letter!") + +(defconst typescript--name-start-re "[a-zA-Z_$]" + "Regexp matching the start of a typescript identifier, without grouping.") + +(defconst typescript--name-re (concat typescript--name-start-re + "\\(?:\\s_\\|\\sw\\)*") + "Regexp matching a typescript identifier, without grouping.") + +(defconst typescript--objfield-re (concat typescript--name-re ":") + "Regexp matching the start of a typescript object field.") + +(defconst typescript--dotted-name-re + (concat typescript--name-re "\\(?:\\." typescript--name-re "\\)*") + "Regexp matching a dot-separated sequence of typescript names.") + +(defconst typescript--plain-method-re + (concat "^\\s-*?\\(" typescript--dotted-name-re "\\)\\.prototype" + "\\.\\(" typescript--name-re "\\)\\s-*?=\\s-*?\\(function\\)\\_>") + "Regexp matching an explicit typescript prototype \"method\" declaration. +Group 1 is a (possibly-dotted) class name, group 2 is a method name, +and group 3 is the 'function' keyword.") + +(defconst typescript--plain-class-re + (concat "^\\s-*\\(" typescript--dotted-name-re "\\)\\.prototype" + "\\s-*=\\s-*{") + "Regexp matching a typescript explicit prototype \"class\" declaration. +An example of this is \"Class.prototype = { method1: ...}\".") + +(defconst typescript--module-declaration-re + "^\\s-*\\(?:declare\\|\\(?:export\\(?:\\s-+default\\)?\\)\\)?" + "Regexp matching ambient declaration modifier or export declaration.") + +;; var NewClass = BaseClass.extend( +(defconst typescript--mp-class-decl-re + (concat "^\\s-*var\\s-+" + "\\(" typescript--name-re "\\)" + "\\s-*=\\s-*" + "\\(" typescript--dotted-name-re + "\\)\\.extend\\(?:Final\\)?\\s-*(\\s-*{?\\s-*$")) + +;; var NewClass = Class.create() +(defconst typescript--prototype-obsolete-class-decl-re + (concat "^\\s-*\\(?:var\\s-+\\)?" + "\\(" typescript--dotted-name-re "\\)" + "\\s-*=\\s-*Class\\.create()")) + +(defconst typescript--prototype-objextend-class-decl-re-1 + (concat "^\\s-*Object\\.extend\\s-*(" + "\\(" typescript--dotted-name-re "\\)" + "\\s-*,\\s-*{")) + +(defconst typescript--prototype-objextend-class-decl-re-2 + (concat "^\\s-*\\(?:var\\s-+\\)?" + "\\(" typescript--dotted-name-re "\\)" + "\\s-*=\\s-*Object\\.extend\\s-*\(")) + +;; var NewClass = Class.create({ +(defconst typescript--prototype-class-decl-re + (concat "^\\s-*\\(?:var\\s-+\\)?" + "\\(" typescript--name-re "\\)" + "\\s-*=\\s-*Class\\.create\\s-*(\\s-*" + "\\(?:\\(" typescript--dotted-name-re "\\)\\s-*,\\s-*\\)?{?")) + +;; Parent class name(s) (yes, multiple inheritance in typescript) are +;; matched with dedicated font-lock matchers +(defconst typescript--dojo-class-decl-re + (concat "^\\s-*dojo\\.declare\\s-*(\"\\(" typescript--dotted-name-re "\\)")) + +(defconst typescript--exttypescript-class-decl-re-1 + (concat "^\\s-*Ext\\.extend\\s-*(" + "\\s-*\\(" typescript--dotted-name-re "\\)" + "\\s-*,\\s-*\\(" typescript--dotted-name-re "\\)") + "Regexp matching an ExtTYPESCRIPT class declaration (style 1).") + +(defconst typescript--exttypescript-class-decl-re-2 + (concat "^\\s-*\\(?:var\\s-+\\)?" + "\\(" typescript--name-re "\\)" + "\\s-*=\\s-*Ext\\.extend\\s-*(\\s-*" + "\\(" typescript--dotted-name-re "\\)") + "Regexp matching an ExtTYPESCRIPT class declaration (style 2).") + +(defconst typescript--mochikit-class-re + (concat "^\\s-*MochiKit\\.Base\\.update\\s-*(\\s-*" + "\\(" typescript--dotted-name-re "\\)") + "Regexp matching a MochiKit class declaration.") + +(defconst typescript--dummy-class-style + '(:name "[Automatically Generated Class]")) + +(defconst typescript--class-styles + `((:name "Plain" + :class-decl ,typescript--plain-class-re + :prototype t + :contexts (toplevel) + :framework typescript) + + (:name "MochiKit" + :class-decl ,typescript--mochikit-class-re + :prototype t + :contexts (toplevel) + :framework mochikit) + + (:name "Prototype (Obsolete)" + :class-decl ,typescript--prototype-obsolete-class-decl-re + :contexts (toplevel) + :framework prototype) + + (:name "Prototype (Modern)" + :class-decl ,typescript--prototype-class-decl-re + :contexts (toplevel) + :framework prototype) + + (:name "Prototype (Object.extend)" + :class-decl ,typescript--prototype-objextend-class-decl-re-1 + :prototype t + :contexts (toplevel) + :framework prototype) + + (:name "Prototype (Object.extend) 2" + :class-decl ,typescript--prototype-objextend-class-decl-re-2 + :prototype t + :contexts (toplevel) + :framework prototype) + + (:name "Dojo" + :class-decl ,typescript--dojo-class-decl-re + :contexts (toplevel) + :framework dojo) + + (:name "ExtTYPESCRIPT (style 1)" + :class-decl ,typescript--exttypescript-class-decl-re-1 + :prototype t + :contexts (toplevel) + :framework exttypescript) + + (:name "ExtTYPESCRIPT (style 2)" + :class-decl ,typescript--exttypescript-class-decl-re-2 + :contexts (toplevel) + :framework exttypescript) + + (:name "Merrill Press" + :class-decl ,typescript--mp-class-decl-re + :contexts (toplevel) + :framework merrillpress)) + + "List of typescript class definition styles. + +A class definition style is a plist with the following keys: + +:name is a human-readable name of the class type + +:class-decl is a regular expression giving the start of the +class. Its first group must match the name of its class. If there +is a parent class, the second group should match, and it should be +the name of the class. + +If :prototype is present and non-nil, the parser will merge +declarations for this constructs with others at the same lexical +level that have the same name. Otherwise, multiple definitions +will create multiple top-level entries. Don't use :prototype +unnecessarily: it has an associated cost in performance. + +If :strip-prototype is present and non-nil, then if the class +name as matched contains") + +(defconst typescript--available-frameworks + (cl-loop with available-frameworks + for style in typescript--class-styles + for framework = (plist-get style :framework) + unless (memq framework available-frameworks) + collect framework into available-frameworks + finally return available-frameworks) + "List of available typescript frameworks symbols.") + +(defconst typescript--function-heading-1-re + (concat + typescript--module-declaration-re + "\\s-*function\\s-+\\(" typescript--name-re "\\)") + "Regexp matching the start of a typescript function header. +Match group 1 is the name of the function.") + +(defconst typescript--function-heading-2-re + (concat + "^\\s-*\\(" typescript--name-re "\\)\\s-*:\\s-*function\\_>") + "Regexp matching the start of a function entry in an associative array. +Match group 1 is the name of the function.") + +(defconst typescript--function-heading-3-re + (concat + "^\\s-*\\(?:var\\s-+\\)?\\(" typescript--dotted-name-re "\\)" + "\\s-*=\\s-*function\\_>") + "Regexp matching a line in the typescript form \"var MUMBLE = function\". +Match group 1 is MUMBLE.") + +(defun typescript--regexp-opt-symbol (list) + "Like `regexp-opt', but surround the result with `\\\\_<' and `\\\\_>'." + (concat "\\_<" (regexp-opt list t) "\\_>")) + +(defconst typescript--keyword-re + (typescript--regexp-opt-symbol + '("abstract" "any" "as" "async" "await" "boolean" "bigint" "break" "case" "catch" "class" "const" + "constructor" "continue" "declare" "default" "delete" "do" "else" + "enum" "export" "extends" "extern" "false" "finally" "for" + "function" "from" "get" "goto" "if" "implements" "import" "in" "instanceof" + "interface" "keyof" "let" "module" "namespace" "never" "new" "null" "number" "object" "of" + "private" "protected" "public" "readonly" "return" "set" "static" "string" + "super" "switch" "this" "throw" "true" + "try" "type" "typeof" "unknown" "var" "void" + "while")) ; yield is handled separately + "Regexp matching any typescript keyword.") + +(defconst typescript--basic-type-re + (typescript--regexp-opt-symbol + '("any" "bool" "boolean" "bigint" "never" "number" "string" "unknown" "void")) + "Regular expression matching any predefined type in typescript.") + +(defconst typescript--access-modifier-re + (typescript--regexp-opt-symbol + '("private" "protected" "public" "readonly" "static" "extends" "implements")) + "Regular expression matching access modifiers.") + +(defconst typescript--decorator-re + (concat "\\(@" typescript--name-re "\\)")) + +(defconst typescript--constant-re + (typescript--regexp-opt-symbol '("false" "null" "undefined" + "Infinity" "NaN" + "true" "arguments" "this")) + "Regular expression matching any future reserved words in typescript.") + +(defconst typescript--builtin-re + (typescript--regexp-opt-symbol + '("console")) + "Regular expression matching builtins.") + +(defconst typescript--function-call-re "\\(\\(?:\\w\\|\\s_\\)+\\)\\(<.+>\\)?\s*(" + "Regular expression matching function calls.") + +(defconst typescript--font-lock-keywords-1 + (list + "\\_" + (list typescript--function-heading-1-re 1 font-lock-function-name-face) + (list typescript--function-heading-2-re 1 font-lock-function-name-face)) + "Level one font lock keywords for `typescript-mode'.") + +(defconst typescript--font-lock-keywords-2 + (append typescript--font-lock-keywords-1 + (list (list typescript--keyword-re 1 font-lock-keyword-face) + (list "\\_" + "\\s-+\\(each\\)\\_>" nil nil + (list 1 'font-lock-keyword-face)) + (cons "\\_\\)" 'font-lock-keyword-face) + (cons typescript--basic-type-re font-lock-type-face) + (cons typescript--constant-re font-lock-constant-face))) + "Level two font lock keywords for `typescript-mode'.") + +;; typescript--pitem is the basic building block of the lexical +;; database. When one refers to a real part of the buffer, the region +;; of text to which it refers is split into a conceptual header and +;; body. Consider the (very short) block described by a hypothetical +;; typescript--pitem: +;; +;; function foo(a,b,c) { return 42; } +;; ^ ^ ^ +;; | | | +;; +- h-begin +- h-end +- b-end +;; +;; (Remember that these are buffer positions, and therefore point +;; between characters, not at them. An arrow drawn to a character +;; indicates the corresponding position is between that character and +;; the one immediately preceding it.) +;; +;; The header is the region of text [h-begin, h-end], and is +;; the text needed to unambiguously recognize the start of the +;; construct. If the entire header is not present, the construct is +;; not recognized at all. No other pitems may be nested inside the +;; header. +;; +;; The body is the region [h-end, b-end]. It may contain nested +;; typescript--pitem instances. The body of a pitem may be empty: in +;; that case, b-end is equal to header-end. +;; +;; The three points obey the following relationship: +;; +;; h-begin < h-end <= b-end +;; +;; We put a text property in the buffer on the character *before* +;; h-end, and if we see it, on the character *before* b-end. +;; +;; The text property for h-end, typescript--pstate, is actually a list +;; of all typescript--pitem instances open after the marked character. +;; +;; The text property for b-end, typescript--pend, is simply the +;; typescript--pitem that ends after the marked character. (Because +;; pitems always end when the paren-depth drops below a critical +;; value, and because we can only drop one level per character, only +;; one pitem may end at a given character.) +;; +;; In the structure below, we only store h-begin and (sometimes) +;; b-end. We can trivially and quickly find h-end by going to h-begin +;; and searching for an typescript--pstate text property. Since no other +;; typescript--pitem instances can be nested inside the header of a +;; pitem, the location after the character with this text property +;; must be h-end. +;; +;; typescript--pitem instances are never modified (with the exception +;; of the b-end field). Instead, modified copies are added at subseqnce parse points. +;; (The exception for b-end and its caveats is described below.) +;; + +(cl-defstruct (typescript--pitem (:type list)) + ;; IMPORTANT: Do not alter the position of fields within the list. + ;; Various bits of code depend on their positions, particularly + ;; anything that manipulates the list of children. + + ;; List of children inside this pitem's body + (children nil :read-only t) + + ;; When we reach this paren depth after h-end, the pitem ends + (paren-depth nil :read-only t) + + ;; Symbol or class-style plist if this is a class + (type nil :read-only t) + + ;; See above + (h-begin nil :read-only t) + + ;; List of strings giving the parts of the name of this pitem (e.g., + ;; '("MyClass" "myMethod"), or t if this pitem is anonymous + (name nil :read-only t) + + ;; THIS FIELD IS MUTATED, and its value is shared by all copies of + ;; this pitem: when we copy-and-modify pitem instances, we share + ;; their tail structures, so all the copies actually have the same + ;; terminating cons cell. We modify that shared cons cell directly. + ;; + ;; The field value is either a number (buffer location) or nil if + ;; unknown. + ;; + ;; If the field's value is greater than `typescript--cache-end', the + ;; value is stale and must be treated as if it were nil. Conversely, + ;; if this field is nil, it is guaranteed that this pitem is open up + ;; to at least `typescript--cache-end'. (This property is handy when + ;; computing whether we're inside a given pitem.) + ;; + (b-end nil)) + +;; The pitem we start parsing with. +(defconst typescript--initial-pitem + (make-typescript--pitem + :paren-depth most-negative-fixnum + :type 'toplevel)) + +;; When we say "jsdoc" here, we mean "jsdoc 3". There exist multiple dialects of +;; "jsdoc documentation". + +;; Note that all typedoc/jsdoc regexp by themselves would match occurrences that appear outside +;; documentation comments. The logic that uses these regexps must guard against it. +(defconst typescript-typedoc-link-tag-regexp + "\\[\\[.*?\\]\\]" + "Matches a typedoc link.") + +(defconst typescript-typedoc-literal-markup-regexp + "\\(`+\\).*?\\1" + "Matches a typedoc keyword markup.") + +(defconst typescript-jsdoc-before-tag-regexp + "\\(?:^\\s-*\\*+\\|/\\*\\*\\)\\s-*" + "Matches everything we allow before the @ of a jsdoc tag.") + +;; This was taken from js2-mode. +(defconst typescript-jsdoc-param-tag-regexp + (concat typescript-jsdoc-before-tag-regexp + "\\(@" + (regexp-opt + '("arg" + "argument" + "param" + "prop" + "property" + "typedef")) + "\\)" + "\\s-*\\({[^}]+}\\)?" ; optional type + "\\s-*\\[?\\([[:alnum:]_$\.]+\\)?\\]?" ; name + "\\_>") + "Matches jsdoc tags with optional type and optional param name.") + +;; This was taken from js2-mode. +;; and extended with tags in http://usejsdoc.org/ +(defconst typescript-jsdoc-typed-tag-regexp + (concat typescript-jsdoc-before-tag-regexp + "\\(@" + (regexp-opt + '("enum" + "extends" + "field" + "id" + "implements" + "lends" + "mods" + "requires" + "return" + "returns" + "throw" + "throws" + "type" + "yield" + "yields")) + "\\)\\s-*\\({[^}]+}\\)?") + "Matches jsdoc tags with optional type.") + +;; This was taken from js2-mode. +;; and extended with tags in http://usejsdoc.org/ +(defconst typescript-jsdoc-arg-tag-regexp + (concat typescript-jsdoc-before-tag-regexp + "\\(@" + (regexp-opt + '("access" + "alias" + "augments" + "base" + "borrows" + "bug" + "callback" + "config" + "default" + "define" + "emits" + "exception" + "extends" + "external" + "fires" + "func" + "function" + "host" + "kind" + "listens" + "member" + "memberof" + "method" + "mixes" + "module" + "name" + "namespace" + "requires" + "since" + "suppress" + "this" + "throws" + "var" + "variation" + "version")) + "\\)\\s-+\\([^ \t]+\\)") + "Matches jsdoc tags with a single argument.") + +;; This was taken from js2-mode +;; and extended with tags in http://usejsdoc.org/ +(defconst typescript-jsdoc-empty-tag-regexp + (concat typescript-jsdoc-before-tag-regexp + "\\(@" + (regexp-opt + '("abstract" + "addon" + "async" + "author" + "class" + "classdesc" + "const" + "constant" + "constructor" + "constructs" + "copyright" + "default" + "defaultvalue" + "deprecated" + "desc" + "description" + "event" + "example" + "exec" + "export" + "exports" + "file" + "fileoverview" + "final" + "func" + "function" + "generator" + "global" + "hidden" + "hideconstructor" + "ignore" + "implicitcast" + "inheritdoc" + "inner" + "instance" + "interface" + "license" + "method" + "mixin" + "noalias" + "noshadow" + "notypecheck" + "override" + "overview" + "owner" + "package" + "preserve" + "preservetry" + "private" + "protected" + "public" + "readonly" + "static" + "summary" + "supported" + "todo" + "tutorial" + "virtual")) + "\\)\\s-*") + "Matches empty jsdoc tags.") + +;; Note that this regexp by itself would match tslint flags that appear inside +;; strings. The logic using this regexp must guard against it. +(defconst typescript-tslint-flag-regexp + "\\(?://\\|/\\*\\)\\s-*\\(tslint:.*?\\)\\(?:\\*/\\|$\\)" + "Matches tslint flags.") + +;;; Faces + +(defface typescript-jsdoc-tag + '((t :foreground "SlateGray")) + "Face used to highlight @whatever tags in jsdoc comments." + :group 'typescript) + +(defface typescript-jsdoc-type + '((t :foreground "SteelBlue")) + "Face used to highlight {FooBar} types in jsdoc comments." + :group 'typescript) + +(defface typescript-jsdoc-value + '((t :foreground "gold4")) + "Face used to highlight tag values in jsdoc comments." + :group 'typescript) + +(defface typescript-access-modifier-face + '((t (:inherit font-lock-keyword-face))) + "Face used to highlight access modifiers." + :group 'typescript) + +(defface typescript-this-face + '((t (:inherit font-lock-keyword-face))) + "Face used to highlight 'this' keyword." + :group 'typescript) + +(defface typescript-primitive-face + '((t (:inherit font-lock-keyword-face))) + "Face used to highlight builtin types." + :group 'typescript) + +;;; User Customization + +(defgroup typescript nil + "Customization variables for typescript mode." + :tag "typescript" + :group 'languages) + +(defcustom typescript-indent-level 4 + "Number of spaces for each indentation step in `typescript-mode'." + :type 'integer + :safe 'integerp + :group 'typescript) +;;;###autoload(put 'typescript-indent-level 'safe-local-variable #'integerp) + +(defcustom typescript-expr-indent-offset 0 + "Number of additional spaces used for indentation of continued expressions. +The value must be no less than minus `typescript-indent-level'." + :type 'integer + :safe 'integerp + :group 'typescript) + +(defcustom typescript-indent-switch-clauses t + "Enable indenting of switch case and default clauses to +replicate tsserver behaviour. Indent level is taken to be +`typescript-indent-level'." + :type 'boolean + :group 'typescript) + +(defcustom typescript-auto-indent-flag t + "Whether to automatically indent when typing punctuation characters. +If non-nil, the characters {}();,: also indent the current line +in typescript mode." + :type 'boolean + :group 'typescript) + +(defcustom typescript-flat-functions nil + "Treat nested functions as top-level functions in `typescript-mode'. +This applies to function movement, marking, and so on." + :type 'boolean + :group 'typescript) + +(defcustom typescript-comment-lineup-func #'c-lineup-C-comments + "Lineup function for `cc-mode-style', for C comments in `typescript-mode'." + :type 'function + :group 'typescript) + +(defcustom typescript-enabled-frameworks typescript--available-frameworks + "Frameworks recognized by `typescript-mode'. +To improve performance, you may turn off some frameworks you +seldom use, either globally or on a per-buffer basis." + :type (cons 'set (mapcar (lambda (x) + (list 'const x)) + typescript--available-frameworks)) + :group 'typescript) + +(defcustom typescript-mode-hook nil + "*Hook called by `typescript-mode'." + :type 'hook + :group 'typescript) + +(defcustom typescript-autoconvert-to-template-flag nil + "Non-nil means automatically convert plain strings to templates. + +When the flag is non-nil the `typescript-autoconvert-to-template' +is called whenever a plain string delimiter is typed in the buffer." + :type 'boolean + :group 'typescript) + +;;; Public utilities + +(defun typescript-convert-to-template () + "Convert the string at point to a template string." + (interactive) + (save-restriction + (widen) + (save-excursion + (let* ((syntax (syntax-ppss)) + (str-terminator (nth 3 syntax)) + (string-start (or (and str-terminator (nth 8 syntax)) + ;; We have to consider the case that we're on the start delimiter of a string. + ;; We tentatively take (point) as string-start. If it turns out we're + ;; wrong, then typescript--move-to-end-of-plain-string will fail anway, + ;; and we won't use the bogus value. + (progn + (forward-char) + (point))))) + (when (typescript--move-to-end-of-plain-string) + (let ((end-start (or (nth 8 (syntax-ppss)) -1))) + (undo-boundary) + (when (= end-start string-start) + (delete-char 1) + (insert "`"))) + (goto-char string-start) + (delete-char 1) + (insert "`")))))) + +(defun typescript-autoconvert-to-template () + "Automatically convert a plain string to a teplate string, if needed. + +This function is meant to be automatically invoked when the user +enters plain string delimiters. It checks whether the character +before point is the end of a string. If it is, then it checks +whether the string contains ${...}. If it does, then it converts +the string from a plain string to a template." + (interactive) + (save-restriction + (widen) + (save-excursion + (backward-char) + (when (and (memq (char-after) '(?' ?\")) + (not (eq (char-before) ?\\))) + (let* ((string-start (nth 8 (syntax-ppss)))) + (when (and string-start + (save-excursion + (re-search-backward "\\${.*?}" string-start t))) + (typescript-convert-to-template))))))) + +;;; KeyMap + +(defvar typescript-mode-map + (let ((keymap (make-sparse-keymap))) + (define-key keymap (kbd "C-c '") #'typescript-convert-to-template) + keymap) + "Keymap for `typescript-mode'.") + +(defun typescript--post-self-insert-function () + (when (and (derived-mode-p 'typescript-mode) + typescript-autoconvert-to-template-flag + (or (eq last-command-event ?\') + (eq last-command-event ?\"))) + (typescript-autoconvert-to-template))) + +;;; Syntax table and parsing + +(defvar typescript-mode-syntax-table + (let ((table (make-syntax-table))) + (c-populate-syntax-table table) + (modify-syntax-entry ?$ "_" table) + (modify-syntax-entry ?` "\"" table) + table) + "Syntax table for `typescript-mode'.") + +(defvar typescript--quick-match-re nil + "Autogenerated regexp used by `typescript-mode' to match buffer constructs.") + +(defvar typescript--quick-match-re-func nil + "Autogenerated regexp used by `typescript-mode' to match constructs and functions.") + +(make-variable-buffer-local 'typescript--quick-match-re) +(make-variable-buffer-local 'typescript--quick-match-re-func) + +(defvar typescript--cache-end 1 + "Last valid buffer position for the `typescript-mode' function cache.") +(make-variable-buffer-local 'typescript--cache-end) + +(defvar typescript--last-parse-pos nil + "Latest parse position reached by `typescript--ensure-cache'.") +(make-variable-buffer-local 'typescript--last-parse-pos) + +(defvar typescript--state-at-last-parse-pos nil + "Parse state at `typescript--last-parse-pos'.") +(make-variable-buffer-local 'typescript--state-at-last-parse-pos) + +(defun typescript--flatten-list (list) + (cl-loop for item in list + nconc (cond ((consp item) + (typescript--flatten-list item)) + (item (list item))))) + +(defun typescript--maybe-join (prefix separator suffix &rest list) + "Helper function for `typescript--update-quick-match-re'. +If LIST contains any element that is not nil, return its non-nil +elements, separated by SEPARATOR, prefixed by PREFIX, and ended +with SUFFIX as with `concat'. Otherwise, if LIST is empty, return +nil. If any element in LIST is itself a list, flatten that +element." + (setq list (typescript--flatten-list list)) + (when list + (concat prefix (mapconcat #'identity list separator) suffix))) + +(defun typescript--update-quick-match-re () + "Internal function used by `typescript-mode' for caching buffer constructs. +This updates `typescript--quick-match-re', based on the current set of +enabled frameworks." + (setq typescript--quick-match-re + (typescript--maybe-join + "^[ \t]*\\(?:" "\\|" "\\)" + + ;; #define mumble + "#define[ \t]+[a-zA-Z_]" + + (when (memq 'exttypescript typescript-enabled-frameworks) + "Ext\\.extend") + + (when (memq 'prototype typescript-enabled-frameworks) + "Object\\.extend") + + ;; var mumble = THING ( + (typescript--maybe-join + "\\(?:var[ \t]+\\)?[a-zA-Z_$0-9.]+[ \t]*=[ \t]*\\(?:" + "\\|" + "\\)[ \t]*\(" + + (when (memq 'prototype typescript-enabled-frameworks) + "Class\\.create") + + (when (memq 'exttypescript typescript-enabled-frameworks) + "Ext\\.extend") + + (when (memq 'merrillpress typescript-enabled-frameworks) + "[a-zA-Z_$0-9]+\\.extend\\(?:Final\\)?")) + + (when (memq 'dojo typescript-enabled-frameworks) + "dojo\\.declare[ \t]*\(") + + (when (memq 'mochikit typescript-enabled-frameworks) + "MochiKit\\.Base\\.update[ \t]*\(") + + ;; mumble.prototypeTHING + (typescript--maybe-join + "[a-zA-Z_$0-9.]+\\.prototype\\(?:" "\\|" "\\)" + + (when (memq 'typescript typescript-enabled-frameworks) + '( ;; foo.prototype.bar = function( + "\\.[a-zA-Z_$0-9]+[ \t]*=[ \t]*function[ \t]*\(" + + ;; mumble.prototype = { + "[ \t]*=[ \t]*{"))))) + + (setq typescript--quick-match-re-func + (concat "function\\|" typescript--quick-match-re))) + +(defun typescript--forward-text-property (propname) + "Move over the next value of PROPNAME in the buffer. +If found, return that value and leave point after the character +having that value; otherwise, return nil and leave point at EOB." + (let ((next-value (get-text-property (point) propname))) + (if next-value + (forward-char) + + (goto-char (next-single-property-change + (point) propname nil (point-max))) + (unless (eobp) + (setq next-value (get-text-property (point) propname)) + (forward-char))) + + next-value)) + +(defun typescript--backward-text-property (propname) + "Move over the previous value of PROPNAME in the buffer. +If found, return that value and leave point just before the +character that has that value, otherwise return nil and leave +point at BOB." + (unless (bobp) + (let ((prev-value (get-text-property (1- (point)) propname))) + (if prev-value + (backward-char) + + (goto-char (previous-single-property-change + (point) propname nil (point-min))) + + (unless (bobp) + (backward-char) + (setq prev-value (get-text-property (point) propname)))) + + prev-value))) + +(defsubst typescript--forward-pstate () + (typescript--forward-text-property 'typescript--pstate)) + +(defsubst typescript--backward-pstate () + (typescript--backward-text-property 'typescript--pstate)) + +(defun typescript--pitem-goto-h-end (pitem) + (goto-char (typescript--pitem-h-begin pitem)) + (typescript--forward-pstate)) + +(defun typescript--re-search-forward-inner (regexp &optional bound count) + "Helper function for `typescript--re-search-forward'." + (let ((parse) + str-terminator) + (while (> count 0) + (re-search-forward regexp bound) + (setq parse (syntax-ppss)) + (cond ((setq str-terminator (nth 3 parse)) + (when (eq str-terminator t) + (setq str-terminator ?/)) + (re-search-forward + (concat "\\([^\\]\\|^\\)" (string str-terminator)) + (save-excursion (end-of-line) (point)) t)) + ((nth 7 parse) + (forward-line)) + ((or (nth 4 parse) + (and (eq (char-before) ?\/) (eq (char-after) ?\*))) + (re-search-forward "\\*/")) + (t + (setq count (1- count)))))) + (point)) + + +(defun typescript--re-search-forward (regexp &optional bound noerror count) + "Search forward, ignoring strings and comments. +This function invokes `re-search-forward', but treats the buffer +as if strings and comments have been removed." + (let ((saved-point (point)) + (search-expr + (cond ((null count) + '(typescript--re-search-forward-inner regexp bound 1)) + ((< count 0) + '(typescript--re-search-backward-inner regexp bound (- count))) + ((> count 0) + '(typescript--re-search-forward-inner regexp bound count))))) + (condition-case err + (eval search-expr) + (search-failed + (goto-char saved-point) + (unless noerror + (error (error-message-string err))))))) + + +(defun typescript--re-search-backward-inner (regexp &optional bound count) + "Auxiliary function for `typescript--re-search-backward'." + (let ((parse)) + (while (> count 0) + (re-search-backward regexp bound) + (when (and (> (point) (point-min)) + (save-excursion (backward-char) (looking-at "/[/*]"))) + (forward-char)) + (setq parse (syntax-ppss)) + (cond + ;; If we are in a comment or a string, jump back to the start + ;; of the comment or string. + ((nth 8 parse) + (goto-char (nth 8 parse))) + ((and (eq (char-before) ?/) (eq (char-after) ?*)) + (re-search-backward "/\\*")) + (t + (setq count (1- count)))))) + (point)) + + +(defun typescript--re-search-backward (regexp &optional bound noerror count) + "Search backward, ignoring strings, and comments. + +This function invokes `re-search-backward' but treats the buffer +as if strings and comments have been removed. + +IMPORTANT NOTE: searching for \"\\n\" with this function to find +line breaks will generally not work, because the final newline of +a one-line comment is considered to be part of the comment and +will be skipped. Take the following code: + + let a = 1; + let b = 2; // Foo + let c = 3; + +If the point is in the last line, searching back for \"\\n\" will +skip over the line with \"let b\". The newline found will be the +one at the end of the line with \"let a\"." + (let ((saved-point (point)) + (search-expr + (cond ((null count) + '(typescript--re-search-backward-inner regexp bound 1)) + ((< count 0) + '(typescript--re-search-forward-inner regexp bound (- count))) + ((> count 0) + '(typescript--re-search-backward-inner regexp bound count))))) + (condition-case err + (eval search-expr) + (search-failed + (goto-char saved-point) + (unless noerror + (error (error-message-string err))))))) + +(defun typescript--forward-expression () + "Move forward over a whole typescript expression. +This function doesn't move over expressions continued across +lines." + (cl-loop + do (progn + (forward-comment most-positive-fixnum) + (cl-loop until (or (eolp) + (progn + (forward-comment most-positive-fixnum) + (memq (char-after) '(?\, ?\; ?\] ?\) ?\})))) + do (forward-sexp))) + while (and (eq (char-after) ?\n) + (save-excursion + (forward-char) + (typescript--continued-expression-p))))) + +(defun typescript--forward-function-decl () + "Move forward over a typescript function declaration. +This puts point at the 'function' keyword. + +If this is a syntactically-correct non-expression function, +return the name of the function, or t if the name could not be +determined. Otherwise, return nil." + (cl-assert (looking-at "\\_")) + (let ((name t)) + (forward-word) + (forward-comment most-positive-fixnum) + (when (looking-at typescript--name-re) + (setq name (match-string-no-properties 0)) + (goto-char (match-end 0))) + (forward-comment most-positive-fixnum) + (and (eq (char-after) ?\( ) + (ignore-errors (forward-list) t) + (progn (forward-comment most-positive-fixnum) + (and (eq (char-after) ?{) + name))))) + +(defun typescript--function-prologue-beginning (&optional pos) + "Return the start of the typescript function prologue containing POS. +A function prologue is everything from start of the definition up +to and including the opening brace. POS defaults to point. +If POS is not in a function prologue, return nil." + (let (prologue-begin) + (save-excursion + (if pos + (goto-char pos) + (setq pos (point))) + + (when (save-excursion + (forward-line 0) + (or (looking-at typescript--function-heading-2-re) + (looking-at typescript--function-heading-3-re))) + + (setq prologue-begin (match-beginning 1)) + (when (<= prologue-begin pos) + (goto-char (match-end 0)))) + + (skip-syntax-backward "w_") + (and (or (looking-at "\\_") + (typescript--re-search-backward "\\_" nil t)) + + (save-match-data (goto-char (match-beginning 0)) + (typescript--forward-function-decl)) + + (<= pos (point)) + (or prologue-begin (match-beginning 0)))))) + +(defun typescript--beginning-of-defun-raw () + "Helper function for `typescript-beginning-of-defun'. +Go to previous defun-beginning and return the parse state for it, +or nil if we went all the way back to bob and don't find +anything." + (typescript--ensure-cache) + (let (pstate) + (while (and (setq pstate (typescript--backward-pstate)) + (not (eq 'function (typescript--pitem-type (car pstate)))))) + (and (not (bobp)) pstate))) + +(defun typescript--pstate-is-toplevel-defun (pstate) + "Helper function for `typescript--beginning-of-defun-nested'. +If PSTATE represents a non-empty top-level defun, return the +top-most pitem. Otherwise, return nil." + (cl-loop for pitem in pstate + with func-depth = 0 + with func-pitem + if (eq 'function (typescript--pitem-type pitem)) + do (cl-incf func-depth) + and do (setq func-pitem pitem) + finally return (if (eq func-depth 1) func-pitem))) + +(defun typescript--beginning-of-defun-nested () + "Helper function for `typescript--beginning-of-defun'. +Return the pitem of the function we went to the beginning of." + (or + ;; Look for the smallest function that encloses point... + (cl-loop for pitem in (typescript--parse-state-at-point) + if (and (eq 'function (typescript--pitem-type pitem)) + (typescript--inside-pitem-p pitem)) + do (goto-char (typescript--pitem-h-begin pitem)) + and return pitem) + + ;; ...and if that isn't found, look for the previous top-level + ;; defun + (cl-loop for pstate = (typescript--backward-pstate) + while pstate + if (typescript--pstate-is-toplevel-defun pstate) + do (goto-char (typescript--pitem-h-begin it)) + and return it))) + +(defun typescript--beginning-of-defun-flat () + "Helper function for `typescript-beginning-of-defun'." + (let ((pstate (typescript--beginning-of-defun-raw))) + (when pstate + (goto-char (typescript--pitem-h-begin (car pstate)))))) + +(defun typescript-beginning-of-defun (&optional arg) + "Value of `beginning-of-defun-function' for `typescript-mode'." + (setq arg (or arg 1)) + (while (and (not (eobp)) (< arg 0)) + (cl-incf arg) + (when (and (not typescript-flat-functions) + (or (eq (typescript-syntactic-context) 'function) + (typescript--function-prologue-beginning))) + (typescript-end-of-defun)) + + (if (typescript--re-search-forward + "\\_" nil t) + (goto-char (typescript--function-prologue-beginning)) + (goto-char (point-max)))) + + (while (> arg 0) + (cl-decf arg) + ;; If we're just past the end of a function, the user probably wants + ;; to go to the beginning of *that* function + (when (eq (char-before) ?}) + (backward-char)) + + (let ((prologue-begin (typescript--function-prologue-beginning))) + (cond ((and prologue-begin (< prologue-begin (point))) + (goto-char prologue-begin)) + + (typescript-flat-functions + (typescript--beginning-of-defun-flat)) + (t + (typescript--beginning-of-defun-nested)))))) + +(defun typescript--flush-caches (&optional beg ignored) + "Flush the `typescript-mode' syntax cache after position BEG. +BEG defaults to `point-min', meaning to flush the entire cache." + (interactive) + (setq beg (or beg (save-restriction (widen) (point-min)))) + (setq typescript--cache-end (min typescript--cache-end beg))) + +(defmacro typescript--debug (&rest arguments) + ;; `(message ,@arguments) + ) + +(defun typescript--ensure-cache--pop-if-ended (open-items paren-depth) + (let ((top-item (car open-items))) + (when (<= paren-depth (typescript--pitem-paren-depth top-item)) + (cl-assert (not (get-text-property (1- (point)) 'typescript-pend))) + (put-text-property (1- (point)) (point) 'typescript--pend top-item) + (setf (typescript--pitem-b-end top-item) (point)) + (setq open-items + ;; open-items must contain at least two items for this to + ;; work, but because we push a dummy item to start with, + ;; that assumption holds. + (cons (typescript--pitem-add-child (cl-second open-items) top-item) + (cddr open-items))))) + open-items) + +(defmacro typescript--ensure-cache--update-parse () + "Helper function for `typescript--ensure-cache'. +Update parsing information up to point, referring to parse, +prev-parse-point, goal-point, and open-items bound lexically in +the body of `typescript--ensure-cache'." + `(progn + (setq goal-point (point)) + (goto-char prev-parse-point) + (while (progn + (setq open-items (typescript--ensure-cache--pop-if-ended + open-items (car parse))) + ;; Make sure parse-partial-sexp doesn't stop because we *entered* + ;; the given depth -- i.e., make sure we're deeper than the target + ;; depth. + (cl-assert (> (nth 0 parse) + (typescript--pitem-paren-depth (car open-items)))) + (setq parse (parse-partial-sexp + prev-parse-point goal-point + (typescript--pitem-paren-depth (car open-items)) + nil parse)) + +;; (let ((overlay (make-overlay prev-parse-point (point)))) +;; (overlay-put overlay 'face '(:background "red")) +;; (unwind-protect +;; (progn +;; (typescript--debug "parsed: %S" parse) +;; (sit-for 1)) +;; (delete-overlay overlay))) + + (setq prev-parse-point (point)) + (< (point) goal-point))) + + (setq open-items (typescript--ensure-cache--pop-if-ended + open-items (car parse))))) + +(defun typescript--show-cache-at-point () + (interactive) + (require 'pp) + (let ((prop (get-text-property (point) 'typescript--pstate))) + (with-output-to-temp-buffer "*Help*" + (pp prop)))) + +(defun typescript--split-name (string) + "Split a typescript name into its dot-separated parts. +This also removes any prototype parts from the split name +\(unless the name is just \"prototype\" to start with)." + (let ((name (save-match-data + (split-string string "\\." t)))) + (unless (and (= (length name) 1) + (equal (car name) "prototype")) + + (setq name (remove "prototype" name))))) + +(defvar typescript--guess-function-name-start nil) + +(defun typescript--guess-function-name (position) + "Guess the name of the typescript function at POSITION. +POSITION should be just after the end of the word \"function\". +Return the name of the function, or nil if the name could not be +guessed. + +This function clobbers match data. If we find the preamble +begins earlier than expected while guessing the function name, +set `typescript--guess-function-name-start' to that position; otherwise, +set that variable to nil." + (setq typescript--guess-function-name-start nil) + (save-excursion + (goto-char position) + (forward-line 0) + (cond + ((looking-at typescript--function-heading-3-re) + (and (eq (match-end 0) position) + (setq typescript--guess-function-name-start (match-beginning 1)) + (match-string-no-properties 1))) + + ((looking-at typescript--function-heading-2-re) + (and (eq (match-end 0) position) + (setq typescript--guess-function-name-start (match-beginning 1)) + (match-string-no-properties 1)))))) + +(defun typescript--clear-stale-cache () + ;; Clear any endings that occur after point + (let (end-prop) + (save-excursion + (while (setq end-prop (typescript--forward-text-property + 'typescript--pend)) + (setf (typescript--pitem-b-end end-prop) nil)))) + + ;; Remove any cache properties after this point + (remove-text-properties (point) (point-max) + '(typescript--pstate t typescript--pend t))) + +(defun typescript--ensure-cache (&optional limit) + "Ensures brace cache is valid up to the character before LIMIT. +LIMIT defaults to point." + (setq limit (or limit (point))) + (when (< typescript--cache-end limit) + + (c-save-buffer-state + (open-items + orig-match-start + orig-match-end + orig-depth + parse + prev-parse-point + name + case-fold-search + filtered-class-styles + new-item + goal-point + end-prop) + + ;; Figure out which class styles we need to look for + (setq filtered-class-styles + (cl-loop for style in typescript--class-styles + if (memq (plist-get style :framework) + typescript-enabled-frameworks) + collect style)) + + (save-excursion + (save-restriction + (widen) + + ;; Find last known good position + (goto-char typescript--cache-end) + (unless (bobp) + (setq open-items (get-text-property + (1- (point)) 'typescript--pstate)) + + (unless open-items + (goto-char (previous-single-property-change + (point) 'typescript--pstate nil (point-min))) + + (unless (bobp) + (setq open-items (get-text-property (1- (point)) + 'typescript--pstate)) + (cl-assert open-items)))) + + (unless open-items + ;; Make a placeholder for the top-level definition + (setq open-items (list typescript--initial-pitem))) + + (setq parse (syntax-ppss)) + (setq prev-parse-point (point)) + + (typescript--clear-stale-cache) + + (narrow-to-region (point-min) limit) + + (cl-loop while (re-search-forward typescript--quick-match-re-func nil t) + for orig-match-start = (goto-char (match-beginning 0)) + for orig-match-end = (match-end 0) + do (typescript--ensure-cache--update-parse) + for orig-depth = (nth 0 parse) + + ;; Each of these conditions should return non-nil if + ;; we should add a new item and leave point at the end + ;; of the new item's header (h-end in the + ;; typescript--pitem diagram). This point is the one + ;; after the last character we need to unambiguously + ;; detect this construct. If one of these evaluates to + ;; nil, the location of the point is ignored. + if (cond + ;; In comment or string + ((nth 8 parse) nil) + + ;; Regular function declaration + ((and (looking-at "\\_") + (setq name (typescript--forward-function-decl))) + + (when (eq name t) + (setq name (typescript--guess-function-name orig-match-end)) + (if name + (when typescript--guess-function-name-start + (setq orig-match-start + typescript--guess-function-name-start)) + + (setq name t))) + + (cl-assert (eq (char-after) ?{)) + (forward-char) + (make-typescript--pitem + :paren-depth orig-depth + :h-begin orig-match-start + :type 'function + :name (if (eq name t) + name + (typescript--split-name name)))) + + ;; "Prototype function" declaration + ((looking-at typescript--plain-method-re) + (goto-char (match-beginning 3)) + (when (save-match-data + (typescript--forward-function-decl)) + (forward-char) + (make-typescript--pitem + :paren-depth orig-depth + :h-begin orig-match-start + :type 'function + :name (nconc (typescript--split-name + (match-string-no-properties 1)) + (list (match-string-no-properties 2)))))) + + ;; Class definition + ((cl-loop with syntactic-context = + (typescript--syntactic-context-from-pstate open-items) + for class-style in filtered-class-styles + if (and (memq syntactic-context + (plist-get class-style :contexts)) + (looking-at (plist-get class-style + :class-decl))) + do (goto-char (match-end 0)) + and return + (make-typescript--pitem + :paren-depth orig-depth + :h-begin orig-match-start + :type class-style + :name (typescript--split-name + (match-string-no-properties 1)))))) + + do (typescript--ensure-cache--update-parse) + and do (push it open-items) + and do (put-text-property + (1- (point)) (point) 'typescript--pstate open-items) + else do (goto-char orig-match-end)) + + (goto-char limit) + (typescript--ensure-cache--update-parse) + (setq typescript--cache-end limit) + (setq typescript--last-parse-pos limit) + (setq typescript--state-at-last-parse-pos open-items)))))) + +(defun typescript--end-of-defun-flat () + "Helper function for `typescript-end-of-defun'." + (cl-loop while (typescript--re-search-forward "}" nil t) + do (typescript--ensure-cache) + if (get-text-property (1- (point)) 'typescript--pend) + if (eq 'function (typescript--pitem-type it)) + return t + finally do (goto-char (point-max)))) + +(defun typescript--end-of-defun-nested () + "Helper function for `typescript-end-of-defun'." + (let* (pitem + (this-end (save-excursion + (and (setq pitem (typescript--beginning-of-defun-nested)) + (typescript--pitem-goto-h-end pitem) + (progn (backward-char) + (forward-list) + (point))))) + found) + + (if (and this-end (< (point) this-end)) + ;; We're already inside a function; just go to its end. + (goto-char this-end) + + ;; Otherwise, go to the end of the next function... + (while (and (typescript--re-search-forward "\\_" nil t) + (not (setq found (progn + (goto-char (match-beginning 0)) + (typescript--forward-function-decl)))))) + + (if found (forward-list) + ;; ... or eob. + (goto-char (point-max)))))) + +(defun typescript-end-of-defun (&optional arg) + "Value of `end-of-defun-function' for `typescript-mode'." + (setq arg (or arg 1)) + (while (and (not (bobp)) (< arg 0)) + (cl-incf arg) + (typescript-beginning-of-defun) + (typescript-beginning-of-defun) + (unless (bobp) + (typescript-end-of-defun))) + + (while (> arg 0) + (cl-decf arg) + ;; look for function backward. if we're inside it, go to that + ;; function's end. otherwise, search for the next function's end and + ;; go there + (if typescript-flat-functions + (typescript--end-of-defun-flat) + + ;; if we're doing nested functions, see whether we're in the + ;; prologue. If we are, go to the end of the function; otherwise, + ;; call typescript--end-of-defun-nested to do the real work + (let ((prologue-begin (typescript--function-prologue-beginning))) + (cond ((and prologue-begin (<= prologue-begin (point))) + (goto-char prologue-begin) + (re-search-forward "\\_" + (1 font-lock-constant-face)) + + (,(rx symbol-start "class" (+ space) (group (+ (or (syntax word) (syntax symbol))))) + (1 font-lock-type-face)) + + (,(rx symbol-start "extends" (+ space) (group (+ (or (syntax word) (syntax symbol))))) + (1 font-lock-type-face)) + + (,(rx symbol-start "implements" (+ space)) + (,(rx symbol-start (+ (syntax word))) nil nil (0 font-lock-type-face))) + + (,(rx symbol-start "interface" (+ space) (group (+ (or (syntax word) (syntax symbol))))) + (1 font-lock-type-face)) + + (,(rx symbol-start "type" (+ space) (group (+ (or (syntax word) (syntax symbol))))) + (1 font-lock-type-face)) + + (,(rx symbol-start "enum" (+ space) (group (+ (or (syntax word) (syntax symbol))))) + (1 font-lock-type-face)) + + ;; Highlights class being declared, in parts + (typescript--class-decl-matcher + ,(concat "\\(" typescript--name-re "\\)\\(?:\\.\\|.*$\\)") + (goto-char (match-beginning 1)) + nil + (1 font-lock-type-face)) + + ;; Highlights parent class, in parts, if available + (typescript--class-decl-matcher + ,(concat "\\(" typescript--name-re "\\)\\(?:\\.\\|.*$\\)") + (if (match-beginning 2) + (progn + (setq typescript--tmp-location (match-end 2)) + (goto-char typescript--tmp-location) + (insert "=") + (goto-char (match-beginning 2))) + (setq typescript--tmp-location nil) + (goto-char (point-at-eol))) + (when typescript--tmp-location + (save-excursion + (goto-char typescript--tmp-location) + (delete-char 1))) + (1 font-lock-type-face)) + + ;; Highlights parent class + (typescript--class-decl-matcher + (2 font-lock-type-face nil t)) + + ;; Dojo needs its own matcher to override the string highlighting + (,(typescript--make-framework-matcher + 'dojo + "^\\s-*dojo\\.declare\\s-*(\"" + "\\(" typescript--dotted-name-re "\\)" + "\\(?:\"\\s-*,\\s-*\\(" typescript--dotted-name-re "\\)\\)?") + (1 font-lock-type-face t) + (2 font-lock-type-face nil t)) + + ;; Match Dojo base classes. Of course Mojo has to be different + ;; from everything else under the sun... + (,(typescript--make-framework-matcher + 'dojo + "^\\s-*dojo\\.declare\\s-*(\"" + "\\(" typescript--dotted-name-re "\\)\"\\s-*,\\s-*\\[") + ,(concat "[[,]\\s-*\\(" typescript--dotted-name-re "\\)\\s-*" + "\\(?:\\].*$\\)?") + (backward-char) + (end-of-line) + (1 font-lock-type-face)) + + ;; continued Dojo base-class list + (,(typescript--make-framework-matcher + 'dojo + "^\\s-*" typescript--dotted-name-re "\\s-*[],]") + ,(concat "\\(" typescript--dotted-name-re "\\)" + "\\s-*\\(?:\\].*$\\)?") + (if (save-excursion (backward-char) + (typescript--inside-dojo-class-list-p)) + (forward-symbol -1) + (end-of-line)) + (end-of-line) + (1 font-lock-type-face)) + + ;; variable declarations + ,(list + (concat "\\_<\\(const\\|var\\|let\\)\\_>\\|" typescript--basic-type-re) + (list #'typescript--variable-decl-matcher nil nil nil)) + + ;; class instantiation + ,(list + (concat "\\_\\s-+\\(" typescript--dotted-name-re "\\)") + (list 1 'font-lock-type-face)) + + ;; instanceof + ,(list + (concat "\\_\\s-+\\(" typescript--dotted-name-re "\\)") + (list 1 'font-lock-type-face)) + + ;; formal parameters + ,(list + (concat + "\\_\\(\\s-+" typescript--name-re "\\)?\\s-*\\(<.*>\\)?\\s-*(\\s-*" + typescript--name-start-re) + (list (concat "\\(" typescript--name-re "\\)\\(\\s-*).*\\)?") + '(backward-char) + '(end-of-line) + '(1 font-lock-variable-name-face))) + + ;; continued formal parameter list + ,(list + (concat + "^\\s-*" typescript--name-re "\\s-*[,)]") + (list typescript--name-re + '(if (save-excursion (backward-char) + (typescript--inside-param-list-p)) + (forward-symbol -1) + (end-of-line)) + '(end-of-line) + '(0 font-lock-variable-name-face)))) + "Level three font lock for `typescript-mode'.") + +(defun typescript--flyspell-mode-predicate () + "A custom predicate to help `flyspell-prog-mode' determine whether a word should be checked." + ;; We depend on fontification for our results. font-lock-ensure is defined on + ;; Emacs 25 and over. Earlier versions use font-lock-fontify-buffer. + (if (fboundp 'font-lock-ensure) + (font-lock-ensure) + (font-lock-fontify-buffer)) + (and + ;; Check with the default method that flyspell provides. + (flyspell-generic-progmode-verify) + + ;; + ;; And eliminate cases specific to our mode we don't want to have + ;; spell-checked. + ;; + + ;; Don't check the module names in import statements. + (save-excursion + (not (let* ((parse (syntax-ppss (1- (point)))) + (string-start-pos (and (nth 3 parse) + (nth 8 parse)))) + (and string-start-pos + (save-match-data + ;; Move to back to the start of the string, then past any ws + ;; and then past any non-ws to see if we have "from" or "import". + (goto-char string-start-pos) + (typescript--backward-syntactic-ws) + (skip-syntax-backward "^-" (point-at-bol)) + (looking-at "from\\|import\\s-")))))))) + +(defun typescript--inside-pitem-p (pitem) + "Return whether point is inside the given pitem's header or body." + (typescript--ensure-cache) + (cl-assert (typescript--pitem-h-begin pitem)) + (cl-assert (typescript--pitem-paren-depth pitem)) + + (and (> (point) (typescript--pitem-h-begin pitem)) + (or (null (typescript--pitem-b-end pitem)) + (> (typescript--pitem-b-end pitem) (point))))) + +(defun typescript--parse-state-at-point () + "Parse the typescript program state at point. +Return a list of `typescript--pitem' instances that apply to point, most +specific first. In the worst case, the current toplevel instance +will be returned." + (save-excursion + (save-restriction + (widen) + (typescript--ensure-cache) + (let* ((bound (if (eobp) (point) (1+ (point)))) + (pstate (or (save-excursion + (typescript--backward-pstate)) + (list typescript--initial-pitem)))) + + ;; Loop until we either hit a pitem at BOB or pitem ends after + ;; point (or at point if we're at eob) + (cl-loop for pitem = (car pstate) + until (or (eq (typescript--pitem-type pitem) + 'toplevel) + (typescript--inside-pitem-p pitem)) + do (pop pstate)) + + pstate)))) + +(defun typescript--syntactic-context-from-pstate (pstate) + "Return the typescript syntactic context corresponding to PSTATE." + (let ((type (typescript--pitem-type (car pstate)))) + (cond ((memq type '(function macro)) + type) + ((consp type) + 'class) + (t 'toplevel)))) + +(defun typescript-syntactic-context () + "Return the typescript syntactic context at point. +When called interatively, also display a message with that +context." + (interactive) + (let* ((syntactic-context (typescript--syntactic-context-from-pstate + (typescript--parse-state-at-point)))) + + (when (called-interactively-p 'interactive) + (message "Syntactic context: %s" syntactic-context)) + + syntactic-context)) + +(defun typescript--class-decl-matcher (limit) + "Font lock function used by `typescript-mode'. +This performs fontification according to `typescript--class-styles'." + (cl-loop initially (typescript--ensure-cache limit) + while (re-search-forward typescript--quick-match-re limit t) + for orig-end = (match-end 0) + do (goto-char (match-beginning 0)) + if (cl-loop for style in typescript--class-styles + for decl-re = (plist-get style :class-decl) + if (and (memq (plist-get style :framework) + typescript-enabled-frameworks) + (memq (typescript-syntactic-context) + (plist-get style :contexts)) + decl-re + (looking-at decl-re)) + do (goto-char (match-end 0)) + and return t) + return t + else do (goto-char orig-end))) + +(defconst typescript--font-lock-keywords-4 + `( + ;; highlights that override previous levels + ;; + + ;; special highlight for `this' keyword + ("\\(this\\)\\." + (1 'typescript-this-face)) + + (,typescript--access-modifier-re (1 'typescript-access-modifier-face)) + (,typescript--basic-type-re (1 'typescript-primitive-face)) + + ;; generics support + ,(list + (concat typescript--name-re "\\s-*" "<\\s-*" typescript--name-start-re) + (list (concat "\\(" typescript--name-re "\\)\\(\\s-*>[^<]*\\)?") + '(backward-char) + '(end-of-line) + '(1 font-lock-type-face))) + + ;; type-highlighting in variable/parameter declarations + ;; supports a small variety of common declarations: + ;; - let a: SomeType; + ;; - private b: SomeType; + ;; - private someFunc(var: SomeType) { + ;; - private array: SomeType[] + ;; - private generic: SomeType + ;; - private genericArray: SomeType[] + ;; - function testFunc(): SomeType<> { + ;; TODO: namespaced classes! + ,(list + (concat ":\\s-\\(" typescript--type-name-re "\\)\\(<" typescript--type-name-re ">\\)?\\(\[\]\\)?\\([,;]\\)?\\s-*{?") + '(1 'font-lock-type-face)) + + ;; type-casts + ,(list + (concat "<\\(" typescript--type-name-re "\\)>") + '(1 'font-lock-type-face)) + + ;; highlights that append to previous levels + ;; + ,@typescript--font-lock-keywords-3 + + (,typescript--decorator-re (1 font-lock-function-name-face)) + (,typescript--function-call-re (1 font-lock-function-name-face)) + (,typescript--builtin-re (1 font-lock-type-face)) + + ;; arrow function + ("\\(=>\\)" + (1 font-lock-keyword-face))) + "Level four font lock for `typescript-mode'.") + +(defconst typescript--font-lock-keywords + '(typescript--font-lock-keywords-4 typescript--font-lock-keywords-1 + typescript--font-lock-keywords-2 + typescript--font-lock-keywords-3 + typescript--font-lock-keywords-4) + "Font lock keywords for `typescript-mode'. See `font-lock-keywords'.") + +;;; Propertize + +;; +;; The propertize code was adapted from: +;; https://github.com/emacs-mirror/emacs/blob/489d6466372f488adc53897435fff290394b62f7/lisp/progmodes/js.el +;; + +(defconst typescript--syntax-propertize-regexp-regexp + (rx + ;; Start of regexp. + "/" + (0+ (or + ;; Match characters outside of a character class. + (not (any ?\[ ?/ ?\\)) + ;; Match backslash quoted characters. + (and "\\" not-newline) + ;; Match character class. + (and + "[" + (0+ (or + (not (any ?\] ?\\)) + (and "\\" not-newline))) + "]"))) + (group (zero-or-one "/"))) + "Regular expression matching a JavaScript regexp literal.") + +(defun typescript-syntax-propertize-regexp (end) + (let ((ppss (syntax-ppss))) + (when (eq (nth 3 ppss) ?/) + ;; A /.../ regexp. + (goto-char (nth 8 ppss)) + (when (looking-at typescript--syntax-propertize-regexp-regexp) + ;; Don't touch text after END. + (when (> end (match-end 1)) + (setq end (match-end 1))) + (put-text-property (match-beginning 1) end + 'syntax-table (string-to-syntax "\"/")) + (goto-char end))))) + +(defun typescript-syntax-propertize (start end) + ;; JavaScript allows immediate regular expression objects, written /.../. + (funcall + (syntax-propertize-rules + ;; Distinguish /-division from /-regexp chars (and from /-comment-starter). + ;; FIXME: Allow regexps after infix ops like + ... + ;; https://developer.mozilla.org/en/JavaScript/Reference/Operators + ;; We can probably just add +, -, <, >, %, ^, ~, ?, : at which + ;; point I think only * and / would be missing which could also be added, + ;; but need care to avoid affecting the // and */ comment markers. + ("\\(?:^\\|[=([{,:;|&!]\\|\\_\\)\\(?:[ \t]\\)*\\(/\\)[^/*]" + (1 (ignore + (forward-char -1) + (when (or (not (memq (char-after (match-beginning 0)) '(?\s ?\t))) + ;; If the / is at the beginning of line, we have to check + ;; the end of the previous text. + (save-excursion + (goto-char (match-beginning 0)) + (forward-comment (- (point))) + (memq (char-before) + (eval-when-compile (append "=({[,:;" '(nil)))))) + (put-text-property (match-beginning 1) (match-end 1) + 'syntax-table (string-to-syntax "\"/")) + (typescript-syntax-propertize-regexp end))))) + ;; Hash-bang at beginning of buffer. + ("\\`\\(#\\)!" (1 "< b"))) + start end)) + +;;; Indentation + +(defconst typescript--possibly-braceless-keyword-re + (typescript--regexp-opt-symbol + '("catch" "do" "else" "finally" "for" "if" "try" "while" "with")) + "Regexp matching keywords optionally followed by an opening brace.") + +(defconst typescript--indent-keyword-re + (typescript--regexp-opt-symbol '("in" "instanceof")) + "Regexp matching keywords that affect indentation of continued expressions.") + +(defconst typescript--indent-operator-re + (concat "[-+*/%<>=&^|?:.]\\([^-+*/]\\|$\\)\\|" typescript--indent-keyword-re) + "Regexp matching operators that affect indentation of continued expressions.") + +;; +;; We purposely do not allow the plus symbol as a prefix here, as this +;; regex is used to check number literal in type annotations, and TS +;; does not allow to use a plus symbol to prefix numbers there: you +;; can use 1, but not +1 in a type annotation. +;; +;; This is meant to match NaN, floats, decimals, the two infinities +;; and numbers recorded in binary, octal and hex. +;; +;; This regular expression was derived from: +;; https://stackoverflow.com/a/30987109/ +;; +(defconst typescript--number-literal-re + "\\(?:NaN\\|-?\\(?:0[Bb][01]+\\|0[Oo][0-7]+\\|0[Xx][0-9a-fA-F]+\\|Infinity\\|\\(?:[[:digit:]]*\\.[[:digit:]]+\\|[[:digit:]]+\\)\\(?:[Ee][+-]?[[:digit:]]+\\)?\\)\\)" + "Regexp that matches number literals.") + +(defconst typescript--reserved-start-keywords + '("const" "export" "function" "let" "var") + "These keywords cannot be variable or type names and start a new sentence. +Note that the \"import\" keyword can be a type import since TS2.9, so it might +not start a sentence!") + +(defconst typescript--reserved-start-keywords-re + (typescript--regexp-opt-symbol '("const" "export" "function" "let" "var")) + "A regular expression matching `typescript--reserved-start-keywords'.") + +(defconst typescript--type-vs-ternary-re + (concat "[?]\\|" (typescript--regexp-opt-symbol + (append typescript--reserved-start-keywords + '("as" "class" "interface" "private" "public" "readonly")))) + "Keywords/Symbols that help tell apart colon for types vs ternary operators.") + +(defun typescript--search-backward-matching-angle-bracket-inner (depth) + "Auxiliary function for `typescript--search-backward-matching-angle-bracket'. +DEPTH indicates how nested we think we are: it increases when we cross closing +brackets, and decreases when we cross opening brackets." + ;; We look backwards for a "<" that would correspond to the ">" we started + ;; from. However, there is no guarantee that it exists, since our ">" could + ;; be a greater-than operation. Some symbols will make it clear that we are + ;; *not* in a type annotation, so we can return nil. Otherwise, we keep + ;; looking for the matching one. + (or (<= depth 0) + (and + ;; If we cross over a reserved start keyword, we abandon hope of finding + ;; a matching angle bracket. This prevents extreme recursion depths. + (typescript--re-search-backward (concat "[<>]\\|" typescript--reserved-start-keywords-re) nil t) + (cl-case (char-after) + (?< (typescript--search-backward-matching-angle-bracket-inner (- depth 1))) + (?> (typescript--search-backward-matching-angle-bracket-inner (+ depth 1))))))) + +(defun typescript--search-backward-matching-angle-bracket () + "Search for matching \"<\" preceding a starting \">\". +DEPTH indicates how nested we think we are. Assumes the starting position is +right before the closing \">\". Returns nil when a match was not found, +otherwise returns t and the current position is right before the matching +\"<\"." + (typescript--search-backward-matching-angle-bracket-inner 1)) + +(defun typescript--re-search-backward-ignoring-angle-brackets () + "Search backwards, jumping over text within angle brackets. +Searches specifically for any of \"=\", \"}\", and \"type\"." + (and + (typescript--re-search-backward "[>=}]\\|\\_" nil t) + (or (not (looking-at ">")) + (and + (typescript--search-backward-matching-angle-bracket) + (typescript--re-search-backward-ignoring-angle-brackets))))) + +(defun typescript--looking-at-operator-p () + "Return non-nil if point is on a typescript operator, other than a comma." + (save-match-data + (and (looking-at typescript--indent-operator-re) + (or (not (looking-at ":")) + (save-excursion + (backward-sexp) + (and + (typescript--re-search-backward "[?:{]\\|\\_" nil t) + (looking-at "?")))) + ;; Do not identify forward slashes appearing in a "list" as + ;; an operator. The lists are: arrays, or lists of + ;; arguments. In this context, they must be part of regular + ;; expressions, and not math operators. + (not (and (looking-at "/") + (save-excursion + (typescript--backward-syntactic-ws) + (memq (char-before) '(?, ?\[ ?\())))) + ;; Do not identify methods, or fields, that are named "in" or + ;; "instanceof" as being operator keywords. + (not (and + (looking-at typescript--indent-keyword-re) + (save-excursion + (typescript--backward-syntactic-ws) + (memq (char-before) '(?, ?{ ?} ?\;))))) + ;; Do not identify the symbol > if it is likely part of a type argument + ;; T, but identify it if it is likely a greater-than symbol. This is + ;; a hard problem in the absence of semicolons, see: + ;; https://github.com/ananthakumaran/typescript.el/issues/81 + (not (and + (looking-at ">") + (save-excursion + (and + (typescript--search-backward-matching-angle-bracket) + ;; If we made it here, we found a candidate matching opening + ;; angle bracket. We still need to guess whether it actually + ;; is one, and not a spurious less-than operator! + + ;; Look backwards for the first of: + ;; - one of the symbols: = : + ;; - or a TypeScript keyword + ;; Depending on what comes first, we can make an educated + ;; guess on the nature of our ">" of interest. + (typescript--re-search-backward (concat "[=:]\\|" typescript--keyword-re) nil t) + (or + ;; If the previous keyword is "as", definitely a type. + (looking-at "\\_") + ;; Same goes for type imports. + (looking-at "\\_") + ;; A colon could be either a type symbol, or a ternary + ;; operator, try to guess which. + (and (looking-at ":") + (typescript--re-search-backward typescript--type-vs-ternary-re nil t) + (not (looking-at "?"))) + ;; This final check lets us distinguish between a + ;; 2-argument type "t < a , b > ..." and a use of the "," + ;; operator between two comparisons "t < a , b > ...". + ;; Looking back a little more lets us guess. + (and (looking-at "=") + (typescript--re-search-backward-ignoring-angle-brackets) + (looking-at "\\_"))))))) + (not (and + (looking-at "*") + ;; Generator method (possibly using computed property). + (looking-at (concat "\\* *\\(?:\\[\\|" typescript--name-re + " *(\\)")) + (save-excursion + (typescript--backward-syntactic-ws) + ;; We might misindent some expressions that would + ;; return NaN anyway. Shouldn't be a problem. + (memq (char-before) '(?, ?} ?{ ?\;)))))))) + + +(defun typescript--continued-expression-p () + "Return non-nil if the current line continues an expression." + (save-excursion + (back-to-indentation) + (let ((list-start (nth 1 (syntax-ppss)))) + (and + ;; This not clause is there to eliminate degenerate cases where we have + ;; something that looks like a continued expression but we are in fact at + ;; the beginning of the expression. Example: in `if (a) { .q(1)` when the + ;; point is on the dot, the expression that follows looks like a member + ;; expression but the object on which it is a member is missing. If we + ;; naively treat this as a continued expression, we run into trouble + ;; later. (An infinite loop.) + (not (and list-start + (save-excursion + (typescript--backward-syntactic-ws) + (backward-char) + (eq (point) list-start)))) + ;; Don't identify the spread syntax or rest operator as a "continuation". + (not (looking-at "\\.\\.\\.")) + (or (typescript--looking-at-operator-p) + (and (progn + (typescript--backward-syntactic-ws) + (or (bobp) (backward-char)) + (and (> (point) (point-min)) + (save-excursion (backward-char) (not (looking-at "[/*]/"))) + (typescript--looking-at-operator-p) + (and (progn (backward-char) + (not (looking-at "++\\|--\\|/[/*]")))))))))))) + +(cl-defun typescript--compute-member-expression-indent () + "Determine the indent of a member expression. + +This function must be called with point located at the dot that +starts the member expression. +" + ;; Find the line that has the object from which we are getting thismember. + ;; And set an indent relative to that. + (while (looking-at "\\.") + (typescript--backward-syntactic-ws) + (while (eq (char-before) ?\;) + (backward-char)) + (while (memq (char-before) '(?\] ?} ?\) ?>)) + (if (not (eq (char-before) ?>)) + (backward-list) + (backward-char) + (typescript--backward-over-generic-parameter-list)) + (typescript--backward-syntactic-ws)) + (if (looking-back typescript--dotted-name-re nil) + (back-to-indentation) + (typescript--forward-syntactic-ws))) + (+ (current-column) typescript-indent-level)) + +(defun typescript--end-of-do-while-loop-p () + "Return non-nil if point is on the \"while\" of a do-while statement. +Otherwise, return nil. A braceless do-while statement spanning +several lines requires that the start of the loop is indented to +the same column as the current line." + (interactive) + (save-excursion + (save-match-data + (when (looking-at "\\s-*\\_") + (if (save-excursion + (skip-chars-backward "[ \t\n]*}") + (looking-at "[ \t\n]*}")) + (save-excursion + (backward-list) (forward-symbol -1) (looking-at "\\_")) + (typescript--re-search-backward "\\_" (point-at-bol) t) + (or (looking-at "\\_") + (let ((saved-indent (current-indentation))) + (while (and (typescript--re-search-backward "^\\s-*\\_<" nil t) + (/= (current-indentation) saved-indent))) + (and (looking-at "\\s-*\\_") + (not (typescript--re-search-forward + "\\_" (point-at-eol) t)) + (= (current-indentation) saved-indent))))))))) + + +(defun typescript--ctrl-statement-indentation () + "Helper function for `typescript--proper-indentation'. +Return the proper indentation of the current line if it starts +the body of a control statement without braces; otherwise, return +nil." + (save-excursion + (back-to-indentation) + (when (save-excursion + (and (not (eq (point-at-bol) (point-min))) + (not (looking-at "[{]")) + (progn + (typescript--re-search-backward "[[:graph:]]" nil t) + (or (eobp) (forward-char)) + (when (= (char-before) ?\)) (backward-list)) + (skip-syntax-backward " ") + (skip-syntax-backward "w_") + (and + (looking-at typescript--possibly-braceless-keyword-re) + ;; If preceded by period, it's a method call. + (not (= (char-before) ?.)))) + (not (typescript--end-of-do-while-loop-p)))) + (save-excursion + (goto-char (match-beginning 0)) + (+ (current-indentation) typescript-indent-level))))) + +(defun typescript--get-c-offset (symbol anchor) + (let ((c-offsets-alist + (list (cons 'c typescript-comment-lineup-func)))) + (c-get-syntactic-indentation (list (cons symbol anchor))))) + +(defun typescript--backward-over-generic-parameter-list () + "Search backward for the start of a generic's parameter list and move to it. + +This is a utility function for +`typescript--backward-to-parameter-list'. + +This function must be called with the point placed on the final > +of the generic's parameter list. It will scan backwards to find +the start. If successful, it will move the point to the start of +the list. If not, it does not move the point. + +Returns nil on failure, or the position to which the point was +moved on success." + (when (eq (char-after) ?>) + (let ((depth 1)) + (cl-loop named search-loop + while (> depth 0) + do (progn + (unless (re-search-backward "[<>]" nil t) + (cl-return-from search-loop nil)) + (cond + ((looking-at ">") + (unless (eq (char-before) ?=) + (setq depth (1+ depth)))) + ((looking-at "<") (setq depth (1- depth))))) + finally return (point))))) + +(defun typescript--backward-to-parameter-list () + "Search backward for the end of a parameter list and move to it. + +This is a utility function for `typescript--proper-indentation'. + +This function must be called with the point placed before an +opening curly brace. It will try to skip over the type +annotation that would mark the return value of a function and +move to the end of the parameter list. If it is unsuccessful, it +does not move the point. \"Unsuccessful\" here also means that +the position at which we started did not in fact mark the +beginning of a function. The curly brace belonged to some other +syntactic construct than a function. + +Returns nil on failure, or the position to which the point was +moved on success." + (let ((location + (or + ;; This handles the case of a function with return type annotation. + (save-excursion + (cl-loop named search-loop + do + (typescript--backward-syntactic-ws) + ;; Check whether we are at "):". + (when (and (eq (char-before) ?\:) + (progn + (backward-char) + (skip-syntax-backward " ") + (eq (char-before) ?\)))) + ;; Success! This the end of the parameter list. + (cl-return-from search-loop (point))) + ;; If we recognize a structure that belongs in a return type annotation, + ;; skip back over it, or fail. + (cond + ;; Arrow of a function definition, or typeguard (eg. foo is SomeClass) + ((looking-back "=>\\|is" (- (point) 2)) + (backward-char 2)) + ;; End of the parameters list of a generic. + ((eq (char-before) ?>) + (backward-char) + (typescript--backward-over-generic-parameter-list)) + ;; Union of types, or a dot in a dotted name. + ((memq (char-before) '(?| ?.)) + (backward-char)) + ((or + ;; End-delimiter of a delimited construct, for constructs + ;; not handled above. + (memq (char-before) '(?\) ?} ?\" ?\])) + ;; This is also dealing with dotted names. This may come + ;; into play if a jump back moves over an entire dotted + ;; name at once. + ;; + ;; The earlier test for dotted names comes into play if the + ;; logic moves over one part of a dotted name at a time (which + ;; is what `backward-sexp` normally does). + (and (looking-back typescript--dotted-name-re nil) + ;; We don't want the loop to walk over constructs like switch (...) or for (...), etc. + (not (save-excursion + (backward-word) + (looking-at "\\_<\\(switch\\|if\\|while\\|until\\|for\\)\\_>\\(?:\\s-\\|\n\\)*("))))) + (condition-case nil + (backward-sexp) + (scan-error (cl-return-from search-loop nil)))) + ((looking-back typescript--number-literal-re + ;; We limit the search back to the previous space or end of line (if possible) + ;; to prevent the search from going over the whole buffer. + (save-excursion (re-search-backward "\\(?:\\s-\\|\n\\)" nil t)) t) + (goto-char (match-beginning 0))) + ;; Otherwise, we failed to find a location. + (t + (cl-return-from search-loop nil))))) + ;; This handles the case of a function without return type annotation. + (progn + (typescript--backward-syntactic-ws) + (when (eq (char-before) ?\)) + (point)))))) + (when location + (goto-char location)))) + +(defun typescript--proper-indentation (parse-status) + "Return the proper indentation for the current line." + (save-excursion + (back-to-indentation) + (let ((member-expr-p (looking-at "\\."))) + (cond ((nth 4 parse-status) ;; Inside a comment. + (typescript--get-c-offset 'c (nth 8 parse-status))) + ((nth 8 parse-status) 0) ;; Inside a string. + ((typescript--ctrl-statement-indentation)) ;; Control statements. + ((eq (char-after) ?#) 0) ;; Looking at a pragma. + ;; Inside a list of things. Note that in the TS contents, the curly braces + ;; marking code blocks are "list of things". + ((nth 1 parse-status) + (let ((indent-start (point)) + (same-indent-p (looking-at "[]})]")) + (switch-keyword-p (looking-at "\\_\\|\\_[^:]")) + (continued-expr-p (typescript--continued-expression-p)) + (list-start (nth 1 parse-status))) + (goto-char list-start) + (if (looking-at "[({[]\\s-*\\(/[/*]\\|$\\)") + (progn + (skip-syntax-backward " ") + (cond + ((or (typescript--backward-to-parameter-list) + (eq (char-before) ?\))) + ;; Take the curly brace as marking off the body of a function. + ;; In that case, we want the code that follows to see the indentation + ;; that was in effect at the beginning of the function declaration, and thus + ;; we want to move back over the list of function parameters. + (condition-case nil + (backward-list) + (error nil))) + ((looking-back "," nil) + ;; If we get here, we have a comma, spaces and an opening curly brace. (And + ;; (point) is just after the comma.) We don't want to move from the current position + ;; so that object literals in parameter lists are properly indented. + nil) + (t + ;; In all other cases, we don't want to move from the curly brace. + (goto-char list-start))) + (back-to-indentation) + (let* ((in-switch-p (unless same-indent-p + (looking-at "\\_"))) + (same-indent-p (or same-indent-p + (and switch-keyword-p + in-switch-p))) + (indent + (cond (same-indent-p + (current-column)) + (continued-expr-p + (if (not member-expr-p) + (+ (current-column) (* 2 typescript-indent-level) + typescript-expr-indent-offset) + (goto-char indent-start) + (typescript--compute-member-expression-indent))) + (t + (+ (current-column) typescript-indent-level))))) + (if (and in-switch-p typescript-indent-switch-clauses) + (+ indent typescript-indent-level) + indent))) + (unless same-indent-p + (forward-char) + (skip-chars-forward " \t")) + (if continued-expr-p + (if (not member-expr-p) + (progn (back-to-indentation) + (+ (current-column) typescript-indent-level + typescript-expr-indent-offset)) + (goto-char indent-start) + (typescript--compute-member-expression-indent)) + (current-column))))) + + ((typescript--continued-expression-p) ;; Inside a continued expression. + (if member-expr-p + (typescript--compute-member-expression-indent) + (+ typescript-indent-level typescript-expr-indent-offset))) + (t 0))))) + +(defun typescript-indent-line () + "Indent the current line as typescript." + (interactive) + (save-restriction + (widen) + (let* ((parse-status + (save-excursion (syntax-ppss (point-at-bol)))) + (offset (- (current-column) (current-indentation)))) + (indent-line-to (typescript--proper-indentation parse-status)) + (when (> offset 0) (move-to-column (+ offset (current-indentation))))))) + +;;; Filling + +(defun typescript-c-fill-paragraph (&optional justify) + "Fill the paragraph with `c-fill-paragraph'." + (interactive "*P") + ;; Dynamically replace functions using the lexically scoped cl-letf. + ;; See below for more details: + ;; http://endlessparentheses.com/understanding-letf-and-how-it-replaces-flet.html + (cl-letf (((symbol-function 'c-forward-sws) + (lambda (&optional limit) + (typescript--forward-syntactic-ws limit))) + ((symbol-function 'c-backward-sws) + (lambda (&optional limit) + (typescript--backward-syntactic-ws limit)))) + (let ((fill-paragraph-function 'c-fill-paragraph)) + (c-fill-paragraph justify)))) + +;; We maintain a cache of semantic information, i.e., the classes and +;; functions we've encountered so far. In order to avoid having to +;; re-parse the buffer on every change, we cache the parse state at +;; each interesting point in the buffer. Each parse state is a +;; modified copy of the previous one, or in the case of the first +;; parse state, the empty state. +;; +;; The parse state itself is just a stack of typescript--pitem +;; instances. It starts off containing one element that is never +;; closed, that is initially typescript--initial-pitem. +;; + + +(defun typescript--pitem-format (pitem) + (let ((name (typescript--pitem-name pitem)) + (type (typescript--pitem-type pitem))) + + (format "name:%S type:%S" + name + (if (atom type) + type + (plist-get type :name))))) + +(defun typescript--make-merged-item (item child name-parts) + "Helper function for `typescript--splice-into-items'. +Return a new item that is the result of merging CHILD into +ITEM. NAME-PARTS is a list of parts of the name of CHILD +that we haven't consumed yet." + (typescript--debug "typescript--make-merged-item: {%s} into {%s}" + (typescript--pitem-format child) + (typescript--pitem-format item)) + + ;; If the item we're merging into isn't a class, make it into one + (unless (consp (typescript--pitem-type item)) + (typescript--debug "typescript--make-merged-item: changing dest into class") + (setq item (make-typescript--pitem + :children (list item) + + ;; Use the child's class-style if it's available + :type (if (atom (typescript--pitem-type child)) + typescript--dummy-class-style + (typescript--pitem-type child)) + + :name (typescript--pitem-strname item)))) + + ;; Now we can merge either a function or a class into a class + (cons (cond + ((cdr name-parts) + (typescript--debug "typescript--make-merged-item: recursing") + ;; if we have more name-parts to go before we get to the + ;; bottom of the class hierarchy, call the merger + ;; recursively + (typescript--splice-into-items (car item) child + (cdr name-parts))) + + ((atom (typescript--pitem-type child)) + (typescript--debug "typescript--make-merged-item: straight merge") + ;; Not merging a class, but something else, so just prepend + ;; it + (cons child (car item))) + + (t + ;; Otherwise, merge the new child's items into those + ;; of the new class + (typescript--debug "typescript--make-merged-item: merging class contents") + (append (car child) (car item)))) + (cdr item))) + +(defun typescript--pitem-strname (pitem) + "Last part of the name of PITEM, as a string or symbol." + (let ((name (typescript--pitem-name pitem))) + (if (consp name) + (car (last name)) + name))) + +(defun typescript--splice-into-items (items child name-parts) + "Splice CHILD into the `typescript--pitem' ITEMS at NAME-PARTS. +If a class doesn't exist in the tree, create it. Return +the new items list. NAME-PARTS is a list of strings given +the broken-down class name of the item to insert." + + (let ((top-name (car name-parts)) + (item-ptr items) + new-items last-new-item new-cons item) + + (typescript--debug "typescript--splice-into-items: name-parts: %S items:%S" + name-parts + (mapcar #'typescript--pitem-name items)) + + (cl-assert (stringp top-name)) + (cl-assert (> (length top-name) 0)) + + ;; If top-name isn't found in items, then we build a copy of items + ;; and throw it away. But that's okay, since most of the time, we + ;; *will* find an instance. + + (while (and item-ptr + (cond ((equal (typescript--pitem-strname (car item-ptr)) top-name) + ;; Okay, we found an entry with the right name. Splice + ;; the merged item into the list... + (setq new-cons (cons (typescript--make-merged-item + (car item-ptr) child + name-parts) + (cdr item-ptr))) + + (if last-new-item + (setcdr last-new-item new-cons) + (setq new-items new-cons)) + + ;; ...and terminate the loop + nil) + + (t + ;; Otherwise, copy the current cons and move onto the + ;; text. This is tricky; we keep track of the tail of + ;; the list that begins with new-items in + ;; last-new-item. + (setq new-cons (cons (car item-ptr) nil)) + (if last-new-item + (setcdr last-new-item new-cons) + (setq new-items new-cons)) + (setq last-new-item new-cons) + + ;; Go to the next cell in items + (setq item-ptr (cdr item-ptr)))))) + + (if item-ptr + ;; Yay! We stopped because we found something, not because + ;; we ran out of items to search. Just return the new + ;; list. + (progn + (typescript--debug "search succeeded: %S" name-parts) + new-items) + + ;; We didn't find anything. If the child is a class and we don't + ;; have any classes to drill down into, just push that class; + ;; otherwise, make a fake class and carry on. + (typescript--debug "search failed: %S" name-parts) + (cons (if (cdr name-parts) + ;; We have name-parts left to process. Make a fake + ;; class for this particular part... + (make-typescript--pitem + ;; ...and recursively digest the rest of the name + :children (typescript--splice-into-items + nil child (cdr name-parts)) + :type typescript--dummy-class-style + :name top-name) + + ;; Otherwise, this is the only name we have, so stick + ;; the item on the front of the list + child) + items)))) + +(defun typescript--pitem-add-child (pitem child) + "Copy `typescript--pitem' PITEM, and push CHILD onto its list of children." + (cl-assert (integerp (typescript--pitem-h-begin child))) + (cl-assert (if (consp (typescript--pitem-name child)) + (cl-loop for part in (typescript--pitem-name child) + always (stringp part)) + t)) + + ;; This trick works because we know (based on our cl-defstructs) that + ;; the child list is always the first element, and so the second + ;; element and beyond can be shared when we make our "copy". + (cons + + (let ((name (typescript--pitem-name child)) + (type (typescript--pitem-type child))) + + (cond ((cdr-safe name) ; true if a list of at least two elements + ;; Use slow path because we need class lookup + (typescript--splice-into-items (car pitem) child name)) + + ((and (consp type) + (plist-get type :prototype)) + + ;; Use slow path because we need class merging. We know + ;; name is a list here because down in + ;; `typescript--ensure-cache', we made sure to only add + ;; class entries with lists for :name + (cl-assert (consp name)) + (typescript--splice-into-items (car pitem) child name)) + + (t + ;; Fast path + (cons child (car pitem))))) + + (cdr pitem))) + +;;; compilation-mode support + +;; tsc supports formatting errors in two general ways: plain and +;; pretty. ("Plain" is our term for "not pretty".) In tsc versions +;; prior to 2.7, the plain and pretty formats both used the same +;; format for references into files. `typescript-tsc-error-regexp` +;; covers both plain and pretty for those versions. +;; +;; Version 2.7 changed the pretty format so as to format source code +;; references differently. This required the introduction of +;; `typescript-tsc-pretty-error-regexp`. The format of plain error +;; messages did not change. So from that version onwards, +;; `typescript-tsc-error-regexp` covers plain error messages and +;; `typescript-tsc-pretty-error-regexp` covers pretty error messages. + +;; handle plain compiler-errors like the following when doing M-x compiletsc +;; +;; greeter.ts(24,9): error TS2362: The left-hand side of an arithmetic operation must be of type 'any', 'number' or an enum type. +;; greeter.ts(30,12): error TS2339: Property 'indexOf' does not exist on type 'number'. +(defconst typescript-tsc-error-regexp + (concat + "^[[:blank:]]*" + "\\([^(\r\n)]+\\)(\\([0-9]+\\),\\([0-9]+\\)):[[:blank:]]+" + "error [[:alnum:]]+: [^\r\n]+$") + "Regexp to match errors generated by tsc.") + +;; handle pretty compiler-errors like the following when doing M-x compiletsc +;; test.ts:2:7 - error TS2322: Type '2' is not assignable to type 'string'. +(defconst typescript-tsc-pretty-error-regexp + (concat + "^[[:blank:]]*" + "\\([^(\r\n)]+\\):\\([0-9]+\\):\\([0-9]+\\) - [[:blank:]]*" + "error [[:alnum:]]+: [^\r\n]+$") + "Regexp to match errors generated by tsc.") + +;; +;; Should handle output like: +;; src/modules/authenticator.ts[1, 83]: ' should be " +;; (quotemarks) src/modules/authenticator.ts[2, 26]: ' should be " +;; ERROR: (quotemarks) src/modules/authenticator.ts[2, 26]: ' should be " +;; WARNING: src/modules/authenticator.ts[2, 26]: ' should be " +;; +;; "(quotemarks)" it the rule name. It is produced when using the +;; "verbose" formatter. The "verbose" formatter is identical to the +;; default ("prose") formatter, except for the additional rule name. +;; +;; "ERROR:" and "WARNING:" are the severity. This was added in tslint +;; 5.0. Prior versions have no notion of severity and simply omit this +;; part. +;; +(defconst typescript-tslint-report-regexp + (concat + "^[[:blank:]]*" + ;; severity ("type" in Emacs' parlance) + "\\(?:\\(?:ERROR\\|\\(WARNING\\)\\):[[:blank:]]+\\)?" + ;; rule name + "\\((.*)[[:blank:]]+\\)?" + ;; filename + "\\([^(\r\n)]+\\)" + "\\[" + ;; line + "\\([[:digit:]]+\\)" + ", " + ;; column + "\\([[:digit:]]+\\)" + "\\]: " + ;; message + ".*$") + "Regexp to match reports generated by tslint.") + +(defconst typescript-nglint-error-regexp + (concat + ;; severity ("type" in Emacs' parlance) + "ERROR:[[:blank:]]+" + + ;; filename + "\\([^(\r\n)]+\\)" + ":" + ;; line + "\\([[:digit:]]+\\)" + ":" + ;; column + "\\([[:digit:]]+\\)" + + " - " + ;; message + ".*$")) + +(defconst typescript-nglint-warning-regexp + (concat + ;; severity ("type" in Emacs' parlance) + "WARNING:[[:blank:]]+" + + ;; filename + "\\([^(\r\n)]+\\)" + ":" + ;; line + "\\([[:digit:]]+\\)" + ":" + ;; column + "\\([[:digit:]]+\\)" + + " - " + ;; message + ".*$")) + +(dolist + (regexp + `((typescript-tsc + ,typescript-tsc-error-regexp + 1 2 3 2) + + (typescript-tsc-pretty + ,typescript-tsc-pretty-error-regexp + 1 2 3 2) + + (typescript-tslint + ,typescript-tslint-report-regexp + 3 4 5 (1)) + + (typescript-nglint-error + ,typescript-nglint-error-regexp + 1 2 3 2) + + (typescript-nglint-warning + ,typescript-nglint-warning-regexp + 1 2 3 1))) + (add-to-list 'compilation-error-regexp-alist-alist regexp) + (add-to-list 'compilation-error-regexp-alist (car regexp))) + +;;; Main Function + +;;;###autoload +(define-derived-mode typescript-mode prog-mode "typescript" + "Major mode for editing typescript. + +Key bindings: + +\\{typescript-mode-map}" + + :group 'typescript + :syntax-table typescript-mode-syntax-table + + (setq-local indent-line-function 'typescript-indent-line) + (setq-local beginning-of-defun-function 'typescript-beginning-of-defun) + (setq-local end-of-defun-function 'typescript-end-of-defun) + (setq-local open-paren-in-column-0-is-defun-start nil) + (setq-local font-lock-defaults (list typescript--font-lock-keywords)) + (setq-local syntax-propertize-function #'typescript-syntax-propertize) + (setq-local parse-sexp-ignore-comments t) + (setq-local parse-sexp-lookup-properties t) + + ;; Comments + (setq-local comment-start "// ") + (setq-local comment-end "") + (setq-local fill-paragraph-function 'typescript-c-fill-paragraph) + + ;; Parse cache + (add-hook 'before-change-functions #'typescript--flush-caches t t) + + ;; Frameworks + (typescript--update-quick-match-re) + + ;; for filling, pretend we're cc-mode + (setq c-comment-prefix-regexp "//+\\|\\**" + c-paragraph-start "$" + c-paragraph-separate "$" + c-block-comment-prefix "* " + c-line-comment-starter "//" + c-comment-start-regexp "/[*/]\\|\\s!" + comment-start-skip "\\(//+\\|/\\*+\\)\\s *") + + (setq-local electric-indent-chars + (append "{}():;," electric-indent-chars)) + (setq-local electric-layout-rules + '((?\; . after) (?\{ . after) (?\} . before))) + + (let ((c-buffer-is-cc-mode t)) + ;; FIXME: These are normally set by `c-basic-common-init'. Should + ;; we call it instead? (Bug#6071) + (make-local-variable 'paragraph-start) + (make-local-variable 'paragraph-separate) + (make-local-variable 'paragraph-ignore-fill-prefix) + (make-local-variable 'adaptive-fill-mode) + (make-local-variable 'adaptive-fill-regexp) + (c-setup-paragraph-variables)) + + (add-hook 'post-self-insert-hook + #'typescript--post-self-insert-function) + + (setq-local syntax-begin-function #'typescript--syntax-begin-function)) + +;; Set our custom predicate for flyspell prog mode +(put 'typescript-mode 'flyspell-mode-predicate + 'typescript--flyspell-mode-predicate) + +;;;###autoload +(eval-after-load 'folding + '(when (fboundp 'folding-add-to-marks-list) + (folding-add-to-marks-list 'typescript-mode "// {{{" "// }}}" ))) + +;;;###autoload +(add-to-list 'auto-mode-alist '("\\.ts$" . typescript-mode)) + +(provide 'typescript-mode) + +;;; typescript-mode.el ends here