featurep! will be renamed modulep! in the future, so it's been deprecated. They have identical interfaces, and can be replaced without issue. featurep! was never quite the right name for this macro. It implied that it had some connection to featurep, which it doesn't (only that it was similar in purpose; still, Doom modules are not features). To undo such implications and be consistent with its namespace (and since we're heading into a storm of breaking changes with the v3 release anyway), now was the best opportunity to begin the transition.
329 lines
12 KiB
EmacsLisp
329 lines
12 KiB
EmacsLisp
;;; lang/javascript/config.el -*- lexical-binding: t; -*-
|
|
|
|
(after! projectile
|
|
(pushnew! projectile-project-root-files "package.json")
|
|
(pushnew! projectile-globally-ignored-directories "^node_modules$" "^flow-typed$"))
|
|
|
|
|
|
;;
|
|
;;; Major modes
|
|
|
|
(dolist (feature '(rjsx-mode
|
|
typescript-mode
|
|
web-mode
|
|
(nodejs-repl-mode . nodejs-repl)))
|
|
(let ((pkg (or (cdr-safe feature) feature))
|
|
(mode (or (car-safe feature) feature)))
|
|
(with-eval-after-load pkg
|
|
(set-docsets! mode "JavaScript"
|
|
"AngularJS" "Backbone" "BackboneJS" "Bootstrap" "D3JS" "EmberJS" "Express"
|
|
"ExtJS" "JQuery" "JQuery_Mobile" "JQuery_UI" "KnockoutJS" "Lo-Dash"
|
|
"MarionetteJS" "MomentJS" "NodeJS" "PrototypeJS" "React" "RequireJS"
|
|
"SailsJS" "UnderscoreJS" "VueJS" "ZeptoJS")
|
|
(set-ligatures! mode
|
|
;; Functional
|
|
:def "function"
|
|
:lambda "() =>"
|
|
:composition "compose"
|
|
;; Types
|
|
:null "null"
|
|
:true "true" :false "false"
|
|
;; Flow
|
|
:not "!"
|
|
:and "&&" :or "||"
|
|
:for "for"
|
|
:return "return"
|
|
;; Other
|
|
:yield "import"))))
|
|
|
|
|
|
(use-package! rjsx-mode
|
|
:mode "\\.[mc]?js\\'"
|
|
:mode "\\.es6\\'"
|
|
:mode "\\.pac\\'"
|
|
:interpreter "node"
|
|
:hook (rjsx-mode . rainbow-delimiters-mode)
|
|
:init
|
|
;; Parse node stack traces in the compilation buffer
|
|
(after! compilation
|
|
(add-to-list 'compilation-error-regexp-alist 'node)
|
|
(add-to-list 'compilation-error-regexp-alist-alist
|
|
'(node "^[[:blank:]]*at \\(.*(\\|\\)\\(.+?\\):\\([[:digit:]]+\\):\\([[:digit:]]+\\)"
|
|
2 3 4)))
|
|
:config
|
|
(set-repl-handler! 'rjsx-mode #'+javascript/open-repl)
|
|
(set-electric! 'rjsx-mode :chars '(?\} ?\) ?. ?:))
|
|
|
|
(setq js-chain-indent t
|
|
;; These have become standard in the JS community
|
|
js2-basic-offset 2
|
|
;; Don't mishighlight shebang lines
|
|
js2-skip-preprocessor-directives t
|
|
;; let flycheck handle this
|
|
js2-mode-show-parse-errors nil
|
|
js2-mode-show-strict-warnings nil
|
|
;; Flycheck provides these features, so disable them: conflicting with
|
|
;; the eslint settings.
|
|
js2-strict-missing-semi-warning nil
|
|
;; maximum fontification
|
|
js2-highlight-level 3
|
|
js2-idle-timer-delay 0.15)
|
|
|
|
(setq-hook! 'rjsx-mode-hook
|
|
;; Indent switch-case another step
|
|
js-switch-indent-offset js2-basic-offset)
|
|
|
|
(use-package! xref-js2
|
|
:when (modulep! :tools lookup)
|
|
:init
|
|
(setq xref-js2-search-program 'rg)
|
|
(set-lookup-handlers! 'rjsx-mode
|
|
:xref-backend #'xref-js2-xref-backend))
|
|
|
|
;; HACK `rjsx-electric-gt' relies on js2's parser to tell it when the cursor
|
|
;; is in a self-closing tag, so that it can insert a matching ending tag
|
|
;; at point. The parser doesn't run immediately however, so a fast typist
|
|
;; can outrun it, causing tags to stay unclosed, so force it to parse:
|
|
(defadvice! +javascript-reparse-a (n)
|
|
;; if n != 1, rjsx-electric-gt calls rjsx-maybe-reparse itself
|
|
:before #'rjsx-electric-gt
|
|
(if (= n 1) (rjsx-maybe-reparse))))
|
|
|
|
|
|
(use-package! typescript-mode
|
|
:hook (typescript-mode . rainbow-delimiters-mode)
|
|
:hook (typescript-tsx-mode . rainbow-delimiters-mode)
|
|
:init
|
|
(when (modulep! :lang web)
|
|
(autoload 'typescript-tsx-mode "typescript-mode" nil t))
|
|
|
|
;; REVIEW We associate TSX files with `typescript-tsx-mode' derived from
|
|
;; `web-mode' because `typescript-mode' does not officially support
|
|
;; JSX/TSX. See emacs-typescript/typescript.el#4
|
|
(add-to-list 'auto-mode-alist
|
|
(cons "\\.tsx\\'"
|
|
(if (modulep! :lang web)
|
|
#'typescript-tsx-mode
|
|
#'typescript-mode)))
|
|
|
|
(when (modulep! :checkers syntax)
|
|
(after! flycheck
|
|
(flycheck-add-mode 'javascript-eslint 'web-mode)
|
|
(flycheck-add-mode 'javascript-eslint 'typescript-mode)
|
|
(flycheck-add-mode 'javascript-eslint 'typescript-tsx-mode)
|
|
(flycheck-add-mode 'typescript-tslint 'typescript-tsx-mode)
|
|
(unless (modulep! +lsp)
|
|
(after! tide
|
|
(flycheck-add-next-checker 'typescript-tide '(warning . javascript-eslint) 'append)
|
|
(flycheck-add-mode 'typescript-tide 'typescript-tsx-mode)))
|
|
(add-hook! 'typescript-tsx-mode-hook
|
|
(defun +javascript-disable-tide-checkers-h ()
|
|
(pushnew! flycheck-disabled-checkers
|
|
'javascript-jshint
|
|
'tsx-tide
|
|
'jsx-tide)))))
|
|
:config
|
|
(when (fboundp 'web-mode)
|
|
(define-derived-mode typescript-tsx-mode web-mode "TypeScript-TSX")
|
|
(when (modulep! +lsp)
|
|
(after! lsp-mode
|
|
(add-to-list 'lsp--formatting-indent-alist '(typescript-tsx-mode . typescript-indent-level))))
|
|
(when (modulep! +tree-sitter)
|
|
(after! evil-textobj-tree-sitter
|
|
(pushnew! evil-textobj-tree-sitter-major-mode-language-alist '(typescript-tsx-mode . "tsx")))
|
|
(after! tree-sitter
|
|
(pushnew! tree-sitter-major-mode-language-alist '(typescript-tsx-mode . tsx)))
|
|
;; HACK: the tsx grammer doesn't work with the hightlighting provided by
|
|
;; font-lock-keywords. See emacs-tree-sitter/tree-sitter-langs#23
|
|
(setq-hook! 'typescript-tsx-mode-hook
|
|
tree-sitter-hl-use-font-lock-keywords nil)))
|
|
|
|
(set-docsets! '(typescript-mode typescript-tsx-mode)
|
|
:add "TypeScript" "AngularTS")
|
|
(set-electric! '(typescript-mode typescript-tsx-mode)
|
|
:chars '(?\} ?\))
|
|
:words '("||" "&&"))
|
|
;; HACK Fixes comment continuation on newline
|
|
(autoload 'js2-line-break "js2-mode" nil t)
|
|
(setq-hook! 'typescript-mode-hook
|
|
comment-line-break-function #'js2-line-break
|
|
|
|
;; Most projects use either eslint, prettier, .editorconfig, or tsf in order
|
|
;; to specify indent level and formatting. In the event that no
|
|
;; project-level config is specified (very rarely these days), the community
|
|
;; default is 2, not 4. However, respect what is in tsfmt.json if it is
|
|
;; present in the project
|
|
typescript-indent-level
|
|
(or (and (bound-and-true-p tide-mode)
|
|
(plist-get (tide-tsfmt-options) :indentSize))
|
|
typescript-indent-level)
|
|
|
|
;; Fix #5556: expand .x to className="x" instead of class="x", if
|
|
;; `emmet-mode' is used.
|
|
emmet-expand-jsx-className? t))
|
|
|
|
|
|
;;
|
|
;;; Tools
|
|
|
|
(when (modulep! +tree-sitter)
|
|
(add-hook! '(js2-mode-local-vars-hook
|
|
typescript-mode-local-vars-hook
|
|
typescript-tsx-mode-local-vars-hook
|
|
rjsx-mode-local-vars-hook)
|
|
:append #'tree-sitter!))
|
|
|
|
(add-hook! '(typescript-mode-local-vars-hook
|
|
typescript-tsx-mode-local-vars-hook
|
|
web-mode-local-vars-hook
|
|
rjsx-mode-local-vars-hook)
|
|
(defun +javascript-init-lsp-or-tide-maybe-h ()
|
|
"Start `lsp' or `tide' in the current buffer.
|
|
|
|
LSP will be used if the +lsp flag is enabled for :lang javascript AND if the
|
|
current buffer represents a file in a project.
|
|
|
|
If LSP fails to start (e.g. no available server or project), then we fall back
|
|
to tide."
|
|
(let ((buffer-file-name (buffer-file-name (buffer-base-buffer))))
|
|
(when (derived-mode-p 'js-mode 'typescript-mode 'typescript-tsx-mode)
|
|
(if (null buffer-file-name)
|
|
;; necessary because `tide-setup' and `lsp' will error if not a
|
|
;; file-visiting buffer
|
|
(add-hook 'after-save-hook #'+javascript-init-lsp-or-tide-maybe-h
|
|
nil 'local)
|
|
(or (if (modulep! +lsp) (lsp!))
|
|
;; fall back to tide
|
|
(if (executable-find "node")
|
|
(and (require 'tide nil t)
|
|
(progn (tide-setup) tide-mode))
|
|
(ignore
|
|
(doom-log "Couldn't start tide because 'node' is missing"))))
|
|
(remove-hook 'after-save-hook #'+javascript-init-lsp-or-tide-maybe-h
|
|
'local))))))
|
|
|
|
|
|
(use-package! tide
|
|
:hook (tide-mode . tide-hl-identifier-mode)
|
|
:config
|
|
(set-company-backend! 'tide-mode 'company-tide)
|
|
;; navigation
|
|
(set-lookup-handlers! 'tide-mode :async t
|
|
:xref-backend #'xref-tide-xref-backend
|
|
:documentation #'tide-documentation-at-point)
|
|
(set-popup-rule! "^\\*tide-documentation" :quit t)
|
|
|
|
(setq tide-completion-detailed t
|
|
tide-always-show-documentation t
|
|
;; Fix #1792: by default, tide ignores payloads larger than 100kb. This
|
|
;; is too small for larger projects that produce long completion lists,
|
|
;; so we up it to 512kb.
|
|
tide-server-max-response-length 524288
|
|
;; We'll handle it
|
|
tide-completion-setup-company-backend nil)
|
|
|
|
;; Resolve to `doom-project-root' if `tide-project-root' fails
|
|
(advice-add #'tide-project-root :override #'+javascript-tide-project-root-a)
|
|
|
|
;; Cleanup tsserver when no tide buffers are left
|
|
(add-hook! 'tide-mode-hook
|
|
(add-hook 'kill-buffer-hook #'+javascript-cleanup-tide-processes-h
|
|
nil 'local))
|
|
|
|
;; Eldoc is activated too soon and disables itself, thinking there is no eldoc
|
|
;; support in the current buffer, so we must re-enable it later once eldoc
|
|
;; support exists. It is set *after* tide-mode is enabled, so enabling it on
|
|
;; `tide-mode-hook' is too early, so...
|
|
(advice-add #'tide-setup :after #'eldoc-mode)
|
|
|
|
(map! :localleader
|
|
:map tide-mode-map
|
|
"R" #'tide-restart-server
|
|
"f" #'tide-format
|
|
"rrs" #'tide-rename-symbol
|
|
"roi" #'tide-organize-imports))
|
|
|
|
|
|
(use-package! js2-refactor
|
|
:hook ((js2-mode rjsx-mode) . js2-refactor-mode)
|
|
:init
|
|
(map! :after js2-mode
|
|
:map js2-mode-map
|
|
:localleader
|
|
(:prefix ("r" . "refactor")
|
|
(:prefix ("a" . "add/arguments"))
|
|
(:prefix ("b" . "barf"))
|
|
(:prefix ("c" . "contract"))
|
|
(:prefix ("d" . "debug"))
|
|
(:prefix ("e" . "expand/extract"))
|
|
(:prefix ("i" . "inject/inline/introduce"))
|
|
(:prefix ("l" . "localize/log"))
|
|
(:prefix ("o" . "organize"))
|
|
(:prefix ("r" . "rename"))
|
|
(:prefix ("s" . "slurp/split/string"))
|
|
(:prefix ("t" . "toggle"))
|
|
(:prefix ("u" . "unwrap"))
|
|
(:prefix ("v" . "var"))
|
|
(:prefix ("w" . "wrap"))
|
|
(:prefix ("3" . "ternary"))))
|
|
:config
|
|
(when (modulep! :editor evil +everywhere)
|
|
(add-hook 'js2-refactor-mode-hook #'evil-normalize-keymaps)
|
|
(let ((js2-refactor-mode-map (evil-get-auxiliary-keymap js2-refactor-mode-map 'normal t t)))
|
|
(js2r-add-keybindings-with-prefix (format "%s r" doom-localleader-key)))))
|
|
|
|
|
|
;;;###package skewer-mode
|
|
(map! :localleader
|
|
(:after js2-mode
|
|
:map js2-mode-map
|
|
"S" #'+javascript/skewer-this-buffer
|
|
:prefix ("s" . "skewer"))
|
|
:prefix "s"
|
|
(:after skewer-mode
|
|
:map skewer-mode-map
|
|
"E" #'skewer-eval-last-expression
|
|
"e" #'skewer-eval-defun
|
|
"f" #'skewer-load-buffer)
|
|
|
|
(:after skewer-css
|
|
:map skewer-css-mode-map
|
|
"e" #'skewer-css-eval-current-declaration
|
|
"r" #'skewer-css-eval-current-rule
|
|
"b" #'skewer-css-eval-buffer
|
|
"c" #'skewer-css-clear-all)
|
|
|
|
(:after skewer-html
|
|
:map skewer-html-mode-map
|
|
"e" #'skewer-html-eval-tag))
|
|
|
|
|
|
;;;###package npm-mode
|
|
(use-package! npm-mode
|
|
:hook ((js-mode typescript-mode) . npm-mode)
|
|
:config
|
|
(map! :localleader
|
|
(:map npm-mode-keymap
|
|
"n" npm-mode-command-keymap)
|
|
(:after js2-mode
|
|
:map js2-mode-map
|
|
:prefix ("n" . "npm"))))
|
|
|
|
|
|
;;
|
|
;;; Projects
|
|
|
|
(def-project-mode! +javascript-npm-mode
|
|
:modes '(html-mode
|
|
css-mode
|
|
web-mode
|
|
markdown-mode
|
|
js-mode ; includes js2-mode and rjsx-mode
|
|
json-mode
|
|
typescript-mode
|
|
solidity-mode)
|
|
:when (locate-dominating-file default-directory "package.json")
|
|
:add-hooks '(add-node-modules-path npm-mode))
|
|
|
|
(def-project-mode! +javascript-gulp-mode
|
|
:when (locate-dominating-file default-directory "gulpfile.js"))
|