17ee0e400b
After moving off of Meta, Dotfiles has a greater responsibility to manage configs. Vim, Tmux, and Emacs are now within Stow's purview.
323 lines
11 KiB
EmacsLisp
323 lines
11 KiB
EmacsLisp
;;; flow-minor-mode.el --- Flow type mode based on web-mode. -*- lexical-binding: t -*-
|
||
|
||
;; This source code is licensed under the BSD-style license found in
|
||
;; the LICENSE file in the root directory of this source tree.
|
||
|
||
;; Version: 0.3
|
||
;; Package-Version: 20180315.1124
|
||
;; URL: https://github.com/an-sh/flow-minor-mode
|
||
|
||
;; Package-Requires: ((emacs "25.1"))
|
||
|
||
;;; Commentary:
|
||
|
||
;; Minor mode for flowtype.org, derived from web-mode. Essentially a
|
||
;; rewrite of an official flow-for-emacs snippet into a standalone
|
||
;; mode with an improved usability.
|
||
;;
|
||
;; To enable this mode, enable it in your preferred javascript mode's
|
||
;; hooks:
|
||
;;
|
||
;; (add-hook 'js2-mode-hook 'flow-minor-enable-automatically)
|
||
;;
|
||
;; This will enable flow-minor-mode for a file only when there is a
|
||
;; "// @flow" declaration at the first line and a `.flowconfig` file
|
||
;; is present in the project. If you wish to enable flow-minor-mode
|
||
;; for all javascript files, use this instead:
|
||
;;
|
||
;; (add-hook 'js2-mode-hook 'flow-minor-mode)
|
||
;;
|
||
;;; Code:
|
||
|
||
(require 'xref)
|
||
(require 'json)
|
||
(require 'compile)
|
||
|
||
(defconst flow-minor-buffer "*Flow Output*")
|
||
|
||
(defcustom flow-minor-default-binary "flow"
|
||
"Flow executable to use when no project-specific binary is found."
|
||
:group 'flow-minor-mode
|
||
:type 'string)
|
||
|
||
(defcustom flow-minor-jump-other-window nil
|
||
"Jump to definitions in other window."
|
||
:group 'flow-minor-mode
|
||
:type 'boolean)
|
||
|
||
(defcustom flow-minor-stop-server-on-exit t
|
||
"Stop flow server when Emacs exits."
|
||
:group 'flow-minor-mode
|
||
:type 'boolean)
|
||
|
||
(defun flow-minor-column-at-pos (position)
|
||
"Column number at position.
|
||
POSITION point"
|
||
(save-excursion (goto-char position) (current-column)))
|
||
|
||
(defun flow-minor-region ()
|
||
"Format region data."
|
||
(if (use-region-p)
|
||
(let ((begin (region-beginning))
|
||
(end (region-end)))
|
||
(format ":%d:%d,%d:%d"
|
||
(line-number-at-pos begin)
|
||
(flow-minor-column-at-pos begin)
|
||
(line-number-at-pos end)
|
||
(flow-minor-column-at-pos end)))
|
||
""))
|
||
|
||
(defun flow-minor-binary ()
|
||
"Search for a local or global flow binary."
|
||
(let* ((root (locate-dominating-file
|
||
(or (buffer-file-name) default-directory)
|
||
"node_modules"))
|
||
(flow (and root
|
||
(expand-file-name "node_modules/.bin/flow"
|
||
root))))
|
||
(if (and flow (file-executable-p flow))
|
||
flow
|
||
flow-minor-default-binary)))
|
||
|
||
(defun flow-minor-cmd (&rest args)
|
||
"Run flow with arguments, outputs to flow buffer.
|
||
ARGS"
|
||
(apply #'call-process (flow-minor-binary) nil flow-minor-buffer t args))
|
||
|
||
(defun flow-minor-cmd-ignore-output (&rest args)
|
||
"Run flow with arguments, ignore output.
|
||
ARGS"
|
||
(apply #'call-process (flow-minor-binary) nil nil nil args))
|
||
|
||
(defun flow-minor-cmd-to-string (&rest args)
|
||
"Run flow with arguments, outputs to string.
|
||
ARGS"
|
||
(with-temp-buffer
|
||
(apply #'call-process (flow-minor-binary) nil t nil args)
|
||
(buffer-string)))
|
||
|
||
(defmacro flow-minor-with-flow (&rest body)
|
||
"With flow.
|
||
BODY progn"
|
||
`(progn
|
||
(flow-minor-cmd-ignore-output "start")
|
||
,@body))
|
||
|
||
(defmacro flow-minor-region-command (region-sym &rest body)
|
||
"Flow command on a region.
|
||
REGION-SYM symbol
|
||
BODY progn"
|
||
(declare (indent defun))
|
||
`(flow-minor-with-flow
|
||
(let ((,region-sym (concat (buffer-file-name) (flow-minor-region))))
|
||
(switch-to-buffer-other-window flow-minor-buffer)
|
||
(setf buffer-read-only nil)
|
||
(erase-buffer)
|
||
,@body)))
|
||
|
||
(defun flow-minor-status ()
|
||
"Show errors."
|
||
(interactive)
|
||
(flow-minor-region-command region
|
||
(flow-minor-cmd "status" "--from" "emacs")
|
||
(compilation-mode)
|
||
(setf buffer-read-only t)))
|
||
|
||
(defun flow-minor-suggest ()
|
||
"Fill types."
|
||
(interactive)
|
||
(flow-minor-region-command region
|
||
(flow-minor-cmd "suggest" region)
|
||
(diff-mode)
|
||
(setf buffer-read-only t)))
|
||
|
||
(defun flow-minor-coverage ()
|
||
"Show coverage."
|
||
(interactive)
|
||
(flow-minor-region-command region
|
||
(message "%s" region)
|
||
(flow-minor-cmd "coverage" region)
|
||
(compilation-mode)
|
||
(setf buffer-read-only t)))
|
||
|
||
(defvar flow-type-font-lock-highlight
|
||
'(
|
||
("\\([-_[:alnum:]]+\\)\\??:" . font-lock-variable-name-face)
|
||
("\\_<\\(true\\|false\\|null\\|undefined\\|void\\)\\_>" . font-lock-constant-face)
|
||
("<\\([-_[:alnum:]]+\\)>" . font-lock-type-face)
|
||
))
|
||
|
||
(defun flow-minor-colorize-buffer ()
|
||
(setq font-lock-defaults '(flow-type-font-lock-highlight))
|
||
(font-lock-fontify-buffer))
|
||
|
||
(defun flow-minor-colorize-type (text)
|
||
(with-temp-buffer
|
||
(insert text)
|
||
(flow-minor-colorize-buffer)
|
||
(buffer-string)))
|
||
|
||
(defun flow-minor-type-at-pos ()
|
||
"Show type at position."
|
||
(interactive)
|
||
(flow-minor-with-flow
|
||
(let* ((file (buffer-file-name))
|
||
(line (number-to-string (line-number-at-pos)))
|
||
(col (number-to-string (1+ (current-column))))
|
||
(type (flow-minor-cmd-to-string "type-at-pos" file line col)))
|
||
(message "%s" (flow-minor-colorize-type (car (split-string type "\n")))))))
|
||
|
||
(defun flow-minor-jump-to-definition ()
|
||
"Jump to definition."
|
||
(interactive)
|
||
(flow-minor-with-flow
|
||
(let* ((file (buffer-file-name))
|
||
(line (number-to-string (line-number-at-pos)))
|
||
(col (number-to-string (1+ (current-column))))
|
||
(location (json-read-from-string
|
||
(flow-minor-cmd-to-string "get-def" "--json" file line col)))
|
||
(path (alist-get 'path location))
|
||
(line (alist-get 'line location))
|
||
(offset-in-line (alist-get 'start location)))
|
||
(if (> (length path) 0)
|
||
(progn
|
||
(xref-push-marker-stack)
|
||
(funcall (if flow-minor-jump-other-window #'find-file-other-window #'find-file) path)
|
||
(goto-line line)
|
||
(when (> offset-in-line 0)
|
||
(forward-char (1- offset-in-line))))
|
||
(message "Not found")))))
|
||
|
||
(defvar flow-minor-mode-map (make-sparse-keymap)
|
||
"Keymap for ‘flow-minor-mode’.")
|
||
|
||
(define-key flow-minor-mode-map (kbd "M-.") 'flow-minor-jump-to-definition)
|
||
(define-key flow-minor-mode-map (kbd "M-,") 'xref-pop-marker-stack)
|
||
|
||
(define-key flow-minor-mode-map (kbd "C-c C-c s") 'flow-minor-status)
|
||
(define-key flow-minor-mode-map (kbd "C-c C-c c") 'flow-minor-coverage)
|
||
(define-key flow-minor-mode-map (kbd "C-c C-c t") 'flow-minor-type-at-pos)
|
||
(define-key flow-minor-mode-map (kbd "C-c C-c f") 'flow-minor-suggest)
|
||
|
||
(define-key flow-minor-mode-map [menu-bar flow-minor-mode]
|
||
(cons "Flow" flow-minor-mode-map))
|
||
|
||
(define-key flow-minor-mode-map [menu-bar flow-minor-mode flow-minor-mode-s]
|
||
'(menu-item "Flow status" flow-minor-status))
|
||
|
||
(define-key flow-minor-mode-map [menu-bar flow-minor-mode flow-minor-mode-c]
|
||
'(menu-item "Flow coverage" flow-minor-coverage))
|
||
|
||
(define-key flow-minor-mode-map [menu-bar flow-minor-mode flow-minor-mode-t]
|
||
'(menu-item "Type at point" flow-minor-type-at-pos))
|
||
|
||
(define-key flow-minor-mode-map [menu-bar flow-minor-mode flow-minor-mode-f]
|
||
'(menu-item "Type suggestions" flow-minor-suggest))
|
||
|
||
(defun flow-minor-stop-flow-server ()
|
||
"Stop flow hook."
|
||
(if flow-minor-stop-server-on-exit (ignore-errors (flow-minor-cmd-ignore-output "stop"))))
|
||
|
||
(add-hook 'kill-emacs-hook 'flow-minor-stop-flow-server t)
|
||
|
||
(defun flow-minor-maybe-delete-process (name)
|
||
(when (get-process name)
|
||
(delete-process name)))
|
||
|
||
(defun flow-minor-eldoc-sentinel (process _event)
|
||
(when (eq (process-status process) 'exit)
|
||
(if (eq (process-exit-status process) 0)
|
||
(with-current-buffer "*Flow Eldoc*"
|
||
(goto-char (point-min))
|
||
(forward-line 1)
|
||
(delete-region (point) (point-max))
|
||
(flow-minor-colorize-buffer)
|
||
(eldoc-message (car (split-string (buffer-substring (point-min) (point-max)) "\n")))))))
|
||
|
||
(defun flow-minor-eldoc-documentation-function ()
|
||
"Display type at point with eldoc."
|
||
(flow-minor-maybe-delete-process "flow-minor-eldoc")
|
||
|
||
(let* ((line (line-number-at-pos (point)))
|
||
(col (+ 1 (current-column)))
|
||
(buffer (get-buffer-create "*Flow Eldoc*"))
|
||
(errorbuffer (get-buffer-create "*Flow Eldoc Error*"))
|
||
(command (list (flow-minor-binary)
|
||
"type-at-pos"
|
||
"--path" buffer-file-name
|
||
(number-to-string line)
|
||
(number-to-string col)))
|
||
(process (make-process :name "flow-minor-eldoc"
|
||
:buffer buffer
|
||
:command command
|
||
:connection-type 'pipe
|
||
:sentinel 'flow-minor-eldoc-sentinel
|
||
:stderr errorbuffer)))
|
||
(with-current-buffer buffer
|
||
(erase-buffer))
|
||
(with-current-buffer errorbuffer
|
||
(erase-buffer))
|
||
(save-restriction
|
||
(widen)
|
||
(process-send-region process (point-min) (point-max)))
|
||
(process-send-string process "\n")
|
||
(process-send-eof process))
|
||
nil)
|
||
|
||
;;;###autoload
|
||
(define-minor-mode flow-minor-mode
|
||
"Flow mode"
|
||
nil " Flow" flow-minor-mode-map
|
||
(if flow-minor-mode
|
||
(progn
|
||
(setq-local eldoc-documentation-function 'flow-minor-eldoc-documentation-function)
|
||
(eldoc-mode))))
|
||
|
||
(defun flow-minor-tag-present-p ()
|
||
"Return true if the '// @flow' tag is present in the current buffer."
|
||
(save-excursion
|
||
(goto-char (point-min))
|
||
(let (stop found)
|
||
(while (not stop)
|
||
(when (not (re-search-forward "[^\n[:space:]]" nil t))
|
||
(setq stop t))
|
||
(if (equal (point) (point-min))
|
||
(setq stop t)
|
||
(backward-char))
|
||
(cond ((or (looking-at "//+[ ]*@flow")
|
||
(looking-at "/\\**[ ]*@flow"))
|
||
(setq found t)
|
||
(setq stop t))
|
||
((looking-at "//")
|
||
(forward-line))
|
||
((looking-at "/\\*")
|
||
(when (not (re-search-forward "*/" nil t))
|
||
(setq stop t)))
|
||
(t (setq stop t))))
|
||
found)))
|
||
|
||
(defun flow-minor-configured-p ()
|
||
"Predicate to check configuration."
|
||
(locate-dominating-file
|
||
(or (buffer-file-name) default-directory)
|
||
".flowconfig"))
|
||
|
||
;;;###autoload
|
||
(defun flow-minor-enable-automatically ()
|
||
"Search for a flow marker and enable flow-minor-mode."
|
||
(when (and (flow-minor-configured-p)
|
||
(flow-minor-tag-present-p))
|
||
(flow-minor-mode +1)))
|
||
|
||
(defun flow-status ()
|
||
"Invoke flow to check types"
|
||
(interactive)
|
||
(let ((cmd "flow status")
|
||
(regexp '(flow "^\\(Error:\\)[ \t]+\\(\\(.+\\):\\([[:digit:]]+\\)\\)"
|
||
3 4 nil (1) 2 (1 compilation-error-face))))
|
||
(add-to-list 'compilation-error-regexp-alist 'flow)
|
||
(add-to-list 'compilation-error-regexp-alist-alist regexp)
|
||
(compile cmd)))
|
||
|
||
(provide 'flow-minor-mode)
|
||
;;; flow-minor-mode.el ends here
|