1;;; alchemist-server.el --- Interface to the Alchemist Elixir server. -*- lexical-binding: t -*-
2
3;; Copyright © 2015 Samuel Tonini
4
5;; Author: Samuel Tonini <tonini.samuel@gmail.com
6
7;; This file is not part of GNU Emacs.
8
9;; This program is free software: you can redistribute it and/or modify
10;; it under the terms of the GNU General Public License as published by
11;; the Free Software Foundation, either version 3 of the License, or
12;; (at your option) any later version.
13
14;; This program is distributed in the hope that it will be useful,
15;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17;; GNU General Public License for more details.
18
19;; You should have received a copy of the GNU General Public License
20;; along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22;;; Commentary:
23
24;; Interface to the Alchemist Elixir server.
25
26;;; Code:
27
28(require 'alchemist-execute)
29
30(defgroup alchemist-server nil
31  "Interface to the Alchemist Elixir server."
32  :prefix "alchemist-server-"
33  :group 'alchemist)
34
35(defvar alchemist-server-processes '()
36  "Store running Alchemist server processes.")
37
38(defvar alchemist-server-env "dev"
39  "Default environment in for the Alchemist server.")
40
41(defvar alchemist-server-envs '("dev" "prod" "test" "shared")
42  "List of available Alchemist server environments.")
43
44(defconst alchemist-server
45  (concat (file-name-directory load-file-name) "alchemist-server/run.exs")
46  "Path to the Alchemist server file.")
47
48(defconst alchemist-server-command
49  (format "%s %s %s"
50          alchemist-execute-command
51          alchemist-server
52          alchemist-server-env)
53  "Alchemist server command.")
54
55(defconst alchemist-server-codes '((server-eval "EVAL")
56                                   (server-defl "DEFL")
57                                   (server-info "INFO")
58                                   (server-docl "DOCL")
59                                   (server-comp "COMP"))
60  "Alchemist server API codes.")
61
62(defun alchemist-server-start (env)
63  "Start alchemist server for the current mix project in specific ENV.
64
65If a server already running, the current one will be killed and new one
66will be started instead."
67  (interactive (list
68                (completing-read (format "(Alchemist-Server) run in environment: (default: %s) " alchemist-server-env)
69                                 alchemist-server-envs nil nil nil)))
70  (when (alchemist-server-process-p)
71    (kill-process (alchemist-server-process)))
72  (alchemist-server-start-in-env env))
73
74(defun alchemist-server-start-if-not-running ()
75  "Start a new Alchemist server if not already running.
76
77An Alchemist server will be started for the current Elixir mix project."
78  (unless (alchemist-server-process-p)
79    (alchemist-server-start-in-env alchemist-server-env)))
80
81(defun alchemist-server-start-in-env (env)
82  "Start an Alchemist server with the ENV."
83  (let* ((process-name (alchemist-server-process-name))
84         (default-directory (if (string= process-name "alchemist-server")
85                                default-directory
86                              process-name))
87         (server-command (format "%s %s %s"
88                                 alchemist-execute-command
89                                 (shell-quote-argument alchemist-server)
90                                 (shell-quote-argument env)))
91         (process (start-process-shell-command process-name "*alchemist-server*" server-command)))
92    (set-process-query-on-exit-flag process nil)
93    (alchemist-server--store-process process)))
94
95(defun alchemist-server--store-process (process)
96  "Store PROCESS in `alchemist-server-processes'."
97  (let ((process-name (alchemist-server-process-name)))
98    (if (cdr (assoc process-name alchemist-server-processes))
99        (setq alchemist-server-processes
100              (delq (assoc process-name alchemist-server-processes) alchemist-server-processes)))
101    (add-to-list 'alchemist-server-processes (cons process-name process))))
102
103(defun alchemist-server-process-p ()
104  "Return non-nil if a process for the current
105Elixir mix project is live."
106  (process-live-p (alchemist-server-process)))
107
108(defun alchemist-server-process ()
109  "Return process for the current Elixir mix project."
110  (cdr (assoc (alchemist-server-process-name) alchemist-server-processes)))
111
112(defun alchemist-server-process-name ()
113  "Return process name for the current Elixir mix project."
114  (let* ((process-name (if (alchemist-project-elixir-p)
115                           "alchemist-server"
116                         (alchemist-project-root)))
117         (process-name (if process-name
118                           process-name
119                         "alchemist-server")))
120    process-name))
121
122(defun alchemist-server-api-code (symbol)
123  "Return Alchemist server API code for SYMBOL."
124  (car (cdr (assoc symbol alchemist-server-codes))))
125
126(defconst alchemist-server-code-end-marker-regex
127  (format "END-OF-\\(%s\\|%s\\|%s\\|%s\\|%s\\)$"
128          (alchemist-server-api-code 'server-eval)
129          (alchemist-server-api-code 'server-defl)
130          (alchemist-server-api-code 'server-info)
131          (alchemist-server-api-code 'server-docl)
132          (alchemist-server-api-code 'server-comp))
133  "Regular expression to identify Alchemist server API end markers.")
134
135(defun alchemist-server-contains-end-marker-p (string)
136  "Return non-nil if STRING contain an Alchemist server API end marker."
137  (when string
138    (string-match-p alchemist-server-code-end-marker-regex string)))
139
140(defun alchemist-server-build-request-string (code &optional args)
141  "Build Alchemist server request string for CODE.
142
143If ARGS available add them to the request string."
144  (let* ((code (car (cdr (assoc code alchemist-server-codes)))))
145    (if args
146        (format "%s %s\n" code args)
147      (format "%s\n" code))))
148
149(defun alchemist-server-prepare-filter-output (output)
150  "Clean OUTPUT by remove Alchemist server API end markes."
151  (let* ((output (apply #'concat (reverse output)))
152         (output (replace-regexp-in-string alchemist-server-code-end-marker-regex "" output))
153         (output (replace-regexp-in-string "\n+$" "" output)))
154    output))
155
156(defun alchemist-server-send-request (string filter)
157  "Send STRING to Alchemist server API and set FILTER to process."
158  (alchemist-server-start-if-not-running)
159  (set-process-filter (alchemist-server-process) filter)
160  (process-send-string (alchemist-server-process) string))
161
162(defun alchemist-server-goto (args filter)
163  "Make an Alchemist server source request with ARGS.
164
165Process server respond with FILTER."
166  (alchemist-server-start-if-not-running)
167  (alchemist-server-send-request (alchemist-server-build-request-string 'server-defl args) filter))
168
169(defun alchemist-server-info (args filter)
170  "Make an Alchemist server mix request.
171
172Process server respond with FILTER."
173  (alchemist-server-start-if-not-running)
174  (alchemist-server-send-request (alchemist-server-build-request-string 'server-info args) filter))
175
176(defun alchemist-server-help-with-modules (filter)
177  "Make an Alchemist server modules request.
178
179Process server respond with FILTER."
180  (alchemist-server-start-if-not-running)
181  (alchemist-server-send-request (alchemist-server-build-request-string 'server-info) filter))
182
183(defun alchemist-server-help (args filter)
184  "Make an Alchemist server doc request with ARGS.
185
186Process server respond with FILTER."
187  (alchemist-server-start-if-not-running)
188  (alchemist-server-send-request (alchemist-server-build-request-string 'server-docl args) filter))
189
190(defun alchemist-server-eval (args filter)
191  "Make an Alchemist server evaluate request with FILE.
192
193Process server respond with FILTER."
194  (alchemist-server-start-if-not-running)
195  (alchemist-server-send-request (alchemist-server-build-request-string 'server-eval args) filter))
196
197(defun alchemist-server-complete-candidates (args filter)
198  "Make an Alchemist server complete request with ARGS.
199
200Process server respond with FILTER."
201  (alchemist-server-start-if-not-running)
202  (alchemist-server-send-request (alchemist-server-build-request-string 'server-comp args) filter))
203
204(defun alchemist-server-status ()
205  "Report the server status for the current Elixir project."
206  (interactive)
207  (message "Alchemist-Server-Status: [Project: %s Status: %s]"
208           (alchemist-server-process-name)
209           (if (alchemist-server-process-p)
210               "Connected"
211             "Not Connected")))
212
213(provide 'alchemist-server)
214
215;;; alchemist-server.el ends here
216