;;; company-flow.el --- Flow backend for company-mode -*- lexical-binding: t -*- ;; Copyright (C) 2016 by Aaron Jensen ;; Author: Aaron Jensen ;; URL: https://github.com/aaronjensen/company-flow ;; Package-Version: 20180225.2159 ;; Version: 0.1.0 ;; Package-Requires: ((company "0.8.0") (dash "2.13.0")) ;;; Commentary: ;; This package adds support for flow to company. It requires ;; flow to be in your path. ;; To use it, add to your company-backends: ;; (eval-after-load 'company ;; '(add-to-list 'company-backends 'company-flow)) ;;; License: ;; This file is not part of GNU Emacs. ;; However, it is distributed under the same license. ;; GNU Emacs 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, or (at your option) ;; any later version. ;; GNU Emacs 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 GNU Emacs; see the file COPYING. If not, write to the ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, ;; Boston, MA 02110-1301, USA. ;;; Code: (require 'company) (require 'dash) (defgroup company-flow () "Flow company backend." :group 'company :prefix "company-flow-") (defcustom company-flow-executable "flow" "Flow executable to run." :group 'company-flow :type 'string) (make-variable-buffer-local 'company-flow-executable) (defcustom company-flow-modes '( js-mode js-jsx-mode js2-mode js2-jsx-mode rjsx-mode web-mode ) "List of major modes where company-flow will be providing completions." :type '(choice (const :tag "All" nil) (repeat (symbol :tag "Major mode"))) :group 'company-flow) (defun company-flow--handle-signal (process _event) (when (memq (process-status process) '(signal exit)) (let ((callback (process-get process 'company-flow-callback)) (prefix (process-get process 'company-flow-prefix))) (if (and (eq (process-status process) 'exit) (eq (process-exit-status process) 0)) (funcall callback (->> process company-flow--get-output company-flow--parse-output ;; Remove nils (--filter it))) (funcall callback nil))))) (defun company-flow--make-candidate (line) "Creates a candidate with a meta property from LINE. LINE is expected to look like: registrationSuccess () => {type: 'REGISTRATION_SUCCESS'}" (let ((first-space (string-match " " line))) (when first-space (let ((text (substring line 0 first-space)) (meta (substring line (+ 1 first-space)))) (propertize text 'meta meta))))) (defun company-flow--parse-output (output) (when (not (equal output "Error: not enough type information to autocomplete\n")) (mapcar 'company-flow--make-candidate (split-string output "\n")))) (defun company-flow--get-output (process) "Get the complete output of PROCESS." (with-demoted-errors "Error while retrieving process output: %S" (let ((pending-output (process-get process 'company-flow-pending-output))) (apply #'concat (nreverse pending-output))))) (defun company-flow--receive-checker-output (process output) "Receive a syntax checking PROCESS OUTPUT." (push output (process-get process 'company-flow-pending-output))) (defun company-flow--process-send-buffer (process) "Send all contents of current buffer to PROCESS. Sends all contents of the current buffer to the standard input of PROCESS, and terminates standard input with EOF." (save-restriction (widen) (process-send-region process (point-min) (point-max))) ;; flow requires EOF be on its own line (process-send-string process "\n") (process-send-eof process)) (defun company-flow--candidates-query (prefix callback) (let* ((line (line-number-at-pos (point))) (col (+ 1 (current-column))) (command (list (executable-find company-flow-executable) "autocomplete" "--quiet" buffer-file-name (number-to-string line) (number-to-string col))) (process-connection-type nil) (process (apply 'start-process "company-flow" nil command))) (set-process-sentinel process #'company-flow--handle-signal) (set-process-filter process #'company-flow--receive-checker-output) (process-put process 'company-flow-callback callback) (process-put process 'company-flow-prefix prefix) (company-flow--process-send-buffer process))) (defun company-flow--prefix () "Grab prefix for flow." (and (or (null company-flow-modes) (-contains? company-flow-modes major-mode)) company-flow-executable (executable-find company-flow-executable) buffer-file-name (file-exists-p buffer-file-name) (not (company-in-string-or-comment)) (locate-dominating-file buffer-file-name ".flowconfig") (or (company-grab-symbol-cons "\\." 1) 'stop))) (defun company-flow--annotation (candidate) (format " %s" (get-text-property 0 'meta candidate))) (defun company-flow--meta (candidate) (format "%s: %s" candidate (get-text-property 0 'meta candidate))) (defvar-local company-flow--debounce-state nil) (defun company-flow--debounce-callback (prefix callback) (lambda (candidates) (let ((current-prefix (car company-flow--debounce-state)) (current-callback (cdr company-flow--debounce-state))) (when (and current-prefix (company-flow--string-prefix-p prefix current-prefix)) (setq company-flow--debounce-state nil) (funcall current-callback (all-completions current-prefix candidates)))))) (defun company-flow--prefix-to-string (prefix) "Return a string or nil from a prefix. `company-grab-symbol-cons' can return (\"prefix\" . t) or just \"prefix\", but we only care about the string." (if (consp prefix) (car prefix) prefix)) (defun company-flow--string-prefix-p (a b) (string-prefix-p (company-flow--prefix-to-string a) (company-flow--prefix-to-string b))) (defun company-flow--debounce-async (prefix candidate-fn) "Return a function that will properly debounce candidate queries by comparing the in-flight query's prefix to PREFIX. CANDIDATE-FN should take two arguments, PREFIX and the typical async callback. Note that the candidate list provided to the callback by CANDIDATE-FN will be filtered via `all-completions' with the most current prefix, so it is not necessary to do this filtering in CANDIDATE-FN. Use like: (cons :async (company-flow--debounce-async arg 'your-query-fn))" (lambda (callback) (let ((current-prefix (car company-flow--debounce-state))) (unless (and current-prefix (company-flow--string-prefix-p prefix current-prefix)) (funcall candidate-fn prefix (company-flow--debounce-callback prefix callback))) (setq company-flow--debounce-state (cons (company-flow--prefix-to-string prefix) callback))))) ;;;###autoload (defun company-flow (command &optional arg &rest _args) (interactive (list 'interactive)) (pcase command (`interactive (company-begin-backend 'company-flow)) (`prefix (company-flow--prefix)) (`annotation (company-flow--annotation arg)) (`meta (company-flow--meta arg)) (`sorted t) (`candidates (cons :async (company-flow--debounce-async arg 'company-flow--candidates-query))))) (provide 'company-flow) ;;; company-flow.el ends here