blob: 8a8bed8d48b580fd940e56d8a216cd343fb36ffd (
plain) (
tree)
|
|
;;; 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.1824
;; 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
|