commit ccb24dd46e2d4dd7cfc5ab1deb2452625f7161ba Author: Wojciech Kozlowski Date: Sat Sep 9 00:25:09 2017 +0100 Squashed 'emacs-racer/' content from commit 6e0d1b3 git-subtree-dir: emacs-racer git-subtree-split: 6e0d1b3ebd54497c0cc995a92f09328ff101cd33 diff --git a/.ert-runner b/.ert-runner new file mode 100644 index 0000000..e35e9c9 --- /dev/null +++ b/.ert-runner @@ -0,0 +1 @@ +-L . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4691b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.cask diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..43a1c00 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +language: generic +before_install: + - curl -fsSkL https://gist.github.com/rejeep/ebcd57c3af83b049833b/raw > x.sh && source ./x.sh + - evm install $EVM_EMACS --use --skip + - cask +env: + - EVM_EMACS=emacs-24.3-travis + - EVM_EMACS=emacs-24.4-travis + - EVM_EMACS=emacs-24.5-travis + - EVM_EMACS=emacs-25.1-travis + - EVM_EMACS=emacs-git-snapshot-travis +script: + - emacs --version + - make test + +notifications: + email: false + +matrix: + fast_finish: true + allow_failures: + - env: EVM_EMACS=emacs-git-snapshot-travis diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..47e2cd6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,51 @@ +# v1.3 (unreleased) + +* `racer-rust-src-path` is now set automatically by default. +* New simpler installation instructions based on `rustup`. +* Fixed an issue with racer completion in indirect buffers. + +# v1.2 + +* Added the command `racer-debug` to help users diagnose issues. +* We now explicitly try `~/.cargo/bin/racer` if `racer` isn't on path. +* We no longer offer completions inside comments by default (it tends + to be slow and rarely offers completions). See + `racer-complete-in-comments`. +* Eldoc descriptions of modules now abbreviate the path relative to + the project and the user's home directory. +* Several improvements to markdown rendering in `racer-describe`. + +# v1.1 + +* Fixed a crash when point is at the beginning of buffer. +* Fixed a crash when not in a cargo project. +* Added `racer-cargo-home`, which enables completion for cargo crates. +* Various improvements to formatting of completion candidates. +* Added `racer-describe`. + +# v1.0.2 + +* Trigger completions after `::` or `.`. +* Compatibility with latest company +* Fixed an issue where TAGS from other projects were also completion + candidates + +# v1.0.1 + +No changes since v0.0.2. + +This release was created to [work around an issue +where MELPA stable](https://github.com/milkypostman/melpa/issues/3205) +had created a v1.0.0 from an early version of racer.el + +# v0.0.2 + +Initial release. Includes: + +* Code completion with company +* Jump to definition +* Eldoc + +Early users who are using `racer-activate` or `racer-turn-on-eldoc` +should use `racer-mode` and `eldoc-mode` instead. The former have been +deprecated. diff --git a/Cask b/Cask new file mode 100644 index 0000000..a597c84 --- /dev/null +++ b/Cask @@ -0,0 +1,14 @@ +(source gnu) +(source melpa) + +(package-file "racer.el") + +(depends-on "company") +(depends-on "dash") +(depends-on "s") +(depends-on "f") +(depends-on "rust-mode") + +(development + (depends-on "ert-runner") + (depends-on "undercover")) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2e34430 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +EMACS ?= emacs +CASK ?= cask + +all: test + +test: clean-elc + ${MAKE} unit + ${MAKE} compile + ${MAKE} unit + ${MAKE} clean-elc + +unit: + ${CASK} exec ert-runner + +compile: + ${CASK} exec ${EMACS} -Q -batch -f batch-byte-compile racer.el + +clean-elc: + rm -f racer.elc + +.PHONY: all test unit compile diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c075e0 --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ +# Racer for Emacs +[![MELPA](http://melpa.org/packages/racer-badge.svg)](http://melpa.org/#/racer) +[![MELPA Stable](http://stable.melpa.org/packages/racer-badge.svg)](http://stable.melpa.org/#/racer) +[![Build Status](https://travis-ci.org/racer-rust/emacs-racer.svg?branch=master)](https://travis-ci.org/racer-rust/emacs-racer) +[![Coverage Status](https://coveralls.io/repos/github/racer-rust/emacs-racer/badge.svg?branch=master)](https://coveralls.io/github/racer-rust/emacs-racer?branch=master) + +This is the official Emacs package for +[Racer](http://github.com/phildawes/racer). + + +**Table of Contents** + +- [Racer for Emacs](#racer-for-emacs) + - [Completion](#completion) + - [Find Definitions](#find-definitions) + - [Describe Functions and Types](#describe-functions-and-types) + - [Installation](#installation) + - [Testing your setup](#testing-your-setup) + - [Tests](#tests) + + + +## Completion + +racer.el supports code completion of variables, functions and modules. + +![racer completion screenshot](images/racer_completion.png) + +You can also press F1 to pop up a help buffer for the current +completion candidate. + +Note that due to a +[limitation of racer](https://github.com/phildawes/racer/issues/389), +racer.el cannot offer completion for macros. + +## Find Definitions + +racer.el can jump to definition of functions and types. + +![racer go to definition](images/racer_goto.gif) + +You can use M-. to go to the definition, and M-, +to go back. + +## Describe Functions and Types + +racer.el can show a help buffer based on the docstring of the thing at +point. + +![racer completion screenshot](images/racer_help.png) + +Use M-x racer-describe to open the help buffer. + +## Installation + +1. Install [Racer](http://github.com/phildawes/racer) and download the + source code of Rust: + + ``` + $ rustup component add rust-src + $ cargo install racer + ``` + +2. Allow Emacs to install packages from MELPA: + + ```el + (require 'package) + (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/")) + ``` + +3. Install the Emacs package for Racer: `M-x package-install RET racer RET` + +4. Configure Emacs to activate racer when rust-mode starts: + ```el + (add-hook 'rust-mode-hook #'racer-mode) + (add-hook 'racer-mode-hook #'eldoc-mode) + ``` + + For completions, install company with `M-x package-install RET company RET`. A sample configuration: + ```el + + (add-hook 'racer-mode-hook #'company-mode) + + (require 'rust-mode) + (define-key rust-mode-map (kbd "TAB") #'company-indent-or-complete-common) + (setq company-tooltip-align-annotations t) + ``` + For automatic completions, customize `company-idle-delay` and + `company-minimum-prefix-length`. + +### Testing your setup + +To test **completion**: Open a rust file and try typing ```use +std::io::B``` and press TAB. + +To test **go to definition**: Place your cursor over a symbol and press +`M-.` to jump to its definition. + +Press `M-,` to jump back to the previous cursor location. + +If **it doesn't work**, try `M-x racer-debug` to see what command was +run and what output was returned. + +## Tests + +racer.el includes tests. To run them, you need to install +[Cask](https://github.com/cask/cask), then: + +``` +$ cask install +$ cask exec ert-runner +``` diff --git a/images/racer_completion.png b/images/racer_completion.png new file mode 100644 index 0000000..6a7f0e4 Binary files /dev/null and b/images/racer_completion.png differ diff --git a/images/racer_goto.gif b/images/racer_goto.gif new file mode 100644 index 0000000..3bca8a2 Binary files /dev/null and b/images/racer_goto.gif differ diff --git a/images/racer_help.png b/images/racer_help.png new file mode 100644 index 0000000..fc473fa Binary files /dev/null and b/images/racer_help.png differ diff --git a/racer.el b/racer.el new file mode 100644 index 0000000..97c8095 --- /dev/null +++ b/racer.el @@ -0,0 +1,768 @@ +;;; racer.el --- code completion, goto-definition and docs browsing for Rust via racer -*- lexical-binding: t -*- + +;; Copyright (c) 2014 Phil Dawes + +;; Author: Phil Dawes +;; URL: https://github.com/racer-rust/emacs-racer +;; Version: 1.3 +;; Package-Requires: ((emacs "24.3") (rust-mode "0.2.0") (dash "2.13.0") (s "1.10.0") (f "0.18.2")) +;; Keywords: abbrev, convenience, matching, rust, tools + +;; This file is not part of GNU Emacs. + +;; Permission is hereby granted, free of charge, to any +;; person obtaining a copy of this software and associated +;; documentation files (the "Software"), to deal in the +;; Software without restriction, including without +;; limitation the rights to use, copy, modify, merge, +;; publish, distribute, sublicense, and/or sell copies of +;; the Software, and to permit persons to whom the Software +;; is furnished to do so, subject to the following +;; conditions: + +;; The above copyright notice and this permission notice +;; shall be included in all copies or substantial portions +;; of the Software. + +;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +;; ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +;; TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +;; PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +;; SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +;; CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +;; OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +;; IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +;; DEALINGS IN THE SOFTWARE. + +;;; Commentary: + +;; Please see the readme for full documentation: +;; https://github.com/racer-rust/emacs-racer + +;;; Quickstart: + +;; You will need to configure Emacs to find racer: +;; +;; (setq racer-rust-src-path "/src/") +;; (setq racer-cmd "/target/release/racer") +;; +;; To activate racer in Rust buffers, run: +;; +;; (add-hook 'rust-mode-hook #'racer-mode) +;; +;; You can also use racer to find definition at point via +;; `racer-find-definition', bound to `M-.' by default. +;; +;; Finally, you can also use Racer to show the signature of the +;; current function in the minibuffer: +;; +;; (add-hook 'racer-mode-hook #'eldoc-mode) + +;;; Code: + +(require 'dash) +(require 'etags) +(require 'rust-mode) +(require 's) +(require 'f) +(require 'thingatpt) +(require 'button) +(require 'help-mode) + +(defgroup racer nil + "Code completion, goto-definition and docs browsing for Rust via racer." + :link '(url-link "https://github.com/racer-rust/emacs-racer/") + :group 'rust-mode) + +(defcustom racer-cmd + (or (executable-find "racer") + (f-expand "~/.cargo/bin/racer") + "/usr/local/bin/racer") + "Path to the racer binary." + :type 'file + :group 'racer) + +(defcustom racer-rust-src-path + (or + (getenv "RUST_SRC_PATH") + (when (executable-find "rustc") + (let* ((sysroot (s-trim-right + (shell-command-to-string + (format "%s --print sysroot" (executable-find "rustc"))))) + (src-path (f-join sysroot "lib/rustlib/src/rust/src"))) + (when (file-exists-p src-path) + src-path) + src-path)) + "/usr/local/src/rust/src") + + "Path to the rust source tree. +If nil, we will query $RUST_SRC_PATH at runtime. +If $RUST_SRC_PATH is not set, look for rust source in rustup's install directory." + :type 'file + :group 'racer) + +(defcustom racer-cargo-home + (or + (getenv "CARGO_HOME") + "~/.cargo") + "Path to your current cargo home. Usually `~/.cargo'. +If nil, we will query $CARGO_HOME at runtime." + :type 'file + :group 'racer) + +(defun racer--cargo-project-root () + "Find the root of the current Cargo project." + (let ((root (locate-dominating-file (or (buffer-file-name (buffer-base-buffer)) default-directory) + "Cargo.toml"))) + (and root (file-truename root)))) + +(defun racer--header (text) + "Helper function for adding text properties to TEXT." + (propertize text 'face 'racer-help-heading-face)) + +(defvar racer--prev-state nil) + +(defun racer-debug () + "Open a buffer describing the last racer command run. +Helps users find configuration issues, or file bugs on +racer or racer.el." + (interactive) + (unless racer--prev-state + (user-error "Must run a racer command before debugging")) + (let ((buf (get-buffer-create "*racer-debug*")) + (inhibit-read-only t)) + (with-current-buffer buf + (erase-buffer) + (setq buffer-read-only t) + (let* ((process-environment + (plist-get racer--prev-state :process-environment)) + (rust-src-path-used + (--first (s-prefix-p "RUST_SRC_PATH=" it) process-environment)) + (cargo-home-used + (--first (s-prefix-p "CARGO_HOME=" it) process-environment)) + (stdout (plist-get racer--prev-state :stdout)) + (stderr (plist-get racer--prev-state :stderr))) + (insert + ;; Summarise the actual command that we run. + (racer--header "The last racer command was:\n\n") + (format "$ cd %s\n" + (plist-get racer--prev-state :default-directory)) + (format "$ export %s\n" cargo-home-used) + (format "$ export %s\n" rust-src-path-used) + (format "$ %s %s\n\n" + (plist-get racer--prev-state :program) + (s-join " " (plist-get racer--prev-state :args))) + + ;; Describe the exit code and outputs. + (racer--header + (format "This command terminated with exit code %s.\n\n" + (plist-get racer--prev-state :exit-code))) + (if (s-blank? stdout) + (racer--header "No output on stdout.\n\n") + (format "%s\n\n%s\n\n" + (racer--header "stdout:") + (s-trim-right stdout))) + (if (s-blank? stderr) + (racer--header "No output on stderr.\n\n") + (format "%s\n\n%s\n\n" + (racer--header "stderr:") + (s-trim-right stderr))) + + ;; Give copy-paste instructions for reproducing any errors + ;; the user has seen. + (racer--header + (s-word-wrap 60 "The temporary file will have been deleted. You should be able to reproduce the same output from racer with the following command:\n\n")) + (format "$ %s %s %s %s\n\n" cargo-home-used rust-src-path-used + (plist-get racer--prev-state :program) + (s-join " " + (-drop-last 1 (plist-get racer--prev-state :args)))) + + ;; Tell the user what to do next if they have problems. + (racer--header "Please report bugs ") + (racer--url-button "on GitHub" "https://github.com/racer-rust/emacs-racer/issues/new") + (racer--header ".")))) + (switch-to-buffer buf) + (goto-char (point-min)))) + +(defun racer--call (command &rest args) + "Call racer command COMMAND with args ARGS. +Return stdout if COMMAND exits normally, otherwise show an +error." + (let ((rust-src-path (or racer-rust-src-path (getenv "RUST_SRC_PATH"))) + (cargo-home (or racer-cargo-home (getenv "CARGO_HOME")))) + (when (null rust-src-path) + (user-error "You need to set `racer-rust-src-path' or `RUST_SRC_PATH'")) + (unless (file-exists-p rust-src-path) + (user-error "No such directory: %s. Please set `racer-rust-src-path' or `RUST_SRC_PATH'" + rust-src-path)) + (let ((default-directory (or (racer--cargo-project-root) default-directory)) + (process-environment (append (list + (format "RUST_SRC_PATH=%s" (expand-file-name rust-src-path)) + (format "CARGO_HOME=%s" (expand-file-name cargo-home))) + process-environment))) + (-let [(exit-code stdout _stderr) + (racer--shell-command racer-cmd (cons command args))] + ;; Use `equal' instead of `zero' as exit-code can be a string + ;; "Aborted" if racer crashes. + (unless (equal 0 exit-code) + (user-error "%s exited with %s. `M-x racer-debug' for more info" + racer-cmd exit-code)) + stdout)))) + +(defmacro racer--with-temporary-file (path-sym &rest body) + "Create a temporary file, and bind its path to PATH-SYM. +Evaluate BODY, then delete the temporary file." + (declare (indent 1) (debug (symbolp body))) + `(let ((,path-sym (make-temp-file "racer"))) + (unwind-protect + (progn ,@body) + (delete-file ,path-sym)))) + +(defun racer--slurp (file) + "Return the contents of FILE as a string." + (with-temp-buffer + (insert-file-contents-literally file) + (buffer-string))) + +(defun racer--shell-command (program args) + "Execute PROGRAM with ARGS. +Return a list (exit-code stdout stderr)." + (racer--with-temporary-file tmp-file-for-stderr + (let (exit-code stdout stderr) + ;; Create a temporary buffer for `call-process` to write stdout + ;; into. + (with-temp-buffer + (setq exit-code + (apply #'call-process program nil + (list (current-buffer) tmp-file-for-stderr) + nil args)) + (setq stdout (buffer-string))) + (setq stderr (racer--slurp tmp-file-for-stderr)) + (setq racer--prev-state + (list + :program program + :args args + :exit-code exit-code + :stdout stdout + :stderr stderr + :default-directory default-directory + :process-environment process-environment)) + (list exit-code stdout stderr)))) + +(defun racer--call-at-point (command) + "Call racer command COMMAND at point of current buffer. +Return a list of all the lines returned by the command." + (racer--with-temporary-file tmp-file + (write-region nil nil tmp-file nil 'silent) + (s-lines + (s-trim-right + (racer--call command + (number-to-string (line-number-at-pos)) + (number-to-string (racer--current-column)) + (buffer-file-name (buffer-base-buffer)) + tmp-file))))) + +(defun racer--read-rust-string (string) + "Convert STRING, a rust string literal, to an elisp string." + (when string + (->> string + ;; Remove outer double quotes. + (s-chop-prefix "\"") + (s-chop-suffix "\"") + ;; Replace escaped characters. + (s-replace "\\n" "\n") + (s-replace "\\\"" "\"") + (s-replace "\\'" "'") + (s-replace "\\;" ";")))) + +(defun racer--split-parts (raw-output) + "Given RAW-OUTPUT from racer, split on semicolons and doublequotes. +Unescape strings as necessary." + (let ((parts nil) + (current "") + (i 0)) + (while (< i (length raw-output)) + (let ((char (elt raw-output i)) + (prev-char (and (> i 0) (elt raw-output (1- i))))) + (cond + ;; A semicolon that wasn't escaped, start a new part. + ((and (equal char ?\;) (not (equal prev-char ?\\))) + (push current parts) + (setq current "")) + (t + (setq current (concat current (string char)))))) + (setq i (1+ i))) + (push current parts) + (mapcar #'racer--read-rust-string (nreverse parts)))) + +(defun racer--split-snippet-match (line) + "Given LINE, a string \"MATCH ...\" from complete-with-snippet, +split it into its constituent parts." + (let* ((match-parts (racer--split-parts line)) + (docstring (nth 7 match-parts))) + (when (and match-parts (equal (length match-parts) 8)) + (list :name (s-chop-prefix "MATCH " (nth 0 match-parts)) + :line (string-to-number (nth 2 match-parts)) + :column (string-to-number (nth 3 match-parts)) + :path (nth 4 match-parts) + ;; Struct or Function: + :kind (nth 5 match-parts) + :signature (nth 6 match-parts) + :docstring (if (> (length docstring) 0) docstring nil))))) + +(defun racer--describe-at-point (name) + "Get a description of the symbol at point matching NAME. +If there are multiple possibilities with this NAME, prompt +the user to choose." + (let* ((output-lines (save-excursion + ;; Move to the end of the current symbol, to + ;; increase racer accuracy. + (skip-syntax-forward "w_") + (racer--call-at-point "complete-with-snippet"))) + (all-matches (--map (when (s-starts-with-p "MATCH " it) + (racer--split-snippet-match it)) + output-lines)) + (relevant-matches (--filter (equal (plist-get it :name) name) + all-matches))) + (if (> (length relevant-matches) 1) + ;; We might have multiple matches with the same name but + ;; different types. E.g. Vec::from. + (let ((signature + (completing-read "Multiple matches: " + (--map (plist-get it :signature) relevant-matches)))) + (--first (equal (plist-get it :signature) signature) relevant-matches)) + (-first-item relevant-matches)))) + +(defun racer--help-buf (contents) + "Create a *Racer Help* buffer with CONTENTS." + (let ((buf (get-buffer-create "*Racer Help*")) + ;; If the buffer already existed, we need to be able to + ;; override `buffer-read-only'. + (inhibit-read-only t)) + (with-current-buffer buf + (erase-buffer) + (insert contents) + (setq buffer-read-only t) + (goto-char (point-min)) + (racer-help-mode)) + buf)) + +(defface racer-help-heading-face + '((t :weight bold)) + "Face for markdown headings in *Racer Help* buffers.") + +(defun racer--url-p (target) + "Return t if TARGET looks like a fully qualified URL." + (not (null + (string-match-p (rx bol "http" (? "s") "://") target)))) + +(defun racer--propertize-links (markdown) + "Propertize links in MARKDOWN." + (replace-regexp-in-string + ;; Text of the form [foo](http://example.com) + (rx "[" (group (+? (not (any "]")))) "](" (group (+? anything)) ")") + ;; For every match: + (lambda (whole-match) + ;; Extract link and target. + (let ((link-text (match-string 1 whole-match)) + (link-target (match-string 2 whole-match))) + ;; If it's a web URL, use a clickable link. + (if (racer--url-p link-target) + (racer--url-button link-text link-target) + ;; Otherwise, just discard the target. + link-text))) + markdown)) + +(defun racer--propertize-all-inline-code (markdown) + "Given a single line MARKDOWN, replace all instances of `foo` or +\[`foo`\] with a propertized string." + (let ((highlight-group + (lambda (whole-match) + (racer--syntax-highlight (match-string 1 whole-match))))) + (->> markdown + (replace-regexp-in-string + (rx "[`" (group (+? anything)) "`]") + highlight-group) + (replace-regexp-in-string + (rx "`" (group (+? anything)) "`") + highlight-group)))) + +(defun racer--indent-block (str) + "Indent every line in STR." + (s-join "\n" (--map (concat " " it) (s-lines str)))) + +(defun racer--trim-newlines (str) + "Remove newlines from the start and end of STR." + (->> str + (s-chop-prefix "\n") + (s-chop-suffix "\n"))) + +(defun racer--remove-footnote-links (str) + "Remove footnote links from markdown STR." + (->> (s-lines str) + (--remove (string-match-p (rx bol "[`" (+? anything) "`]: ") it)) + (s-join "\n") + ;; Collapse consecutive blank lines caused by removing footnotes. + (s-replace "\n\n\n" "\n\n"))) + +(defun racer--docstring-sections (docstring) + "Split DOCSTRING into text, code and heading sections." + (let* ((sections nil) + (current-section-lines nil) + (section-type :text) + ;; Helper function. + (finish-current-section + (lambda () + (when current-section-lines + (let ((current-section + (s-join "\n" (nreverse current-section-lines)))) + (unless (s-blank? current-section) + (push (list section-type current-section) sections)) + (setq current-section-lines nil)))))) + (dolist (line (s-lines docstring)) + (cond + ;; If this is a closing ``` + ((and (s-starts-with-p "```" line) (eq section-type :code)) + (push line current-section-lines) + (funcall finish-current-section) + (setq section-type :text)) + ;; If this is an opening ``` + ((s-starts-with-p "```" line) + (funcall finish-current-section) + (push line current-section-lines) + (setq section-type :code)) + ;; Headings + ((and (not (eq section-type :code)) (s-starts-with-p "# " line)) + (funcall finish-current-section) + (push (list :heading line) sections)) + ;; Normal text. + (t + (push line current-section-lines)))) + (funcall finish-current-section) + (nreverse sections))) + +(defun racer--clean-code-section (section) + "Given a SECTION, a markdown code block, remove +fenced code delimiters and code annotations." + (->> (s-lines section) + (-drop 1) + (-drop-last 1) + ;; Ignore annotations like # #[allow(dead_code)] + (--remove (s-starts-with-p "# " it)) + (s-join "\n"))) + +(defun racer--propertize-docstring (docstring) + "Replace markdown syntax in DOCSTRING with text properties." + (let* ((sections (racer--docstring-sections docstring)) + (propertized-sections + (--map (-let [(section-type section) it] + ;; Remove trailing newlines, so we can ensure we + ;; have consistent blank lines between sections. + (racer--trim-newlines + (pcase section-type + (:text + (racer--propertize-all-inline-code + (racer--propertize-links + (racer--remove-footnote-links + section)))) + (:code + (racer--indent-block + (racer--syntax-highlight + (racer--clean-code-section section)))) + (:heading + (racer--header + (s-chop-prefix "# " section)))))) + sections))) + (s-join "\n\n" propertized-sections))) + +(defun racer--find-file (path line column) + "Open PATH and move point to LINE and COLUMN." + (find-file path) + (goto-char (point-min)) + (forward-line (1- line)) + (forward-char column)) + +(defun racer--button-go-to-src (button) + (racer--find-file + (button-get button 'path) + (button-get button 'line) + (button-get button 'column))) + +(define-button-type 'racer-src-button + 'action 'racer--button-go-to-src + 'follow-link t + 'help-echo "Go to definition") + +(defun racer--url-button (text url) + "Return a button that opens a browser at URL." + (with-temp-buffer + (insert-text-button + text + :type 'help-url + 'help-args (list url)) + (buffer-string))) + +(defun racer--src-button (path line column) + "Return a button that navigates to PATH at LINE number and +COLUMN number." + ;; Convert "/foo/bar/baz/foo.rs" to "baz/foo.rs" + (let* ((filename (f-filename path)) + (parent-dir (f-filename (f-parent path))) + (short-path (f-join parent-dir filename))) + (with-temp-buffer + (insert-text-button + short-path + :type 'racer-src-button + 'path path + 'line line + 'column column) + (buffer-string)))) + +(defun racer--kind-description (raw-kind) + "Human friendly description of a rust kind. +For example, 'EnumKind' -> 'an enum kind'." + (let* ((parts (s-split-words raw-kind)) + (description (s-join " " (--map (downcase it) parts))) + (a (if (string-match-p (rx bos (or "a" "e" "i" "o" "u")) description) + "an" "a"))) + (format "%s %s" a description))) + +(defun racer--describe (name) + "Return a *Racer Help* buffer for the function or type at point. +If there are multiple candidates at point, use NAME to find the +correct value." + (let ((description (racer--describe-at-point name))) + (when description + (let* ((name (plist-get description :name)) + (raw-docstring (plist-get description :docstring)) + (docstring (if raw-docstring + (racer--propertize-docstring raw-docstring) + "Not documented.")) + (kind (plist-get description :kind))) + (racer--help-buf + (format + "%s is %s defined in %s.\n\n%s%s" + name + (racer--kind-description kind) + (racer--src-button + (plist-get description :path) + (plist-get description :line) + (plist-get description :column)) + (if (equal kind "Module") + ;; No point showing the 'signature' of modules, which is + ;; just their full path. + "" + (format " %s\n\n" (racer--syntax-highlight (plist-get description :signature)))) + docstring)))))) + +(defun racer-describe () + "Show a *Racer Help* buffer for the function or type at point." + (interactive) + (let ((buf (racer--describe (thing-at-point 'symbol)))) + (if buf + (temp-buffer-window-show buf) + (user-error "No function or type found at point")))) + +(defvar racer-help-mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map (make-composed-keymap button-buffer-map + special-mode-map)) + map) + "Keymap for racer help mode.") + +(define-derived-mode racer-help-mode fundamental-mode + "Racer-Help" + "Major mode for *Racer Help* buffers. + +Commands: +\\{racer-help-mode-map}") + +(defcustom racer-complete-in-comments + nil + "If non-nil, query racer for completions inside comments too." + :type 'boolean + :group 'racer) + +(defun racer-complete-at-point () + "Complete the symbol at point." + (let* ((ppss (syntax-ppss)) + (in-string (nth 3 ppss)) + (in-comment (nth 4 ppss))) + (when (and + (not in-string) + (or (not in-comment) racer-complete-in-comments)) + (let* ((bounds (bounds-of-thing-at-point 'symbol)) + (beg (or (car bounds) (point))) + (end (or (cdr bounds) (point)))) + (list beg end + (completion-table-dynamic #'racer-complete) + :annotation-function #'racer-complete--annotation + :company-prefix-length (racer-complete--prefix-p beg end) + :company-docsig #'racer-complete--docsig + :company-doc-buffer #'racer--describe + :company-location #'racer-complete--location))))) + +(defun racer--file-and-parent (path) + "Convert /foo/bar/baz/q.txt to baz/q.txt." + (let ((file (f-filename path)) + (parent (f-filename (f-parent path)))) + (f-join parent file))) + +(defun racer-complete (&optional _ignore) + "Completion candidates at point." + (->> (racer--call-at-point "complete") + (--filter (s-starts-with? "MATCH" it)) + (--map (-let [(name line col file matchtype ctx) + (s-split-up-to "," (s-chop-prefix "MATCH " it) 5)] + (put-text-property 0 1 'line (string-to-number line) name) + (put-text-property 0 1 'col (string-to-number col) name) + (put-text-property 0 1 'file file name) + (put-text-property 0 1 'matchtype matchtype name) + (put-text-property 0 1 'ctx ctx name) + name)))) + +(defun racer--trim-up-to (needle s) + "Return content after the occurrence of NEEDLE in S." + (-if-let (idx (s-index-of needle s)) + (substring s (+ idx (length needle))) + s)) + +(defun racer-complete--prefix-p (beg _end) + "Return t if a completion should be triggered for a prefix between BEG and END." + (save-excursion + (goto-char beg) + ;; If we're at the beginning of the buffer, we can't look back 2 + ;; characters. + (ignore-errors + (looking-back "\\.\\|::" 2)))) + +(defun racer-complete--annotation (arg) + "Return an annotation for completion candidate ARG." + (let* ((ctx (get-text-property 0 'ctx arg)) + (type (get-text-property 0 'matchtype arg)) + (pretty-ctx + (pcase type + ("Module" + (if (string= arg ctx) + "" + (concat " " (racer--file-and-parent ctx)))) + ("StructField" + (concat " " ctx)) + (_ + (->> ctx + (racer--trim-up-to arg) + (s-chop-suffixes '(" {" "," ";"))))))) + (format "%s : %s" pretty-ctx type))) + +(defun racer-complete--docsig (arg) + "Return a signature for completion candidate ARG." + (racer--syntax-highlight (format "%s" (get-text-property 0 'ctx arg)))) + +(defun racer-complete--location (arg) + "Return location of completion candidate ARG." + (cons (get-text-property 0 'file arg) + (get-text-property 0 'line arg))) + +(defun racer--current-column () + "Get the current column based on underlying character representation." + (length (buffer-substring-no-properties + (line-beginning-position) (point)))) + +;;;###autoload +(defun racer-find-definition () + "Run the racer find-definition command and process the results." + (interactive) + (-if-let (match (--first (s-starts-with? "MATCH" it) + (racer--call-at-point "find-definition"))) + (-let [(_name line col file _matchtype _ctx) + (s-split-up-to "," (s-chop-prefix "MATCH " match) 5)] + (if (fboundp 'xref-push-marker-stack) + (xref-push-marker-stack) + (with-no-warnings + (ring-insert find-tag-marker-ring (point-marker)))) + (racer--find-file file (string-to-number line) (string-to-number col))) + (error "No definition found"))) + +(defun racer--syntax-highlight (str) + "Apply font-lock properties to a string STR of Rust code." + (let (result) + ;; Load all of STR in a rust-mode buffer, and use its + ;; highlighting. + (with-temp-buffer + (insert str) + (delay-mode-hooks (rust-mode)) + (if (fboundp 'font-lock-ensure) + (font-lock-ensure) + (with-no-warnings + (font-lock-fontify-buffer))) + (setq result (buffer-string))) + (when (and + ;; If we haven't applied any properties yet, + (null (text-properties-at 0 result)) + ;; and if it's a standalone symbol, then assume it's a + ;; variable. + (string-match-p (rx bos (+ (any lower "_")) eos) str)) + (setq result (propertize str 'face 'font-lock-variable-name-face))) + result)) + +(defun racer--goto-func-name () + "If point is inside a function call, move to the function name. + +foo(bar, |baz); -> foo|(bar, baz);" + (let ((last-paren-pos (nth 1 (syntax-ppss))) + (start-pos (point))) + (when last-paren-pos + ;; Move to just before the last paren. + (goto-char last-paren-pos) + ;; If we're inside a round paren, we're inside a function call. + (unless (looking-at "(") + ;; Otherwise, return to our start position, as point may have been on a + ;; function already: + ;; foo|(bar, baz); + (goto-char start-pos))))) + +(defun racer--relative (path &optional directory) + "Return PATH relative to DIRECTORY (`default-directory' by default). +If PATH is not in DIRECTORY, just abbreviate it." + (unless directory + (setq directory default-directory)) + (if (s-starts-with? directory path) + (concat "./" (f-relative path directory)) + (f-abbrev path))) + +(defun racer-eldoc () + "Show eldoc for context at point." + (save-excursion + (racer--goto-func-name) + ;; If there's a variable at point: + (-when-let* ((rust-sym (symbol-at-point)) + (comp-possibilities (racer-complete)) + (matching-possibility + (--find (string= it (symbol-name rust-sym)) comp-possibilities)) + (prototype (get-text-property 0 'ctx matching-possibility)) + (matchtype (get-text-property 0 'matchtype matching-possibility))) + (if (equal matchtype "Module") + (racer--relative prototype) + ;; Syntax highlight function signatures. + (racer--syntax-highlight prototype))))) + +(defvar racer-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "M-.") #'racer-find-definition) + (define-key map (kbd "M-,") #'pop-tag-mark) + map)) + +;;;###autoload +(define-minor-mode racer-mode + "Minor mode for racer." + :lighter " racer" + :keymap racer-mode-map + (setq-local eldoc-documentation-function #'racer-eldoc) + (set (make-local-variable 'completion-at-point-functions) nil) + (add-hook 'completion-at-point-functions #'racer-complete-at-point)) + +(define-obsolete-function-alias 'racer-turn-on-eldoc 'eldoc-mode) +(define-obsolete-function-alias 'racer-activate 'racer-mode) + +(provide 'racer) +;;; racer.el ends here diff --git a/test/racer-test.el b/test/racer-test.el new file mode 100644 index 0000000..67ed739 --- /dev/null +++ b/test/racer-test.el @@ -0,0 +1,340 @@ +(require 'racer) +(require 'ert) + +(ert-deftest racer--file-and-parent () + (should + (equal + (racer--file-and-parent "/foo/bar/baz/q.txt") + "baz/q.txt"))) + +(ert-deftest racer--goto-func-name () + (with-temp-buffer + ;; Insert a function call. + (insert "foo(bar, baz);") + ;; Move to the start of the second argument. + (goto-char (point-min)) + (while (not (looking-at "baz")) + (forward-char 1)) + ;; We should be at the end of foo. + (racer--goto-func-name) + (should (equal (point) 4)))) + +(ert-deftest racer--read-rust-string () + (should + (equal + (racer--read-rust-string "\"foo \\n \\\" \\' \\; bar") + "foo \n \" ' ; bar"))) + +(ert-deftest racer--help-buf () + (should + (bufferp + (racer--help-buf "foo bar.")))) + +(ert-deftest racer--propertize-all-inline-code () + (should + (equal-including-properties + (racer--propertize-all-inline-code "foo `bar` [`baz`] biz") + #("foo bar baz biz" 4 7 (face font-lock-variable-name-face) 8 11 (face font-lock-variable-name-face))))) + +(ert-deftest racer--propertize-docstring-code () + "Ensure we render code blocks with indents." + (should + (equal + (racer--propertize-docstring "foo + +```rust +func(); +``` + +bar. + +```text +1 +2 +``` +") + "foo + + func(); + +bar. + + 1 + 2"))) + +(defun racer--remove-properties (text) + "Remove all the properties on TEXT. +Tests that use `equal' ignore properties, but +this makes the ert failure descriptions clearer." + (with-temp-buffer + (insert text) + (buffer-substring-no-properties (point-min) (point-max)))) + +(ert-deftest racer--propertize-docstring-code-annotations () + "Ignore '# foo' lines in code sections in docstrings." + (should + (equal + (racer--remove-properties + (racer--propertize-docstring "``` +# #[allow(dead_code)] +#[derive(Debug)] +struct Foo {} +```")) + " #[derive(Debug)] + struct Foo {}"))) + +(ert-deftest racer--propertize-docstring-code-newlines () + "Ensure we always have a blank line before a code block." + + (should + (equal + (racer--remove-properties + (racer--propertize-docstring "``` +bar1(); +``` +foo +``` +bar2(); +```")) + " bar1(); + +foo + + bar2();"))) + +(ert-deftest racer--propertize-docstring-newlines () + "Ensure we still handle links that have been split over two lines." + (should + (equal + (racer--propertize-docstring "[foo\nbar](baz)") + "foo\nbar"))) + +(ert-deftest racer--propertize-docstring-link-after-attribute () + "We should not confuse attributes with links." + (should + (equal + (racer--remove-properties + (racer--propertize-docstring "Result is annotated with the #[must_use] attribute, +by the [`Write`](../../std/io/trait.Write.html) trait")) + "Result is annotated with the #[must_use] attribute, +by the Write trait"))) + +(ert-deftest racer--propertize-docstring-footnotes () + "Ensure we discard footnote links." + (should + (equal + (racer--remove-properties + (racer--propertize-docstring "foo [`str`] bar + +\[`str`]: ../../std/primitive.str.html + +baz.")) + "foo str bar + +baz."))) + +(ert-deftest racer--propertize-docstring-urls () + "Ensure we render buttons for links with urls." + (let ((result (racer--propertize-docstring "[foo](http://example.com)"))) + (should (equal result "foo")) + (should (equal (get-text-property 0 'button result) '(t)))) + (should + (equal-including-properties + (racer--propertize-docstring "[foo](#bar)") + "foo"))) + +(ert-deftest racer--propertize-docstring-heading () + "Ensure we render markdown headings correctly." + (should + (equal-including-properties + (racer--propertize-docstring "# foo") + #("foo" 0 3 (face racer-help-heading-face))))) + +(ert-deftest racer--split-parts () + "Ensure we correctly parse racer CSV." + (should + (equal (racer--split-parts "foo;bar") + '("foo" "bar"))) + (should + (equal (racer--split-parts "foo;\"bar\"") + '("foo" "bar"))) + (should + (equal (racer--split-parts "foo\\;bar;baz") + '("foo;bar" "baz")))) + +(ert-deftest racer--describe-at-point-name () + "Ensure we extract the correct name in `racer--describe-at-point'." + (cl-letf (((symbol-function 'racer--call) + (lambda (&rest _) + (s-join + "\n" + (list + "PREFIX 36,37,n" + "MATCH new;new();294;11;/home/user/src/rustc-1.10.0/src/libstd/../libcollections/vec.rs;Function;pub fn new() -> Vec;\"Constructs a new, empty `Vec`.\"" + "END"))))) + (should + (equal (plist-get (racer--describe-at-point "new") :name) + "new")))) + +(ert-deftest racer--describe-at-point-nil-docstring () + "If there's no docstring, racer--describe-at-point should use nil." + (cl-letf (((symbol-function 'racer--call) + (lambda (&rest _) + (s-join + "\n" + (list + "PREFIX 36,37,n" + "MATCH new;new();294;11;/home/user/src/rustc-1.10.0/src/libstd/../libcollections/vec.rs;Function;pub fn new() -> Vec;\"\"" + "END"))))) + (should + (null (plist-get (racer--describe-at-point "new") :docstring))))) + +(ert-deftest racer--describe-at-point-shortest () + "If there are multiple matches, we want the shortest. + +Since we've moved point to the end of symbol, the other functions just happen to have the same prefix." + (cl-letf (((symbol-function 'racer--call) + (lambda (&rest _) + (s-join + "\n" + (list + "PREFIX 36,37,n" + "MATCH new_bar;new_bar();294;11;/home/user/src/rustc-1.10.0/src/libstd/../libcollections/vec.rs;Function;pub fn new() -> Vec;\"\"" + "MATCH new;new();294;11;/home/user/src/rustc-1.10.0/src/libstd/../libcollections/vec.rs;Function;pub fn new() -> Vec;\"\"" + "MATCH new_foo;new_foo();294;11;/home/user/src/rustc-1.10.0/src/libstd/../libcollections/vec.rs;Function;pub fn new() -> Vec;\"\"" + "END"))))) + (should + (equal (plist-get (racer--describe-at-point "new") :name) + "new")))) + +(ert-deftest racer--syntax-highlight () + "Ensure we highlight code blocks and snippets correctly." + ;; Highlighting types should always use the type face. + (should + (equal-including-properties + (racer--syntax-highlight "Foo") + #("Foo" 0 3 (face font-lock-type-face)))) + ;; Highlighting keywords. + (should + (equal-including-properties + (racer--syntax-highlight "false") + #("false" 0 5 (face font-lock-keyword-face)))) + ;; Simple variables should be highlighted, even when standalone. + (should + (equal-including-properties + (racer--syntax-highlight "foo") + #("foo" 0 3 (face font-lock-variable-name-face))))) + +(ert-deftest racer-describe () + "Smoke test for `racer-describe'." + (cl-letf (((symbol-function 'racer--call) + (lambda (&rest _) + (s-join + "\n" + (list + "PREFIX 36,37,n" + "MATCH foo;foo();294;11;/home/user/src/rustc-1.10.0/src/libstd/../libcollections/vec.rs;Function;pub fn new() -> Vec;\"\"" + "END"))))) + (with-temp-buffer + (rust-mode) + (insert "foo();") + (goto-char (point-min)) + (racer-describe)))) + +(ert-deftest racer-describe-test-description () + "Ensure we write the correct text summary in the first line +of the racer describe buffer." + (cl-letf (((symbol-function 'racer--call) + (lambda (&rest _) + "PREFIX 8,10,Ok\nMATCH Ok;Ok;253;4;/home/user/src/rustc-1.10.0/src/libstd/../libcore/result.rs;EnumVariant;Ok(#[stable(feature = \"rust1\", since = \"1.0.0\")] T),;\"`Result` is a type that represents either success (`Ok`) or failure (`Err`).\n\nSee the [`std::result`](index.html) module documentation for details.\nEND\n"))) + (with-temp-buffer + (rust-mode) + (insert "Ok") + (goto-char (point-min)) + (switch-to-buffer (racer--describe "Ok")) + (let ((first-line (-first-item (s-lines (buffer-substring-no-properties + (point-min) (point-max)))))) + (should + (equal first-line + "Ok is an enum variant defined in libcore/result.rs.")))))) + +(ert-deftest racer-describe-module-description () + "Ensure we write the correct text summary in the first line +of the racer describe buffer." + (cl-letf (((symbol-function 'racer--call) + (lambda (&rest _) + "PREFIX 13,20,matches\nMATCH matches;matches;1;0;/home/user/.cargo/registry/src/github.com-1ecc6299db9ec823/matches-0.1.2/lib.rs;Module;/home/user/.cargo/registry/src/github.com-1ecc6299db9ec823/matches-0.1.2/lib.rs;\"\"\nEND\n"))) + (with-temp-buffer + (rust-mode) + (insert "extern crate matches;") + (goto-char (1- (point-max))) + (switch-to-buffer (racer--describe "matches")) + (should + (equal (racer--remove-properties (buffer-string)) + "matches is a module defined in matches-0.1.2/lib.rs. + +Not documented."))))) + +(ert-deftest racer-describe-uses-whole-symbol () + "Racer uses the symbol *before* point, so make sure we move point to +the end of the current symbol. + +Otherwise, if the point is at the start of the symbol, we don't find anything." + (let (point-during-call) + (cl-letf (((symbol-function 'racer--call) + (lambda (&rest _) + (setq point-during-call (point)) + (s-join + "\n" + (list + "PREFIX 36,37,n" + "MATCH foo;foo();294;11;/home/user/src/rustc-1.10.0/src/libstd/../libcollections/vec.rs;Function;pub fn new() -> Vec;\"\"" + "END"))))) + (with-temp-buffer + (rust-mode) + (insert "foo();") + (goto-char (point-min)) + ;; This should move point to the end of 'foo' before calling + ;; racer--call. + (racer-describe)) + (should (equal point-during-call 4))))) + +(ert-deftest racer-debug () + "Smoke test for `racer-debug'." + (let ((racer--prev-state + (list + :program "racer" + :args '("complete" "1" "2") + :exit-code 0 + :stdout "PREFIX 1,2,Ok\nMATCH FOO\nEND\n" + :stderr "" + :default-directory "/" + :process-environment + '("RUST_SRC_PATH=/home/user/src/rustc-1.10.0/src" + "CARGO_HOME=/home/user/.cargo")))) + (racer-debug))) + +(ert-deftest racer--relative () + ;; Common case: the path is relative to the directory. + (should (equal (racer--relative "/foo/bar" "/foo") + "./bar")) + ;; Path is not relative, but it's a home directory. + (should (equal (racer--relative (f-expand "~/foo") + (f-expand "~/bar")) + "~/foo")) + ;; Path is not relative and not a home directory. + (should (equal (racer--relative "/foo/bar" "/quux") + "/foo/bar"))) + +(ert-deftest racer-eldoc-no-completions () + "`racer-eldoc' should handle no completions gracefully." + (cl-letf (((symbol-function 'racer--call) + (lambda (&rest _) + "PREFIX 4,4,\nEND\n"))) + (with-temp-buffer + (rust-mode) + (insert "use ") + ;; Midle of the 'use'. + (goto-char 2) + ;; Should return nil without crashing. + (should (null (racer-eldoc)))))) diff --git a/test/test-helper.el b/test/test-helper.el new file mode 100644 index 0000000..89fcec8 --- /dev/null +++ b/test/test-helper.el @@ -0,0 +1,12 @@ +;;; test-helper --- Test helper for racer + +;;; Commentary: + +;;; Code: + +(require 'undercover) +(undercover "racer.el" + (:exclude "*-test.el") + (:report-file "/tmp/undercover-report.json")) + +;;; test-helper.el ends here