├── company_elixir_script.exs ├── LICENSE ├── README.md └── company-elixir.el /company_elixir_script.exs: -------------------------------------------------------------------------------- 1 | evaluator = IEx.Server.start_evaluator([]) 2 | Process.put(:evaluator, evaluator) 3 | 4 | defmodule CompanyElixirServer do 5 | def evaluator do 6 | {Process.get(:evaluator), self()} 7 | end 8 | 9 | def expand(expr) do 10 | case IEx.Autocomplete.expand(Enum.reverse(expr), __MODULE__) do 11 | {:yes, _, result} -> result 12 | _ -> :not_found 13 | end 14 | end 15 | end 16 | 17 | IEx.configure(inspect: [limit: :infinity]) 18 | Logger.remove_backend(:console) 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ayrat Badykov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # company-elixir 2 | 3 | `company-mode` completion backend for Elixir. 4 | 5 | It uses a separate IEx (Elixir's interactive shell) process to fetch completions from. 6 | 7 | WARNING: This is an experimental package. It can be used as an example of an async backend for the Company completion framework - see [my post](https://www.badykov.com/emacs/2020/05/05/async-company-mode-backend/). It works but I don't use anymore. 8 | 9 | ## Installation 10 | 11 | Add `company-elixir` to your load path: 12 | 13 | ``` emacs-lisp 14 | (add-to-list 'load-path "path/to/company-elixir.el") 15 | ``` 16 | 17 | Add `elixir-mode` hook: 18 | 19 | ``` emacs-lisp 20 | (add-hook 'elixir-mode-hook 'company-elixir-hook) 21 | ``` 22 | 23 | ### Notes 24 | 25 | - Since `company-elixir` uses IEx to fetch completions from, your Elixir project should be in a correct state so it can be compiled to run IEx. 26 | - `company-elixir` finds completions for modules only by full path. For example, `Foo.Bar.` will work, `alias Foo.Bar; Bar.` won't work. See `More features` section. 27 | - Keep in mind that EIx starts your application. So if your processes do some heavy work, you may want to disable them. `company-elixir` supports customization of iex command with `company-elixir-iex-command` variable. For example, you can modify it with your custom environment variables. 28 | 29 | ### More features 30 | 31 | - Support of completion of aliased modules will come soon. 32 | 33 | ### Potential features 34 | 35 | Since we already obtained `IEx` process, we can use it to: 36 | 37 | - Fetch docs with [`b/1`](https://hexdocs.pm/iex/IEx.Helpers.html#b/1) 38 | - Interpret Elixir expressions from Emacs 39 | -------------------------------------------------------------------------------- /company-elixir.el: -------------------------------------------------------------------------------- 1 | ;;; Code: -*- lexical-binding: t; -*- 2 | 3 | (require 'ansi-color) 4 | (require 'cl-lib) 5 | (require 'company) 6 | 7 | (defgroup company-elixir nil 8 | "Company-Elixir group." 9 | :prefix "company-elixir-" 10 | :group 'company-elixir) 11 | 12 | (defcustom company-elixir-iex-command "iex -S mix" 13 | "Command used to start iex." 14 | :type 'string 15 | :group 'company-elixir) 16 | 17 | (defcustom company-elixir-major-mode #'elixir-mode 18 | "Major mode for company-elixir." 19 | :type 'function 20 | :group 'company-elixir) 21 | 22 | (defun company-elixir--project-root () 23 | "Find the root of the current mix project." 24 | (let ((closest-path (or buffer-file-name default-directory))) 25 | (if (string-match-p (regexp-quote "apps") closest-path) 26 | (let* ((potential-umbrella-root-parts (butlast (split-string closest-path "/apps/"))) 27 | (potential-umbrella-root (mapconcat #'identity potential-umbrella-root-parts "")) 28 | (umbrella-app-root (mix--find-closest-mix-file-dir potential-umbrella-root))) 29 | (or umbrella-app-root (mix--find-closest-mix-file-dir closest-path))) 30 | (company-elixir--find-closest-mix-file-dir closest-path)))) 31 | 32 | (defun company-elixir--find-closest-mix-file-dir (path) 33 | "Find the closest mix file to the current buffer PATH." 34 | (let ((root (locate-dominating-file path "mix.exs"))) 35 | (when root 36 | (file-truename root)))) 37 | 38 | (defconst company-elixir--script-name "company_elixir_script.exs" "Name of the iex script file.") 39 | 40 | (defconst company-elixir--directory 41 | (if load-file-name (file-name-directory load-file-name) default-directory) 42 | "Iex script directory.") 43 | 44 | (defconst company-elixir--evaluator-init-code 45 | (with-temp-buffer 46 | (insert-file-contents (concat company-elixir--directory company-elixir--script-name)) 47 | (buffer-string)) 48 | "Iex evaluator code.") 49 | 50 | (defvar company-elixir--last-completion nil "The last expression that was completed.") 51 | 52 | (defvar company-elixir--callback nil "Company callback to return candidates to.") 53 | 54 | (defvar company-elixir--processes nil "Iex processes for different projects used by company-elixir.") 55 | 56 | (defun company-elixir--create-process(project-path) 57 | "Create a new iex process for PROJECT-PATH." 58 | (let* ((default-directory project-path) 59 | (process (start-process-shell-command project-path project-path company-elixir-iex-command)) 60 | (key-value-pair (cons project-path process))) 61 | (set-process-query-on-exit-flag process nil) 62 | (process-send-string process company-elixir--evaluator-init-code) 63 | (set-process-filter process #'company-elixir--candidates-filter) 64 | (setq company-elixir--processes (assoc-delete-all project-path company-elixir--processes)) 65 | (setq company-elixir--processes (cons key-value-pair company-elixir--processes)) 66 | process)) 67 | 68 | (defun company-elixir--process (project-path) 69 | "Return an iex process if it exists or create a new one otherwise for PROJECT-PATH." 70 | (let ((existing-process (cdr (assoc project-path company-elixir--processes)))) 71 | (if (and existing-process (process-live-p existing-process)) 72 | existing-process 73 | (company-elixir--create-process project-path)))) 74 | 75 | (defun company-elixir--init-code() 76 | "Read iex evaluator if it's not read or return if it's read." 77 | (or company-elixir--evaluator-init-code 78 | (setq company-elixir--evaluator-init-code (company-elixir--read-init-code)))) 79 | 80 | (defun company-elixir--candidates-filter (_process output) 81 | "Filter OUTPUT from iex process and redirect them to company." 82 | (let ((output-without-ansi-chars (ansi-color-apply output))) 83 | (set-text-properties 0 (length output-without-ansi-chars) nil output-without-ansi-chars) 84 | (let ((output-without-iex (car (split-string output-without-ansi-chars "iex")))) 85 | (if (string-match "\\[" output-without-iex) 86 | (let ((candidates (split-string output-without-iex "\[\],[ \f\t\n\r\v']+" t))) 87 | (company-elixir--return-candidates candidates)))))) 88 | 89 | (defun company-elixir--find-candidates(expr) 90 | "Send request for completion to iex process with EXPR." 91 | (process-send-string (company-elixir--process (company-elixir--project-root)) 92 | (concat "CompanyElixirServer.expand('" expr "')\n"))) 93 | 94 | (defun company-elixir (command &optional arg &rest ignored) 95 | "Completion backend for company-mode." 96 | (interactive (list 'interactive)) 97 | (cl-case command 98 | (interactive (company-begin-backend 'company-elixir)) 99 | (prefix (and (eq major-mode company-elixir-major-mode) 100 | (company-elixir--get-prefix))) 101 | (candidates (cons :async 102 | (lambda (callback) 103 | (setq company-elixir--callback callback) 104 | (setq company-elixir--last-completion arg) 105 | (company-elixir--find-candidates arg)))))) 106 | 107 | (defun company-elixir--return-candidates (candidates) 108 | "Return CANDIDATES to company-mode." 109 | (if company-elixir--callback 110 | (let* ((prefix (cond 111 | ((string-match-p "\\.$" company-elixir--last-completion) company-elixir--last-completion) 112 | ((not (string-match-p "\\." company-elixir--last-completion)) "") 113 | (t (let* ((parts (split-string company-elixir--last-completion "\\.")) 114 | (last-part (car (last parts))) 115 | (last-part-regex (concat last-part "$"))) 116 | (replace-regexp-in-string last-part-regex "" company-elixir--last-completion))))) 117 | (completions (mapcar 118 | (lambda(var) 119 | (concat prefix 120 | (replace-regexp-in-string "/[[:digit:]]$" "" var))) candidates)) 121 | (completions-without-dups (cl-remove-duplicates completions :test #'equal))) 122 | (funcall company-elixir--callback completions-without-dups)))) 123 | 124 | (defun company-elixir--get-prefix () 125 | "Return the expression under the cursor." 126 | (if (or (looking-at "\s") (eolp)) 127 | (let (p1 p2 (skip-chars "-_A-Za-z0-9.?!@:")) 128 | (save-excursion 129 | (skip-chars-backward skip-chars) 130 | (setq p1 (point)) 131 | (skip-chars-forward skip-chars) 132 | (setq p2 (point)) 133 | (buffer-substring-no-properties p1 p2))))) 134 | 135 | (defun company-elixir-hook() 136 | "Add elixir-company to company-backends." 137 | (add-to-list (make-local-variable 'company-backends) 138 | 'company-elixir)) 139 | 140 | (provide 'company-elixir) 141 | ;;; company-elixir ends here 142 | --------------------------------------------------------------------------------