diff --git a/.travis.yml b/.travis.yml index ad66de3..fe24466 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,21 +1,26 @@ language: node_js node_js: - - "0.6" - - "0.8" - - "0.10" - - "0.12" - - "4" - - "5" - - "6" - - "node" + - 6 + - 8 + - 10 env: - - EVM_EMACS=emacs-24.1-travis - - EVM_EMACS=emacs-24.2-travis - - 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-25.2-travis + - EVM_EMACS=emacs-25.3-travis + - EVM_EMACS=emacs-26.1-travis + +matrix: + exclude: + - node_js: 6 + env: EVM_EMACS=emacs-25.1-travis + - node_js: 6 + env: EVM_EMACS=emacs-25.2-travis + - node_js: 6 + env: EVM_EMACS=emacs-25.3-travis + - node_js: 6 + env: EVM_EMACS=emacs-26.1-travis before_install: - curl -fsSkL https://gist.github.com/rejeep/ebcd57c3af83b049833b/raw > x.sh && source ./x.sh diff --git a/README.md b/README.md index ec06c29..626777d 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ Description This program is derived from comint-mode and provides the following features. - * token completion, same as Node.js REPL - * file name completion in string - * incremental history search - * sending JavaScript codes to REPL +* token completion, same as Node.js REPL +* file name completion in string +* incremental history search +* sending JavaScript codes to REPL Usage ----- @@ -19,21 +19,48 @@ Usage Put this file in your Emacs lisp path (e.g. ~/.emacs.d/site-lisp) and add the following line to your .emacs: - (require 'nodejs-repl) +```elisp +(require 'nodejs-repl) +``` Type `M-x nodejs-repl` to run Node.js REPL. See also `comint-mode` to check key bindings. You can define key bindings to send JavaScript codes to REPL like below: - (add-hook 'js-mode-hook - (lambda () - (define-key js-mode-map (kbd "C-x C-e") 'nodejs-repl-send-last-expression) - (define-key js-mode-map (kbd "C-c C-j") 'nodejs-repl-send-line) - (define-key js-mode-map (kbd "C-c C-r") 'nodejs-repl-send-region) - (define-key js-mode-map (kbd "C-c C-l") 'nodejs-repl-load-file) - (define-key js-mode-map (kbd "C-c C-z") 'nodejs-repl-switch-to-repl))) - +```elisp +(add-hook 'js-mode-hook + (lambda () + (define-key js-mode-map (kbd "C-x C-e") 'nodejs-repl-send-last-expression) + (define-key js-mode-map (kbd "C-c C-j") 'nodejs-repl-send-line) + (define-key js-mode-map (kbd "C-c C-r") 'nodejs-repl-send-region) + (define-key js-mode-map (kbd "C-c C-l") 'nodejs-repl-load-file) + (define-key js-mode-map (kbd "C-c C-z") 'nodejs-repl-switch-to-repl))) +``` + +When a version manager such as nvm is used to run different versions +of Node.js, it is often desirable to start the REPL of the version +specified in the .nvmrc file per project. In such case, customize the +`nodejs-repl-command` variable with a function symbol. That function +should query nvm for the Node.js command to run. For example: + +```elisp +(require 'nodejs-repl) +(defun nvm-which () + (let* ((shell (concat (getenv "SHELL") " -l -c 'nvm which'")) + (output (shell-command-to-string shell))) + (cadr (split-string output "[\n]+" t)))) +(setq nodejs-repl-command #'nvm-which) +``` + +The `nvm-which` function can be simpler, and perhaps can run faster, +too, if using Bash: + +```elisp +(defun nvm-which () + (let ((output (shell-command-to-string "source ~/.nvm/nvm.sh; nvm which"))) + (cadr (split-string output "[\n]+" t)))) +``` Author ------ @@ -44,7 +71,7 @@ Takeshi Arabiki (abicky) Copyright and License --------------------- -Copyright (C) 2012-2017 Takeshi Arabiki (abicky) +Copyright (C) 2012-2018 Takeshi Arabiki (abicky) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/nodejs-repl.el b/nodejs-repl.el index f111ac8..f7bac73 100644 --- a/nodejs-repl.el +++ b/nodejs-repl.el @@ -3,7 +3,7 @@ ;; Copyright (C) 2012-2017 Takeshi Arabiki ;; Author: Takeshi Arabiki -;; Version: 0.1.7 +;; Version: 0.2.0 ;; This program is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by @@ -44,6 +44,26 @@ ;; (define-key js-mode-map (kbd "C-c C-l") 'nodejs-repl-load-file) ;; (define-key js-mode-map (kbd "C-c C-z") 'nodejs-repl-switch-to-repl))) ;; +;; When a version manager such as nvm is used to run different versions +;; of Node.js, it is often desirable to start the REPL of the version +;; specified in the .nvmrc file per project. In such case, customize the +;; `nodejs-repl-command` variable with a function symbol. That function +;; should query nvm for the Node.js command to run. For example: +;; +;; (require 'nodejs-repl) +;; (defun nvm-which () +;; (let* ((shell (concat (getenv "SHELL") " -l -c 'nvm which'")) +;; (output (shell-command-to-string shell))) +;; (cadr (split-string output "[\n]+" t)))) +;; (setq nodejs-repl-command #'nvm-which) +;; +;; The `nvm-which` function can be simpler, and perhaps can run faster, +;; too, if using Bash: +;; +;; (defun nvm-which () +;; (let ((output (shell-command-to-string "source ~/.nvm/nvm.sh; nvm which"))) +;; (cadr (split-string output "[\n]+" t)))) +;; (require 'cc-mode) (require 'comint) @@ -53,11 +73,14 @@ "Run Node.js REPL and communicate the process." :group 'processes) -(defconst nodejs-repl-version "0.1.7" +(defconst nodejs-repl-version "0.2.0" "Node.js mode Version.") (defcustom nodejs-repl-command "node" - "Node.js command used in `nodejs-repl-mode'." + "Node.js command used in `nodejs-repl-mode'. If it is a symbol +of a function, the function is called for the path of the Node.js +command. This allows to integrate with a Node.js version manager +such as nvm." :group 'nodejs-repl :type 'string) @@ -106,18 +129,12 @@ See also `comint-process-echoes'" (defvar nodejs-repl-mode-map (let ((map (make-sparse-keymap))) - (define-key map (kbd "TAB") 'comint-dynamic-complete) + (define-key map (kbd "TAB") 'completion-at-point) (define-key map (kbd "C-c C-c") 'nodejs-repl-quit-or-cancel) map)) -;; process.stdout.columns should be set. -;; Node.js 0.8 and 0.10 uses this value as the maximum number of columns, -;; but process.stdout.columns in Emacs is infinity because Emacs returns 0 as winsize.ws_col. -;; The completion candidates won't be displayed if process.stdout.columns is infinity. -;; see also `handleGroup` function in readline.js (defvar nodejs-repl-code-format (concat - "process.stdout.columns = %d;" "require('repl').start('%s', null, null, true, false, " "require('repl')['REPL_MODE_' + '%s'.toUpperCase()])")) @@ -127,14 +144,7 @@ See also `comint-process-echoes'" ;;; if send string like "a; Ma\t", return a; Math\x1b[1G> a; Math\x1b[0K\x1b[10G (defvar nodejs-repl-prompt-re-format - (concat - "\x1b\\[1G" - "\\(" - "\x1b\\[0J%s.*\x1b\\[[0-9]+G.*" ; for Node.js 0.8 - "\\|" - "%s.*\x1b\\[0K\x1b\\[[0-9]+G.*" ; for Node.js 0.4 or 0.6 - "\\)" - "$")) + "\x1b\\[1G\x1b\\[0J%s.*\x1b\\[[0-9]+G.*$") (defvar nodejs-repl-prompt-re (format nodejs-repl-prompt-re-format nodejs-repl-prompt nodejs-repl-prompt)) ;;; not support Unicode characters @@ -153,8 +163,10 @@ See also `comint-process-echoes'" '(! + - void typeof delete)) (defvar nodejs-repl-cache-token "") -(defvar nodejs-repl-cache-candidates ()) +(defvar nodejs-repl-cache-completions ()) +(defvar nodejs-repl-get-completions-for-require-p nil) +(defvar nodejs-repl-prompt-deletion-required-p nil) ;;;-------------------------- ;;; Private functions @@ -207,7 +219,6 @@ See also `comint-process-echoes'" (not (let ((last-line (process-get proc 'last-line))) (or (string-match-p nodejs-repl-prompt-re last-line) - (string-match-p "^\x1b[[0-9]+D$" last-line) ; for Node.js 0.8 (string= last-line string))))) (process-put proc 'running-p nil) (accept-process-output proc interval))) @@ -221,13 +232,13 @@ when receive the output string" (goto-char (point-max)) (process-put proc 'last-line (buffer-substring (point-at-bol) (point))))) -(defun nodejs-repl--get-candidates-from-process (token) - "Get completion candidates sending TAB to Node.js process." +(defun nodejs-repl--get-completions-from-process (token) + "Get completions sending TAB to Node.js process." (let ((ret (if (version< nodejs-repl-nodejs-version "7.0.0") (nodejs-repl--send-string (concat token "\t")) (nodejs-repl--send-string (concat token "\t")) (nodejs-repl--send-string "\t"))) - candidates) + completions) (nodejs-repl-clear-line) (when (not (equal ret token)) (if (string-match-p "\n" ret) @@ -239,21 +250,21 @@ when receive the output string" ;; trim trailing whitespaces (setq ret (replace-regexp-in-string "[ \t\r\n]*\\'" "" ret)) ;; don't split by whitespaces because the prompt might have whitespaces!! - (setq candidates (split-string ret "\n")) + (setq completions (split-string ret "\n")) ;; remove the first element (input) and the last element (prompt) - (setq candidates (reverse (cdr (reverse (cdr candidates))))) + (setq completions (reverse (cdr (reverse (cdr completions))))) ;; split by whitespaces ;; '("encodeURI encodeURIComponent") -> '("encodeURI" "encodeURIComponent") - (setq candidates (split-string - (replace-regexp-in-string " *$" "" (mapconcat 'identity candidates " ")) + (setq completions (split-string + (replace-regexp-in-string " *$" "" (mapconcat 'identity completions " ")) "[ \t\r\n]+")) ) (setq ret (replace-regexp-in-string nodejs-repl-extra-espace-sequence-re "" ret)) (let ((candidate-token (nodejs-repl--get-last-token ret))) - (setq candidates (if (or (null candidate-token) (equal candidate-token token)) + (setq completions (if (or (null candidate-token) (equal candidate-token token)) nil (list candidate-token)))))) - candidates)) + completions)) (defun nodejs-repl--get-or-create-process () (let ((proc (get-process nodejs-repl-process-name))) @@ -276,7 +287,10 @@ when receive the output string" (defun nodejs-repl--clear-cache (string) "Clear caches when outputting the result." (setq nodejs-repl-cache-token "") - (setq nodejs-repl-cache-candidates ())) + (setq nodejs-repl-cache-completions ())) + +(defun nodejs-repl--set-prompt-deletion-required-p () + (setq nodejs-repl-prompt-deletion-required-p t)) (defun nodejs-repl--remove-duplicated-prompt (string) ;; `.load` command of Node.js repl outputs a duplicated prompt @@ -288,6 +302,18 @@ when receive the output string" (when (re-search-forward (concat nodejs-repl-prompt nodejs-repl-prompt) end t) (replace-match nodejs-repl-prompt))))) +(defun nodejs-repl--delete-prompt (string) + ;; A prompt will be inserted if window--adjust-process-windows is called + (when nodejs-repl-prompt-deletion-required-p + (setq nodejs-repl-prompt-deletion-required-p nil) + (let ((beg (or comint-last-output-start + (point-min-marker))) + (end (process-mark (get-buffer-process (current-buffer))))) + (save-excursion + (goto-char beg) + (when (re-search-forward nodejs-repl-prompt end t) + (replace-match "")))))) + ;; cf. https://www.ecma-international.org/ecma-262/#sec-ecmascript-language-expressions (defun nodejs-repl--beginning-of-expression () (search-backward-regexp "[[:graph:]]" nil t) @@ -344,6 +370,51 @@ when receive the output string" (t (error "No proper expression is found backward")))) +(defun nodejs-repl--completion-at-point-function () + (setq nodejs-repl-completion-at-point-called-p t) + (when (comint-after-pmark-p) + (let* ((input (buffer-substring (comint-line-beginning-position) (point))) + require-arg + token-length + file-completion-p) + (setq nodejs-repl-get-completions-for-require-p nil) ;; reset + (if (not (nodejs-repl--in-string-p)) + (setq token-length (length (nodejs-repl--get-last-token input))) + (setq require-arg (nodejs-repl--extract-require-argument input) + nodejs-repl-get-completions-for-require-p t) + (if (and require-arg + (or (= (length require-arg) 1) ; only quote or double quote + (not (string-match-p "[./]" (substring require-arg 1 2))))) ; not file path + (setq token-length (1- (length require-arg))) + (let ((quote-pos (save-excursion + (search-backward-regexp "['\"]" (point-at-bol) t) + (forward-char) + (point)))) + (when quote-pos + (setq file-completion-p t + token-length (- (point) quote-pos)))))) + (when token-length + (list + (save-excursion (backward-char token-length) (point)) + (point) + (if file-completion-p + #'completion-file-name-table + (completion-table-dynamic #'nodejs-repl--get-completions))))))) + +(defun nodejs-repl--get-completions (token) + (let (completions) + (when nodejs-repl-get-completions-for-require-p + (setq token (concat "require('" token))) + (if (and (not (equal nodejs-repl-cache-token "")) + (string-prefix-p nodejs-repl-cache-token token) + (not (string-match-p (concat "^" nodejs-repl-cache-token ".*?[.(/'\"]") token))) + (setq completions nodejs-repl-cache-completions) + (setq completions (nodejs-repl--get-completions-from-process token) + nodejs-repl-cache-token token + nodejs-repl-cache-completions completions)) + completions)) + + ;;;-------------------------- ;;; Public functions ;;;-------------------------- @@ -402,10 +473,6 @@ when receive the output string" (nodejs-repl-send-region (save-excursion (nodejs-repl--beginning-of-expression)) (point))) -;;;###autoload -(defun nodejs-repl-send-last-sexp () (interactive)) ;; Dummy definition for autoload -(define-obsolete-function-alias 'nodejs-repl-send-last-sexp 'nodejs-repl-send-last-expression) - ;;;###autoload (defun nodejs-repl-switch-to-repl () "If there is a `nodejs-repl-process' running switch to it, @@ -428,79 +495,42 @@ otherwise spawn one." (goto-char (point-max)) (delete-region (point-at-bol) (point))))) -(defun nodejs-repl-complete-from-process () - "Dynamically complete tokens at the point." - (when (comint-after-pmark-p) - (let* ((input (buffer-substring (comint-line-beginning-position) (point))) - require-arg - token - candidates - ret) - (if (nodejs-repl--in-string-p) - (progn - (setq require-arg (nodejs-repl--extract-require-argument input)) - (if (and require-arg - (or (= (length require-arg) 1) - (not (string-match-p "[./]" (substring require-arg 1 2))))) - (setq token (concat "require(" require-arg)) - (setq ret (comint-dynamic-complete-as-filename)))) - (setq token (nodejs-repl--get-last-token input))) - (when token - (setq candidates (nodejs-repl-get-candidates token)) - ;; TODO: write unit test - (setq token (replace-regexp-in-string "^require(['\"]" "" token)) - (setq ret (comint-dynamic-simple-complete token candidates))) - (if (eq ret 'sole) - (delete-char -1)) - ret))) - -(defun nodejs-repl-get-candidates (token) - "Get completion candidates." - (let (candidates) - (if (and (not (equal nodejs-repl-cache-token "")) - (string-match-p (concat "^" nodejs-repl-cache-token) token) - (not (string-match-p (concat "^" nodejs-repl-cache-token ".*?[.(/'\"]") token))) - (setq candidates nodejs-repl-cache-candidates) - (if (equal token "require(") ; a bug occurs when press TAB after "require(" in node 0.6 - (setq candidates nil) - (setq candidates (nodejs-repl--get-candidates-from-process token))) - (setq nodejs-repl-cache-token token) - (setq nodejs-repl-cache-candidates candidates)) - candidates)) - - (define-derived-mode nodejs-repl-mode comint-mode "Node.js REPL" "Major mode for Node.js REPL" :syntax-table nodejs-repl-mode-syntax-table (set (make-local-variable 'font-lock-defaults) '(nil nil t)) + (add-hook 'comint-output-filter-functions 'nodejs-repl--delete-prompt nil t) (add-hook 'comint-output-filter-functions 'nodejs-repl--remove-duplicated-prompt nil t) (add-hook 'comint-output-filter-functions 'nodejs-repl--filter-escape-sequnces nil t) (add-hook 'comint-output-filter-functions 'nodejs-repl--clear-cache nil t) (setq comint-input-ignoredups nodejs-repl-input-ignoredups) (setq comint-process-echoes nodejs-repl-process-echoes) - ;; delq seems to change global variables if called this phase - (set (make-local-variable 'comint-dynamic-complete-functions) - (delete 'comint-dynamic-complete-filename comint-dynamic-complete-functions)) - (add-hook 'comint-dynamic-complete-functions 'nodejs-repl-complete-from-process nil t) + (add-hook 'completion-at-point-functions 'nodejs-repl--completion-at-point-function nil t) + (make-local-variable 'window-configuration-change-hook) + (add-hook 'window-configuration-change-hook 'nodejs-repl--set-prompt-deletion-required-p) (ansi-color-for-comint-mode-on)) ;;;###autoload (defun nodejs-repl () "Run Node.js REPL." (interactive) - (setq nodejs-repl-prompt-re - (format nodejs-repl-prompt-re-format nodejs-repl-prompt nodejs-repl-prompt)) - (setq nodejs-repl-nodejs-version - ;; "v7.3.0" => "7.3.0", "v7.x-dev" => "7" - (replace-regexp-in-string nodejs-repl--nodejs-version-re "\\1" - (shell-command-to-string (concat nodejs-repl-command " --version")))) - (let* ((repl-mode (or (getenv "NODE_REPL_MODE") "magic")) - (nodejs-repl-code (format nodejs-repl-code-format - (window-width) nodejs-repl-prompt repl-mode ))) - (pop-to-buffer - (apply 'make-comint nodejs-repl-process-name nodejs-repl-command nil - `(,@nodejs-repl-arguments "-e" ,nodejs-repl-code))) - (nodejs-repl-mode))) + (let ((node-command (if (and (symbolp nodejs-repl-command) + (functionp nodejs-repl-command)) + (funcall nodejs-repl-command) + nodejs-repl-command))) + (setq nodejs-repl-prompt-re + (format nodejs-repl-prompt-re-format nodejs-repl-prompt nodejs-repl-prompt)) + (setq nodejs-repl-nodejs-version + ;; "v7.3.0" => "7.3.0", "v7.x-dev" => "7" + (replace-regexp-in-string nodejs-repl--nodejs-version-re "\\1" + (shell-command-to-string (concat node-command " --version")))) + (let* ((repl-mode (or (getenv "NODE_REPL_MODE") "magic")) + (nodejs-repl-code (format nodejs-repl-code-format + nodejs-repl-prompt repl-mode ))) + (pop-to-buffer + (apply 'make-comint nodejs-repl-process-name node-command nil + `(,@nodejs-repl-arguments "-e" ,nodejs-repl-code))) + (nodejs-repl-mode)))) (provide 'nodejs-repl) ;;; nodejs-repl.el ends here diff --git a/test/test.el b/test/test.el index 248fdd3..09eb713 100644 --- a/test/test.el +++ b/test/test.el @@ -46,31 +46,36 @@ ;; node 0.6.17 outputs "undefined" (string-match-p "a\n\nb\n\\(undefined\\)?" (buffer-string)))) - (desc "nodejs-repl-get-candidates") + (desc "nodejs-repl--get-completions") (expect '("Error") - (delete-dups (nodejs-repl-get-candidates "Err"))) + (delete-dups (nodejs-repl--get-completions "Err"))) (expect '("Error") - (delete-dups (nodejs-repl-get-candidates "Erro"))) + (delete-dups (nodejs-repl--get-completions "Erro"))) (expect "Err" ; use cache? nodejs-repl-cache-token) (expect '("Math.max" "Math.min") - (nodejs-repl-get-candidates "Math.m")) + (nodejs-repl--get-completions "Math.m")) ;; FIXME: this test is meaningless if window width is not set (expect '("encodeURI" "encodeURIComponent") - (delete-dups (nodejs-repl-get-candidates "encode"))) + (delete-dups (nodejs-repl--get-completions "encode"))) - (desc "nodejs-repl-get-candidates for require") + (desc "nodejs-repl--get-completions for require") (expect nil - (nodejs-repl-get-candidates "foo")) + (nodejs-repl--get-completions "foo")) (expect '("require") - (nodejs-repl-get-candidates "requi")) + (nodejs-repl--get-completions "requi")) (expect t - (nodejs-repl-get-candidates "require(") ; update cache - (> (length (nodejs-repl-get-candidates "require(\"")) 1)) - (expect "require(\"" ; update cache? + (> (length (let ((nodejs-repl-get-completions-for-require-p t)) + (nodejs-repl--get-completions ""))) 1)) + (expect "require('" ; update cache? nodejs-repl-cache-token) - (expect "require(\"npm/" ; update cache? - (nodejs-repl-get-candidates "require(\"npm/") + (expect "require('" ; use cache? + (let ((nodejs-repl-get-completions-for-require-p t)) + (nodejs-repl--get-completions "f")) + nodejs-repl-cache-token) + (expect "require('npm/" ; update cache? + (let ((nodejs-repl-get-completions-for-require-p t)) + (nodejs-repl--get-completions "npm/")) nodejs-repl-cache-token) (desc "nodejs-repl--extract-require-argument")