;;; tide.el --- Typescript Interactive Development Environment -*- lexical-binding: t -*- ;; Copyright (C) 2015 Anantha Kumaran. ;; Author: Anantha kumaran ;; URL: http://github.com/ananthakumaran/tide ;; Version: 4.5.4 ;; Keywords: typescript ;; Package-Requires: ((emacs "25.1") (dash "2.10.0") (s "1.11.0") (flycheck "27") (typescript-mode "0.1") (cl-lib "0.5")) ;; 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 ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program 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 this program. If not, see . ;; This file is not part of GNU Emacs. ;;; Commentary: ;;; Code: (require 'typescript-mode) (require 'etags) (require 'json) (require 'cl-lib) (require 'eldoc) (require 'dash) (require 's) (require 'flycheck) (require 'imenu) (require 'thingatpt) (require 'tide-lv) (require 'tabulated-list) (require 'xref) ;; Silence compiler warnings (defvar js2-basic-offset) (defvar js-indent-level) (defvar js3-indent-level) (defvar web-mode-code-indent-offset) (defvar sgml-basic-offset) (defvar company-backends) (declare-function company-grab "company.el" (regexp &optional expression limit)) (declare-function company-grab-symbol-cons "company.el" (idle-begin-after-re &optional max-len)) (declare-function company-begin-backend "company.el" (backend &optional callback)) (declare-function company-in-string-or-comment "company.el" nil) (defgroup tide nil "TypeScript Interactive Development Environment." :prefix "tide-" :group 'tools) (defcustom tide-sync-request-timeout 2 "The number of seconds to wait for a sync response." :type 'integer :group 'tide) (defcustom tide-tsserver-flags nil "List of additional flags to provide when starting tsserver." :type '(repeat string) :group 'tide) (defcustom tide-tsserver-process-environment '() "List of extra environment variables to use when starting tsserver." :type '(repeat string) :group 'tide) (defcustom tide-tsserver-executable nil "Name of tsserver executable to run instead of the bundled tsserver. This may either be an absolute path or a relative path. Relative paths are resolved against the project root directory. Note that this option only works with TypeScript version 2.0 and above." :type '(choice (const nil) string) :group 'tide) (defcustom tide-tscompiler-executable nil "Name of tsc executable. This may either be an absolute path or a relative path. Relative paths are resolved against the project root directory." :type '(choice (const nil) string) :group 'tide) (defcustom tide-node-executable "node" "Name or path of the node executable binary file." :type '(choice (const nil) string) :group 'tide) (defcustom tide-node-flags nil "List of flags to provide to node when starting tsserver. Useful for large TypeScript codebases which need to set max-old-space-size to a higher value." :type '(repeat string) :group 'tide) (defcustom tide-post-code-edit-hook nil "Hook run after code edits are applied in a buffer." :type 'hook :group 'tide) (defcustom tide-sort-completions-by-kind nil "Whether completions should be sorted by kind." :type 'boolean :group 'tide) (defcustom tide-format-options '() "Format options plist." :type '(plist :value-type sexp) :group 'tide) (defcustom tide-user-preferences '(:includeCompletionsForModuleExports t :includeCompletionsWithInsertText t :allowTextChangesInNewFiles t :generateReturnInDocTemplate t) "User preference plist used on the configure request. Check https://github.com/Microsoft/TypeScript/blob/17eaf50b/src/server/protocol.ts#L2684 for the full list of available options." :type '(plist :value-type sexp) :group 'tide) (defcustom tide-disable-suggestions nil "Disable suggestions. If set to non-nil, suggestions will not be shown in flycheck errors and tide-project-errors buffer." :type 'boolean :group 'tide) (defcustom tide-completion-setup-company-backend t "Add `company-tide' to `company-backends'." :type 'boolean :group 'tide) (defcustom tide-completion-ignore-case nil "CASE will be ignored in completion if set to non-nil." :type 'boolean :group 'tide) (defcustom tide-completion-show-source nil "Completion dropdown will contain completion source if set to non-nil." :type 'boolean :group 'tide) (defcustom tide-completion-fuzzy nil "Allow fuzzy completion. By default only candidates with exact prefix match are shown. If set to non-nil, candidates with match anywhere inside the name are shown." :type 'boolean :group 'tide) (defcustom tide-completion-detailed nil "Completion dropdown will contain detailed method information if set to non-nil." :type 'boolean :group 'tide) (defcustom tide-completion-enable-autoimport-suggestions t "Whether to include external module exports in completions." :type 'boolean :group 'tide) (defcustom tide-enable-xref t "Whether to enable xref integration." :type 'boolean :group 'tide) (defcustom tide-navto-item-filter #'tide-navto-item-filter-default "The filter for items returned by tide-nav. Defaults to class, interface, type, enum" :type 'function :group 'tide) (defface tide-file '((t (:inherit font-lock-type-face))) "Face for file names in references output." :group 'tide) (defface tide-line-number '((t (:inherit compilation-line-number))) "Face for line numbers in references output." :group 'tide) (defface tide-match '((t (:inherit match))) "Face for matched symbol in references output." :group 'tide) (defface tide-imenu-type-face '((t (:inherit font-lock-type-face))) "Face for type in imenu list." :group 'tide) (defface tide-choice-face '((t (:inherit font-lock-warning-face))) "Face for choices used in popup window." :group 'tide) (defcustom tide-jump-to-definition-reuse-window t "Reuse existing window when jumping to definition." :type 'boolean :group 'tide) (defcustom tide-imenu-flatten nil "Imenu index will be flattened if set to non-nil." :type 'boolean :group 'tide) (defcustom tide-allow-popup-select '(code-fix refactor) "The list of commands where popup selection is allowed." :type '(set (const code-fix) (const jump-to-implementation) (const refactor)) :group 'tide) (defcustom tide-always-show-documentation nil "Show the documentation window even if only type information is available." :type 'boolean :group 'tide) (defcustom tide-server-max-response-length 102400 "Maximum allowed response length from tsserver. Any response greater than this would be ignored." :type 'integer :group 'tide) (defcustom tide-tsserver-locator-function #'tide-tsserver-locater-npmlocal-projectile-npmglobal "Function used by tide to locate tsserver." :type 'function :group 'tide) (defcustom tide-project-cleanup-delay 60 "The number of idle seconds to wait before cleaning up unused tsservers. Use `nil` to disable automatic cleanups. See also `tide-do-cleanups`." :type '(choice (const nil) integer) :group 'tide) (defcustom tide-tsserver-start-method 'immediate "The method by which tide starts tsserver. `immediate' causes tide to start a tsserver instance as soon as `tide-mode' is turned on. `manual' means that tide will start a tsserver only when the user manually starts one." :type '(choice (const :tag "Start immediately." immediate) (const :tag "Require manual start." manual)) :group 'tide) (defcustom tide-default-mode "TS" "The default mode to open buffers not backed by files (e.g. Org source blocks) in." :type '(choice (const "TS") (const "TSX") (const "JS")(const "JSX")) :group 'tide) (defcustom tide-recenter-after-jump t "Recenter buffer after jumping to definition" :type 'boolean :group 'tide) (defcustom tide-jump-to-fallback #'tide-jump-to-fallback-not-given "The fallback jump function to use when implementations aren't available." :type 'function :group 'tide) (defcustom tide-filter-out-warning-completions nil "Completions whose `:kind' property is \"warning\" will be filtered out if set to non-nil. This option is useful for Javascript code completion, because tsserver often returns a lot of irrelevant completions whose `:kind' property is \"warning\" for Javascript code. You can fix this behavior by setting this variable to non-nil value for Javascript buffers using `setq-local' macro." :type 'boolean :group 'tide :safe #'booleanp) (defcustom tide-native-json-parsing (and (>= emacs-major-version 27) (functionp 'json-serialize) (functionp 'json-parse-buffer) (functionp 'json-parse-string)) "Use native JSON parsing (only emacs >= 27)." :type 'boolean :group 'tide) (defcustom tide-save-buffer-after-code-edit t "Save the buffer after applying code edits." :type 'boolean :group 'tide) (defconst tide--minimal-emacs "25.1" "This is the oldest version of Emacs that tide supports.") (defmacro tide-def-permanent-buffer-local (name &optional init-value) "Declare NAME as buffer local variable." `(progn (defvar ,name ,init-value) (make-variable-buffer-local ',name) (put ',name 'permanent-local t))) (defvar tide-supported-modes '(typescript-mode web-mode js-mode js2-mode js2-jsx-mode js3-mode rjsx-mode)) (defvar tide-server-buffer-name "*tide-server*") (defvar tide-request-counter 0) (defvar tide-project-configs (make-hash-table :test 'equal)) (defvar tide-max-response-length-error-message "Response length from tsserver is greater than maximum allowed response.") (tide-def-permanent-buffer-local tide-project-root nil) (tide-def-permanent-buffer-local tide-buffer-dirty nil) (tide-def-permanent-buffer-local tide-buffer-tmp-file nil) (tide-def-permanent-buffer-local tide-active-buffer-file-name nil) (tide-def-permanent-buffer-local tide-require-manual-setup nil) (defvar tide-servers (make-hash-table :test 'equal)) (defvar tide-response-callbacks (make-hash-table :test 'equal)) (defvar tide-source-root-directory (file-name-directory (or load-file-name buffer-file-name))) (defvar tide-tsserver-directory (expand-file-name "tsserver" tide-source-root-directory)) (defun tide-project-root () "Project root folder determined based on the presence of tsconfig.json." (or tide-project-root (let ((root (or (locate-dominating-file default-directory "tsconfig.json") (locate-dominating-file default-directory "jsconfig.json")))) (unless root (message (tide-join (list "Couldn't locate project root folder with" " a tsconfig.json or jsconfig.json file. Using '" default-directory "' as project root."))) (setq root default-directory)) (let ((full-path (expand-file-name root))) (setq tide-project-root full-path) full-path)))) (defun tide-project-name () (let ((full-path (directory-file-name (tide-project-root)))) (concat (file-name-nondirectory full-path) "-" (substring (md5 full-path) 0 10)))) (defun tide-buffer-file-name () "Returns the path to either the currently open file or the current buffer's parent. This is needed to support indirect buffers, as they don't set `buffer-file-name' correctly." (buffer-file-name (or (and (bound-and-true-p edit-indirect--overlay) (overlay-buffer edit-indirect--overlay)) (and (bound-and-true-p org-src--overlay) (overlay-buffer org-src--overlay)) ;; Needed for org-mode 8.x compatibility (and (bound-and-true-p org-edit-src-overlay) (overlay-buffer org-edit-src-overlay)) (buffer-base-buffer)))) ;;; Compatibility (defvar tide-tsserver-unsupported-commands (make-hash-table :test 'equal)) (defun tide-mark-as-unsupported (command) (puthash (tide-project-name) (cl-pushnew command (gethash (tide-project-name) tide-tsserver-unsupported-commands '())) tide-tsserver-unsupported-commands)) (defun tide-unsupported-p (command) (member command (gethash (tide-project-name) tide-tsserver-unsupported-commands '()))) (defmacro tide-fallback-if-not-supported (new-command new old cb) `(if (tide-unsupported-p ,new-command) (,old ,cb) (,new (lambda (response) (if (tide-command-unknown-p response) (progn (tide-mark-as-unsupported ,new-command) (,old ,cb)) (funcall ,cb response)))))) (defun tide--emacs-at-least (version) "Return t if Emacs is at least at version VERSION. Return nil, otherwise." (not (version< emacs-version version))) ;;; Helpers (defun tide-safe-json-read-file (filename) (condition-case nil (if tide-native-json-parsing (with-temp-buffer (insert-file-contents filename) (goto-char (point-min)) (json-parse-buffer :object-type 'plist :null-object json-null :false-object json-false)) (let ((json-object-type 'plist)) (json-read-file filename))) (error '()))) (defun tide-safe-json-read-string (string) (condition-case nil (if tide-native-json-parsing (json-parse-string string :object-type 'plist :null-object json-null :false-object json-false) (let ((json-object-type 'plist)) (json-read-from-string string))) (error '()))) (defun tide-json-read-object () (if tide-native-json-parsing (json-parse-buffer :object-type 'plist :null-object json-null :false-object json-false :array-type 'list) (let ((json-object-type 'plist) (json-array-type 'list)) (json-read-object)))) (defun tide-json-encode (obj) "Encode OBJ into a JSON string. JSON arrays must be represented with vectors." (if tide-native-json-parsing (json-serialize obj :null-object json-null :false-object json-false) (json-encode obj))) (defun tide-plist-get (list &rest args) (cl-reduce (lambda (object key) (when object (plist-get object key))) args :initial-value list)) (eval-and-compile (defun tide-plist-map (fn plist) (when (and plist (not (cl-evenp (length plist)))) (error "Invalid plist %S" plist)) (-map (-lambda ((key value)) (funcall fn key value)) (-partition 2 plist)))) (defun tide-combine-plists (&rest plists) "Create a single property list from all plists in PLISTS. The process starts by copying the first list, and then setting properties from the other lists. Settings in the last list are the most significant ones and overrule settings in the other lists." (let ((rtn (copy-sequence (pop plists))) p v ls) (while plists (setq ls (pop plists)) (while ls (setq p (pop ls) v (pop ls)) (setq rtn (plist-put rtn p v)))) rtn)) (defun tide-get-file-buffer (file &optional new-file) "Returns a buffer associated with a file. This will return the current buffer if it matches `file'. This way we can support temporary and indirect buffers." (cond ((equal file (tide-buffer-file-name)) (current-buffer)) ((file-exists-p file) (find-file-noselect file)) (new-file (let ((buffer (create-file-buffer file))) (with-current-buffer buffer (set-visited-file-name file) (basic-save-buffer) (display-buffer buffer t)) buffer)) (t (error "Invalid file %S" file)))) (defun tide-response-success-p (response) (and response (equal (plist-get response :success) t))) (defun tide-command-unknown-p (response) (and response (string-equal (plist-get response :command) "unknown"))) (defun tide-tsserver-version-not-supported () (error "Only tsserver 2.0 or greater is supported. Upgrade your tsserver or use older version of tide.")) (defun tide-tsserver-feature-not-supported (min-version) (error "tsserver %S or greater is required for this feature." min-version)) (defmacro tide-on-response-success (response &optional options &rest body) "BODY must be a single expression if OPTIONS is not passed." (declare (indent 2)) (unless body (setq body `(,options)) (setq options '())) (tide-plist-map (lambda (key _value) (unless (member key '(:ignore-empty :min-version)) (error "Invalid options %S" options))) options) (let ((ignore-empty-response (plist-get options :ignore-empty)) (min-version (plist-get options :min-version))) `(cond ((and ,min-version (tide-command-unknown-p ,response)) (tide-tsserver-feature-not-supported ,min-version)) ((tide-response-success-p ,response) (progn ,@body)) (t (-when-let (msg (plist-get ,response :message)) (unless (and ,ignore-empty-response (string-equal msg "No content available.")) (message "%s" msg)) nil))))) (defmacro tide-on-response-success-callback (response &optional options &rest body) (declare (indent 2)) `(lambda (,response) (tide-on-response-success ,response ,options ,@body))) (defun tide-join (list) (mapconcat 'identity list "")) (defun tide-each-buffer (project-name fn) "Callback FN for each buffer within PROJECT-NAME with tide-mode enabled." (-each (buffer-list) (lambda (buffer) (with-current-buffer buffer (when (and (bound-and-true-p tide-mode) (equal (tide-project-name) project-name)) (funcall fn)))))) (defun tide-first-buffer (project-name fn) "Callback FN for the first buffer within PROJECT-NAME with tide-mode enabled." (-when-let (buffer (-first (lambda (buffer) (with-current-buffer buffer (and (bound-and-true-p tide-mode) (equal (tide-project-name) project-name)))) (buffer-list))) (with-current-buffer buffer (funcall fn)))) (defun tide-line-number-at-pos (&optional pos) (let ((p (or pos (point)))) (if (= (point-min) 1) (line-number-at-pos p) (save-excursion (save-restriction (widen) (line-number-at-pos p)))))) (defun tide-current-offset () "Number of characters present from the begining of line to cursor in current line. Offset is one based." (1+ (- (point) (line-beginning-position)))) (defun tide-offset (pos) (save-excursion (save-restriction (widen) (goto-char pos) (tide-current-offset)))) (defun tide-span-to-position (span) (save-excursion (save-restriction (widen) (goto-char (point-min)) (forward-line (1- (plist-get span :line))) (beginning-of-line) (forward-char (1- (plist-get span :offset))) (point)))) (defun tide-file-span-first-line-text (file-span &optional string) (let (end-offset text) (save-excursion (with-current-buffer (tide-get-file-buffer (plist-get file-span :file)) (tide-move-to-location (plist-get file-span :start)) (when string (search-forward string (tide-location-to-point (plist-get file-span :end))) (setq end-offset (current-column))) (setq text (substring-no-properties (replace-regexp-in-string "\n" "" (thing-at-point 'line)))) (when string (put-text-property (- end-offset (length string)) end-offset 'face 'tide-match text)) text)))) (defun tide-display-help-buffer (feature body) (let ((buffer (tide-make-help-buffer feature body))) (display-buffer buffer t) (if help-window-select (progn (pop-to-buffer buffer) (message "Type \"q\" to restore previous buffer")) (message (concat "Type \"q\" in the " feature " buffer to close it"))))) (defun tide-make-help-buffer (feature body) (with-current-buffer (get-buffer-create (concat "*tide-" feature "*")) (setq buffer-read-only t) (let ((inhibit-read-only t)) (erase-buffer) (when body (save-excursion (tide-insert body)))) (local-set-key (kbd "q") #'quit-window) (current-buffer))) (defvar tide-alphabets '(?a ?s ?d ?f ?j ?k ?l)) (defun tide-popup-select-item (prompt list) (let ((hints (-map-indexed (lambda (i item) (concat (propertize (char-to-string (nth i tide-alphabets)) 'face 'tide-choice-face) " " item)) list))) (unwind-protect (progn (tide-lv-message (mapconcat 'identity hints "\n")) (let ((selected (read-char-choice prompt (-take (length list) tide-alphabets)))) (nth (-find-index (lambda (char) (eql selected char)) tide-alphabets) list))) (tide-lv-delete-window)))) (defun tide-completing-read-select-item (prompt list) (completing-read prompt list nil t)) (defun tide-can-use-popup-p (feature) (member feature tide-allow-popup-select)) (defun tide-select-item-from-list (prompt list label-fn allow-popup) (let ((collection (make-hash-table :test 'equal))) (dolist (item list) (puthash (funcall label-fn item) item collection)) (let ((selected-text (if (and (<= (length list) (length tide-alphabets)) allow-popup) (tide-popup-select-item prompt (hash-table-keys collection)) (tide-completing-read-select-item prompt (hash-table-keys collection))))) (gethash selected-text collection)))) (defun tide-command-to-string (program args) (with-temp-buffer (apply #'process-file (-concat (list program nil t nil) args)) (buffer-string))) ;;; Events (defvar tide-event-listeners (make-hash-table :test 'equal)) (defun tide-set-event-listener (listener) (puthash (tide-project-name) (cons (current-buffer) listener) tide-event-listeners)) (defun tide-clear-event-listener () (remhash (tide-project-name) tide-event-listeners)) ;;; Server (defun tide-current-server () (gethash (tide-project-name) tide-servers)) (defun tide-next-request-id () (number-to-string (cl-incf tide-request-counter))) (defun tide-dispatch-response (response) (let* ((request-id (plist-get response :request_seq)) (callback (gethash request-id tide-response-callbacks))) (when callback (let ((buffer (car callback))) (when (buffer-live-p buffer) (with-current-buffer buffer (apply (cdr callback) (list response))))) (remhash request-id tide-response-callbacks)))) (defun tide-dispatch-event (event) (-when-let (listener (gethash (tide-project-name) tide-event-listeners)) (with-current-buffer (car listener) (apply (cdr listener) (list event))))) (defun tide-dispatch (response) (cl-case (intern (plist-get response :type)) ((response) (tide-dispatch-response response)) ((event) (tide-dispatch-event response)))) (defun tide-send-command (name args &optional callback) (unless (tide-current-server) (error "Server does not exist. Run M-x tide-restart-server to start it again")) (tide-sync-buffer-contents) (let* ((request-id (tide-next-request-id)) (command `(:command ,name :seq ,request-id :arguments ,args)) (json-encoding-pretty-print nil) (encoded-command (tide-json-encode command)) (payload (concat encoded-command "\n"))) (process-send-string (tide-current-server) payload) (when callback (puthash request-id (cons (current-buffer) callback) tide-response-callbacks)))) (defun tide-seconds-elapsed-since (time) (time-to-seconds (time-subtract (current-time) time))) (defun tide-send-command-sync (name args) (let* ((start-time (current-time)) (response nil)) (tide-send-command name args (lambda (resp) (setq response resp))) (while (not response) (accept-process-output nil 0.01) (when (> (tide-seconds-elapsed-since start-time) tide-sync-request-timeout) (error "Sync request timed out %s" name))) response)) (defun tide-net-filter (process data) (with-current-buffer (process-buffer process) (goto-char (point-max)) (insert data)) (tide-decode-response process)) (defun tide-net-sentinel (process message) (let ((project-name (process-get process 'project-name))) (message "(%s) tsserver exits: %s." project-name (string-trim message)) (ignore-errors (kill-buffer (process-buffer process))) (tide-cleanup-project project-name))) (defun tide--npm-local () "Return a single path to the package-local typescript package directory or nil." (-when-let (packages-folder (locate-dominating-file default-directory "package.json")) (concat packages-folder "node_modules/typescript/lib/"))) (defun tide--npm-global () "Return a single path to the global typescript package directory or nil." (if (eq system-type 'windows-nt) (concat (getenv "appdata") "\\npm\\node_modules") "/usr/lib/node_modules/typescript/lib/")) (defun tide--npm-global-usrlocal () "Return a single path to the global typescript package directory or nil." ;; this check does not apply to windows. (if (eq system-type 'windows-nt) nil "/usr/local/lib/node_modules/typescript/lib/")) (defun tide--project-package () "Return the package's node_module bin directory using projectile's project root or nil." (when (and (functionp 'projectile-project-p) (functionp 'projectile-project-root) (projectile-project-p)) (concat (projectile-project-root) "node_modules/typescript/lib"))) (defconst tide--tscompiler "tsc.js" "File-name of the typescript compiler.") (defun tide--locate-tscompiler (path) "Locate the typescript compiler in PATH. Return a string representing the existing full path or nil." (let ((exe (expand-file-name tide--tscompiler path))) (when (file-exists-p exe) exe))) (defun tide-tscompiler-locater-npmlocal-projectile-npmglobal () "Locate tsserver through project-local or global system settings." (or (tide--locate-tscompiler (tide--npm-local)) (tide--locate-tscompiler (tide--project-package)) (tide--locate-tscompiler (tide--npm-global)) (tide--locate-tscompiler (tide--npm-global-usrlocal)))) (defun tide-locate-tscompiler-executable () "Locate the typescript compiler executable. If TIDE-TSCOMPILER-EXECUTABLE is set by the user use it. Otherwise check in the npm local package directory, in the project root as defined by projectile, and in the npm global installation." (or (and tide-tscompiler-executable (expand-file-name tide-tscompiler-executable)) (tide-tscompiler-locater-npmlocal-projectile-npmglobal))) (defconst tide--tsserver "tsserver.js" "File-name of the file that node executes to start the typescript server.") (defun tide--locate-tsserver (path) "Locate the typescript server in PATH. Return a string representing the existing full path or nil." (let ((exe (expand-file-name tide--tsserver path))) (when (file-exists-p exe) exe))) (defun tide-tsserver-locater-npmlocal-projectile-npmglobal () "Locate tsserver through project-local or global system settings." (or (tide--locate-tsserver (tide--npm-local)) (tide--locate-tsserver (tide--project-package)) (tide--locate-tsserver (tide--npm-global)) (tide--locate-tsserver (tide--npm-global-usrlocal)))) (defun tide-locate-tsserver-executable () "Locate the typescript server executable. If TIDE-TSSERVER-EXECUTABLE is set by the user use it. Otherwise check in the npm local package directory, in the project root as defined by projectile, and in the npm global installation. If nothing is found use the bundled version." (or (and tide-tsserver-executable (expand-file-name tide-tsserver-executable)) (funcall tide-tsserver-locator-function) (expand-file-name tide--tsserver tide-tsserver-directory))) (defun tide-start-server () (when (tide-current-server) (error "Server already exist")) (message "(%s) Starting tsserver..." (tide-project-name)) (let* ((default-directory (tide-project-root)) (process-environment (append tide-tsserver-process-environment process-environment)) (buf (generate-new-buffer tide-server-buffer-name)) (tsserverjs (tide-locate-tsserver-executable)) ;; Use a pipe to communicate with the subprocess. This fixes a hang ;; when a >1k message is sent on macOS. (process-connection-type nil) (node-process-arguments (append tide-node-flags (list tsserverjs) tide-tsserver-flags)) (process (apply #'start-file-process "tsserver" buf tide-node-executable node-process-arguments))) (set-process-coding-system process 'utf-8-unix 'utf-8-unix) (set-process-filter process #'tide-net-filter) (set-process-sentinel process #'tide-net-sentinel) (set-process-query-on-exit-flag process nil) (with-current-buffer (process-buffer process) (buffer-disable-undo)) (process-put process 'project-name (tide-project-name)) (process-put process 'project-root default-directory) (puthash (tide-project-name) process tide-servers) (message "(%s) tsserver server started successfully." (tide-project-name)) (tide-each-buffer (tide-project-name) #'tide-configure-buffer))) (defun tide-cleanup-buffer-callbacks () (let ((error-response `(:success ,nil))) (maphash (lambda (id callback) (when (equal (current-buffer) (car callback)) (funcall (cdr callback) error-response) (remhash id tide-response-callbacks))) tide-response-callbacks))) (defun tide-cleanup-project (project-name) (tide-each-buffer project-name (lambda () (tide-cleanup-buffer-callbacks))) (tide-cleanup-project-data project-name)) (defun tide-cleanup-project-data (project-name) (remhash project-name tide-servers) (remhash project-name tide-tsserver-unsupported-commands) (remhash project-name tide-project-configs)) (defvar tide--cleanup-timer nil) (defvar tide--cleanup-kinds nil) (defun tide-schedule-cleanup (kind) (cl-pushnew kind tide--cleanup-kinds) (when (and tide-project-cleanup-delay (not tide--cleanup-timer)) (setq tide--cleanup-timer (run-with-idle-timer tide-project-cleanup-delay nil #'tide-do-cleanups)))) (defun tide-schedule-dead-projects-cleanup () (tide-schedule-cleanup 'dead-projects)) (defun tide-schedule-tmp-file-cleanup () (tide-schedule-cleanup 'tmp-file)) (defun tide-do-cleanups (&optional interactivep) "Perform some cleanups. This function is used when Emacs is idle (see `tide-project-cleanup-delay'), and you can also call it interactively (e.g., if you disable the automatic behavior). Currently, two kinds of cleanups are done: * Remove projects that don't have any associated live buffers, and kill their tsserver processes. * Remove tmp files for buffers that are saved." (interactive '(t)) (let ((kinds (if interactivep '(dead-projects tmp-file) tide--cleanup-kinds)) (live-projects '()) (buffers-w/tmp '())) (dolist (b (buffer-list)) (with-current-buffer b (when (bound-and-true-p tide-mode) (cl-pushnew (tide-project-name) live-projects)) (when (bound-and-true-p tide-buffer-tmp-file) (cl-pushnew b buffers-w/tmp)))) (setq tide--cleanup-timer nil tide--cleanup-kinds nil) (when (memq 'dead-projects kinds) (-when-let (to-kill (-difference (hash-table-keys tide-servers) live-projects)) (message "Cleaning up %d projects..." (length to-kill)) (dolist (proj to-kill) (delete-process (process-buffer (gethash proj tide-servers))) (tide-cleanup-project-data proj)))) (when (and (consp buffers-w/tmp) (memq 'tmp-file kinds)) (message "Cleaning up %d tmp files..." (length buffers-w/tmp)) (dolist (b buffers-w/tmp) (with-current-buffer b (tide-remove-tmp-file)))) (unless interactivep (sit-for 5) (message nil)))) (defun tide-start-server-if-nonexistent () "Start a tsserver instance if there is not one already running." (unless (tide-current-server) (tide-start-server))) (defun tide-decode-response-legth () (goto-char (point-min)) (when (re-search-forward "Content-Length: \\([0-9]+\\)" nil t) (string-to-number (match-string 1)))) (defun tide-enough-response-p (length) (save-excursion (when (search-forward "{" nil t) (backward-char 1) (>= (- (position-bytes (point-max)) (position-bytes (point))) (1- length))))) (defun tide-decode-response (process) (with-current-buffer (process-buffer process) (let ((length (tide-decode-response-legth))) (when (and length (tide-enough-response-p length)) (search-forward "{") (backward-char 1) (let ((response (if (> length tide-server-max-response-length) (let ((seq (when (re-search-forward "\"request_seq\":\"\\([0-9]+\\)\"" nil t) (match-string 1)))) (forward-line 1) (and seq `(:success :json-false :type "response" :message ,tide-max-response-length-error-message :request_seq ,seq))) (tide-json-read-object)))) (delete-region (point-min) (point)) (when response (tide-dispatch response))) (when (>= (buffer-size) 16) (tide-decode-response process)))))) ;;; Initialization (defun tide-file-format-options () (tide-combine-plists `(:tabSize ,tab-width :indentSize ,(tide-current-indentsize)) tide-format-options (tide-tsfmt-options))) (defun tide-tsfmt-options () (let ((config-file (expand-file-name "tsfmt.json" (tide-project-root)))) (when (file-exists-p config-file) (tide-safe-json-read-file config-file)))) (defun tide-current-indentsize () (pcase major-mode (`typescript-mode typescript-indent-level) (`js2-mode js2-basic-offset) (`js-mode js-indent-level) (`js3-mode js3-indent-level) (`web-mode web-mode-code-indent-offset) (`js2-jsx-mode sgml-basic-offset) (`rjsx-mode sgml-basic-offset) (_ standard-indent))) (defun tide-command:configure () (tide-send-command "configure" `(:hostInfo ,(emacs-version) :file ,(tide-buffer-file-name) :formatOptions ,(tide-file-format-options) :preferences ,tide-user-preferences))) (defun tide-command:projectInfo (cb &optional need-file-name-list file) (tide-send-command "projectInfo" `(:file ,(or file (tide-buffer-file-name)) :needFileNameList ,need-file-name-list) cb)) (defun tide-command:openfile () (tide-send-command "open" (if tide-require-manual-setup `(:file ,(tide-buffer-file-name) :scriptKindName ,tide-default-mode :fileContent ,(buffer-string)) (append `(:file ,(tide-buffer-file-name)) (let ((extension (upcase (or (file-name-extension (tide-buffer-file-name)) "")))) (when (member extension '("TS" "JS" "TSX" "JSX")) `(:scriptKindName ,extension))))))) (defun tide-command:closefile () (tide-send-command "close" `(:file ,(tide-buffer-file-name)))) ;;; Jump to definition (defun tide-command:definition (&optional cb location) (let ((location (or location `(:file ,(tide-buffer-file-name) :line ,(tide-line-number-at-pos) :offset ,(tide-current-offset))))) (if cb (tide-send-command "definition" location cb) (tide-send-command-sync "definition" location)))) (defun tide-command:typeDefinition (cb) (tide-send-command "typeDefinition" `(:file ,(tide-buffer-file-name) :line ,(tide-line-number-at-pos) :offset ,(tide-current-offset)) cb)) (defun tide-jump-to-definition (&optional arg) "Jump to the definition of the symbol at point. If pointed at an abstract member-declaration, will proceed to look for implementations. When invoked with a prefix arg, jump to the type definition." (interactive "P") (let ((cb (lambda (response) (tide-on-response-success response (-when-let (filespan (car (plist-get response :body))) ;; if we're still at the same location... ;; maybe we're a abstract member which has implementations? (if (and (not arg) (tide-filespan-is-current-location-p filespan)) (tide-jump-to-implementation) (tide-jump-to-filespan filespan tide-jump-to-definition-reuse-window))))))) (if arg (tide-command:typeDefinition cb) (tide-command:definition cb)))) (defun tide-filespan-is-current-location-p (filespan) (let* ((location (plist-get filespan :start)) (new-file-name (plist-get filespan :file))) (and (string-equal new-file-name (tide-buffer-file-name)) (equal (tide-location-to-point location) (point))))) (defun tide-move-to-location (location) (let* ((line (plist-get location :line)) (offset (plist-get location :offset))) (save-restriction (widen) (goto-char (point-min)) (forward-line (1- line))) (unless (and (= offset 0) (= line 0)) (forward-char (1- offset))))) (defun tide-location-to-point (location) (save-excursion (tide-move-to-location location) (point))) (defun tide-recenter-p (filespan &optional recenter-pref) (when recenter-pref (let ((new-file-name (plist-get filespan :file))) (if (string-equal new-file-name (tide-buffer-file-name)) (tide-recenter-in-same-buffer-p filespan) t)))) (defun tide-recenter-in-same-buffer-p (filespan) (let* ((newpos (plist-get (plist-get filespan :start) :line)) (line-diff (abs (- (line-number-at-pos) newpos)))) (> line-diff (window-body-height)))) (defun tide-jump-to-filespan (filespan &optional reuse-window no-marker) (let ((file (plist-get filespan :file)) (should-recenter-p (tide-recenter-p filespan tide-recenter-after-jump))) (unless no-marker (xref-push-marker-stack)) (if reuse-window (pop-to-buffer (tide-get-file-buffer file) '((display-buffer-reuse-window display-buffer-same-window))) (pop-to-buffer (tide-get-file-buffer file))) (tide-move-to-location (plist-get filespan :start)) (when should-recenter-p (recenter)))) (defalias 'tide-jump-back 'pop-tag-mark) ;;; Jump to implementation (defun tide-jump-to-fallback-not-given () (message "No implementations available.")) (defun tide-command:implementation () (tide-send-command-sync "implementation" `(:file ,(tide-buffer-file-name) :line ,(tide-line-number-at-pos) :offset ,(tide-current-offset)))) (defun tide-jump-to-implementation-format-item (item) (let* ((file-name (plist-get item :file)) (line (tide-file-span-first-line-text item)) (file-pos (concat (propertize (file-name-nondirectory file-name) 'face 'tide-file) ":" (propertize (number-to-string (tide-plist-get item :start :line)) 'face 'tide-line-number)))) (concat line " " file-pos))) (defun tide-jump-to-implementation () "Jump to a implementation of the symbol at point." (interactive) (let ((response (tide-command:implementation))) (tide-on-response-success response (let ((impls (plist-get response :body))) (cl-case (length impls) ((0) (funcall tide-jump-to-fallback)) ((1) (tide-jump-to-filespan (car impls))) (t (tide-jump-to-filespan (tide-select-item-from-list "Select implementation: " impls #'tide-jump-to-implementation-format-item (tide-can-use-popup-p 'jump-to-implementation))))))))) ;;; Navigate to named member (defun tide-in-string-p () (nth 3 (syntax-ppss))) (defun tide-get-symbol-at-point () "Returns the symbol found at point, if not deemed 'noise'. Noise can be anything like braces, reserved keywords, etc." (unless (or (tide-in-string-p) (member (face-at-point) '(font-lock-keyword-face))) ;; we could have used symbol-at-point here, but that leaves us unable to ;; differentiate between a symbol named nil and no symbol at all. ;; thing-at-point returns a string OR nil, which means we don't get this problem. (let ((symbol (thing-at-point 'symbol))) (substring-no-properties (if (equal nil symbol) "" symbol))))) (defun tide-nav (arg) "Search and navigate to named types." (interactive "P") (let ((completion-ignore-case t) (last-completions nil) (default (when arg (tide-get-symbol-at-point)))) (-when-let (completion (completing-read-default "Search: " (completion-table-dynamic (lambda (prefix) (let ((response (tide-command:navto prefix))) (tide-on-response-success response (-when-let (navto-items (plist-get response :body)) (setq navto-items (funcall tide-navto-item-filter navto-items)) (setq last-completions navto-items) (-map (lambda (navto-item) (plist-get navto-item :name)) navto-items))))) t) nil t default)) (let ((navto-item (-find (lambda (navto-item) (string-equal completion (plist-get navto-item :name))) last-completions))) (tide-jump-to-filespan navto-item))))) (defun tide-navto-item-filter-default (navto-items) (-filter (lambda (navto-item) (member (plist-get navto-item :kind) '("class" "interface" "type" "enum"))) navto-items)) (defun tide-command:navto (type &optional current-file-only) (tide-send-command-sync "navto" `(:file ,(tide-buffer-file-name) :searchValue ,type :maxResultCount 100 :currentFileOnly ,(or current-file-only :json-false)))) ;;; Eldoc (defun tide-annotate-display-part (display-part &optional highlight) (let ((text (plist-get display-part :text)) (face (pcase (plist-get display-part :kind) ("aliasName" 'font-lock-type-face) ("className" 'font-lock-type-face) ("enumName" 'font-lock-type-face) ("fieldName" nil) ("interfaceName" 'font-lock-type-face) ("keyword" 'font-lock-keyword-face) ("lineBreak" nil) ("numericLiteral" nil) ("stringLiteral" 'font-lock-string-face) ("localName" 'font-lock-variable-name-face) ("methodName" nil) ("moduleName" nil) ("operator" nil) ("parameterName" (and highlight 'eldoc-highlight-function-argument)) ("propertyName" nil) ("punctuation" nil) ("space" nil) ("text" nil) ("typeParameterName" 'font-lock-variable-name-face) ("enumMemberName" 'font-lock-constant-face) ("functionName" 'font-lock-function-name-face) ("regularExpressionLiteral" 'font-lock-string-face)))) (if face (propertize text 'face face) text))) (defun tide-annotate-display-parts (display-parts &optional highlight) (tide-join (-map (lambda (part) (tide-annotate-display-part part highlight)) display-parts))) (defun tide-annotate-signature-parameter (parameter highlight) (tide-join (-map (lambda (part) (tide-annotate-display-part part highlight)) (plist-get parameter :displayParts)))) (defun tide-annotate-signature (signature selected-arg-index) (let ((separator (tide-join (-map #'tide-annotate-display-part (plist-get signature :separatorDisplayParts))))) (tide-join (-concat (-map #'tide-annotate-display-part (plist-get signature :prefixDisplayParts)) (list (mapconcat #'identity (-map-indexed (lambda (i parameter) (tide-annotate-signature-parameter parameter (eq i selected-arg-index))) (plist-get signature :parameters)) separator)) (-map #'tide-annotate-display-part (plist-get signature :suffixDisplayParts)))))) (defun tide-annotate-signatures (body) (let ((selected-index (plist-get body :selectedItemIndex)) (selected-arg-index (plist-get body :argumentIndex))) (tide-annotate-signature (nth selected-index (plist-get body :items)) selected-arg-index))) (defun tide-command:signatureHelp (cb) (tide-send-command "signatureHelp" `(:file ,(tide-buffer-file-name) :line ,(tide-line-number-at-pos) :offset ,(tide-current-offset)) (tide-on-response-success-callback response (:ignore-empty t) (funcall cb (tide-annotate-signatures (plist-get response :body)))))) (defun tide-method-call-p () (or (looking-at "[(,]") (and (not (looking-at "\\sw")) (looking-back "[(,]\n?\\s-*" nil)))) (defun tide-doc-text (quickinfo-or-completion-detail) (or (plist-get quickinfo-or-completion-detail :displayString) ;; old (tide-annotate-display-parts (plist-get quickinfo-or-completion-detail :displayParts)))) (defun tide-doc-documentation (quickinfo-or-completion-detail) (let ((documentation (plist-get quickinfo-or-completion-detail :documentation))) (if (stringp documentation) ;; old documentation (tide-annotate-display-parts documentation)))) (defun tide-format-jsdoc (name text) (setq text (s-trim (or text ""))) (concat (propertize (concat "@" name) 'face 'font-lock-keyword-face) (if (s-contains? "\n" text) "\n" " ") text "\n")) (defun tide-doc-jsdoc (quickinfo-or-completion-detail) (tide-join (-map (lambda (tag) (tide-format-jsdoc (plist-get tag :name) (plist-get tag :text))) (plist-get quickinfo-or-completion-detail :tags)))) (defun tide-construct-documentation (quickinfo-or-completion-detail) (when quickinfo-or-completion-detail (let* ((display-string (tide-doc-text quickinfo-or-completion-detail)) (documentation (tide-doc-documentation quickinfo-or-completion-detail)) (jsdoc (tide-doc-jsdoc quickinfo-or-completion-detail))) (when (or (or (not (s-blank? documentation)) (not (s-blank? jsdoc))) tide-always-show-documentation) (tide-join (-concat (list display-string "\n\n") (if (not (s-blank? documentation)) (list documentation "\n\n") '()) (list jsdoc))))))) (defun tide-command:quickinfo-old (cb) (tide-send-command "quickinfo" `(:file ,(tide-buffer-file-name) :line ,(tide-line-number-at-pos) :offset ,(tide-current-offset)) cb)) (defun tide-command:quickinfo-full (cb) (tide-send-command "quickinfo-full" `(:file ,(tide-buffer-file-name) :line ,(tide-line-number-at-pos) :offset ,(tide-current-offset)) cb)) (defun tide-command:quickinfo (cb) (tide-fallback-if-not-supported "quickinfo-full" tide-command:quickinfo-full tide-command:quickinfo-old cb)) (defun tide-eldoc-function () (unless (member last-command '(next-error previous-error)) (if (tide-method-call-p) (tide-command:signatureHelp #'tide-eldoc-maybe-show) (when (looking-at "\\s_\\|\\sw") (tide-command:quickinfo (tide-on-response-success-callback response (:ignore-empty t) (tide-eldoc-maybe-show (tide-doc-text (plist-get response :body)))))))) nil) (defun tide-eldoc-display-message-p() (if (fboundp 'eldoc-display-message-no-interference-p) (eldoc-display-message-no-interference-p) (eldoc-display-message-p))) ;;; Copied from eldoc.el (defun tide-eldoc-maybe-show (text) (with-demoted-errors "eldoc error: %s" (and (or (tide-eldoc-display-message-p) ;; Erase the last message if we won't display a new one. (when eldoc-last-message (eldoc-message nil) nil)) (eldoc-message text)))) (defun tide-documentation-at-point () "Show documentation of the symbol at point." (interactive) (tide-command:quickinfo (tide-on-response-success-callback response (-if-let (body (tide-construct-documentation (plist-get response :body))) (tide-display-help-buffer "documentation" body) (message "No documentation available."))))) ;;; Buffer Sync (defun tide-remove-tmp-file () (when tide-buffer-tmp-file (delete-file tide-buffer-tmp-file) (setq tide-buffer-tmp-file nil))) (defun tide-command:reloadfile () (tide-send-command "reload" `(:file ,(tide-buffer-file-name) :tmpfile ,(tide-buffer-file-name)))) (defun tide-handle-change (_beg _end _len) (setq tide-buffer-dirty t)) (defun tide-sync-buffer-contents () ;; The real file that backs a buffer could be changed in various ;; ways, one common example is the rename operation. Ensure that we ;; send the open command for the new file before using it as an ;; argument for any other command. (unless (string-equal tide-active-buffer-file-name (tide-buffer-file-name)) (tide-configure-buffer)) (when (and tide-buffer-tmp-file (not (buffer-modified-p))) (tide-schedule-tmp-file-cleanup)) (when tide-buffer-dirty (setq tide-buffer-dirty nil) (unless tide-buffer-tmp-file (setq tide-buffer-tmp-file (make-temp-file "tide"))) (save-restriction (widen) (write-region (point-min) (point-max) tide-buffer-tmp-file nil 'no-message)) (tide-send-command "reload" `(:file ,(tide-buffer-file-name) :tmpfile ,tide-buffer-tmp-file)))) ;;; Code-fixes (defun tide-apply-code-edits (file-code-edits) (save-excursion (dolist (file-code-edit file-code-edits) (let ((file (plist-get file-code-edit :fileName))) (with-current-buffer (tide-get-file-buffer file t) (tide-format-regions (tide-apply-edits (plist-get file-code-edit :textChanges))) ;; Saving won't work if the current buffer is temporary or an indirect ;; buffer (when (and tide-save-buffer-after-code-edit (equal buffer-file-name file)) (basic-save-buffer)) (run-hooks 'tide-post-code-edit-hook)))))) (defun tide-get-flycheck-errors-ids-at-point () (-map #'flycheck-error-id (flycheck-overlay-errors-at (point)))) (defun tide-command:getCodeFixes () (tide-send-command-sync "getCodeFixes" `(:file ,(tide-buffer-file-name) :startLine ,(tide-line-number-at-pos) :startOffset ,(tide-current-offset) :endLine ,(tide-line-number-at-pos) :endOffset ,(+ 1 (tide-current-offset)) :errorCodes ,(vconcat (tide-get-flycheck-errors-ids-at-point))))) (defun tide-command:getCombinedCodeFix (fixId) (tide-send-command-sync "getCombinedCodeFix" `(:scope (:type "file" :args (:file ,(tide-buffer-file-name))) :fixId ,fixId))) (defun tide-get-fix-description (fix) (plist-get fix :description)) (defun tide-apply-codefix (fix) "Apply a single `FIX', which may apply to several files." (tide-apply-code-edits (plist-get fix :changes))) (defun tide-apply-codefix-for-all-in-file (fix) (tide-apply-codefix fix) (-when-let* ((fix-id (plist-get fix :fixId)) (response (tide-command:getCombinedCodeFix fix-id))) (tide-on-response-success response (tide-apply-codefix (plist-get response :body))))) (defun tide-apply-codefixes (fixes codefix-fn) (cond ((= 0 (length fixes)) (message "No code-fixes available.")) ((= 1 (length fixes)) (funcall codefix-fn (car fixes))) (t (funcall codefix-fn (tide-select-item-from-list "Select fix: " fixes #'tide-get-fix-description (tide-can-use-popup-p 'code-fix)))))) (defun tide-code-fix (codefix-fn) (unless (tide-get-flycheck-errors-ids-at-point) (error "No errors available at current point.")) (let ((response (tide-command:getCodeFixes))) (tide-on-response-success response (let ((fixes (plist-get response :body))) (tide-apply-codefixes fixes codefix-fn))))) (defun tide-fix (&optional arg) "Apply code fix for the error at point. When invoked with a prefix arg, apply code fix for all the errors in the file that are similar to the error at point." (interactive "P") (if arg (tide-code-fix #'tide-apply-codefix-for-all-in-file) (tide-code-fix #'tide-apply-codefix))) ;;; Organize Imports (defun tide-command:organizeImports () (tide-send-command-sync "organizeImports" `(:scope (:type "file" :args (:file ,(tide-buffer-file-name)))))) (defun tide-organize-imports () "Organize imports in the file." (interactive) (let ((response (tide-command:organizeImports))) (tide-on-response-success response (:min-version "2.8") (-when-let (changes (plist-get response :body)) (tide-apply-code-edits changes))))) ;;; Refactor (defun tide-location-or-range () (if (use-region-p) (let ((start (region-beginning)) (end (region-end))) `(:startLine ,(tide-line-number-at-pos start) :startOffset ,(tide-offset start) :endLine ,(tide-line-number-at-pos end) :endOffset ,(tide-offset end))) `(:line ,(tide-line-number-at-pos) :offset ,(tide-current-offset)))) (defun tide-command:getEditsForRefactor (refactor action) (tide-send-command-sync "getEditsForRefactor" (append `(:refactor ,refactor :action ,action :file ,(tide-buffer-file-name)) (tide-location-or-range)))) (defun tide-command:getApplicableRefactors () (tide-send-command-sync "getApplicableRefactors" (append `(:file ,(tide-buffer-file-name)) (tide-location-or-range)))) (defun tide-get-refactor-description (refactor) (plist-get refactor :description)) (defun tide-select-refactor (applicable-refactor-infos) (let ((available-refactors (-mapcat (lambda (applicable-refactor-info) (-map (lambda (refactor-action-info) `(:action ,(plist-get refactor-action-info :name) :refactor ,(plist-get applicable-refactor-info :name) :inlineable ,(plist-get applicable-refactor-info :inlineable) :description ,(plist-get refactor-action-info :description))) (plist-get applicable-refactor-info :actions))) applicable-refactor-infos))) (tide-select-item-from-list "Select refactor: " available-refactors #'tide-get-refactor-description (tide-can-use-popup-p 'refactor)))) (defun tide-apply-refactor (selected) (let ((response (tide-command:getEditsForRefactor (plist-get selected :refactor) (plist-get selected :action)))) (tide-on-response-success response (progn (deactivate-mark) (tide-apply-code-edits (tide-plist-get response :body :edits)) (-when-let (rename-location (tide-plist-get response :body :renameLocation)) (with-current-buffer (tide-get-file-buffer (tide-plist-get response :body :renameFilename)) (tide-move-to-location rename-location) (when (tide-can-rename-symbol-p) (tide-rename-symbol)))))))) (defun tide-refactor () "Refactor code at point or current region" (interactive) (let ((response (tide-command:getApplicableRefactors))) (tide-on-response-success response (:min-version "2.4") (-if-let (body (plist-get response :body)) (tide-apply-refactor (tide-select-refactor body)) (message "No refactors available."))))) ;;; Disable tslint warnings (defconst tide-tslint-disable-next-line-regexp "\\s *//\\s *tslint\\s *:\\s *disable-next-line\\s *:\\(.*\\)" "Regexp matching a tslint flag disabling rules on the next line.") (defun tide-add-tslint-disable-next-line () "Add a tslint flag to disable rules generating errors at point. This function adds or modifies a flag of this form to the previous line: // tslint:disable-next-line:[rule1] [rule2] [...] The line will be indented according to the current indentation settings. This function generates rule1, rule2 to cover all the errors present at point. If the previous line does not already contain a disable-next-line flag, a new line is added to hold the new flag. If the previous line already contains a disable-next-line flag, the rule is added to the flag. Note that this function does not preserve the formatting of the already existing flag. The resulting flag will always be formatted as described above." (interactive) (let ((error-ids (delq nil (tide-get-flycheck-errors-ids-at-point))) (start (point))) (when error-ids (save-excursion (if (and (eq 0 (forward-line -1)) (looking-at tide-tslint-disable-next-line-regexp)) ;; We'll update the old flag. (let ((old-list (split-string (match-string 1)))) (delete-region (point) (point-at-eol)) (setq error-ids (append old-list error-ids))) ;; We'll create a new flag. (goto-char start) (beginning-of-line) (open-line 1)) (insert "// tslint:disable-next-line:" (string-join error-ids " ")) (typescript-indent-line))))) ;;; Disable Eslint Warnings (defconst tide-eslint-disable-next-line-regexp "\\s *//\\s *eslint-disable-next-line\\s *\\(.*\\)") (defun tide-add-eslint-disable-next-line () "Add a eslint flag to disable rules generating errors at point. This function adds or modifies a flag of this form to the previous line: // eslint-disable-next-line [rule1], [rule2], [...] The line will be indented according to the current indentation settings. This function generates rule1, rule2 to cover all the errors present at point. If the previous line does not already contain a disable-next-line flag, a new line is added to hold the new flag. If the previous line already contains a disable-next-line flag, the rule is added to the flag. Note that this function does not preserve the formatting of the already existing flag. The resulting flag will always be formatted as described above." (interactive) (let ((error-ids (delq nil (tide-get-flycheck-errors-ids-at-point))) (start (point))) (when error-ids (save-excursion (if (and (eq 0 (forward-line -1)) (looking-at tide-eslint-disable-next-line-regexp)) ;; We'll update the old flag. (let ((old-list (split-string (match-string 1) "\,[\s]+"))) (delete-region (point) (point-at-eol)) (setq error-ids (append old-list error-ids))) ;; We'll create a new flag. (goto-char start) (beginning-of-line) (open-line 1)) (insert "// eslint-disable-next-line " (string-join error-ids ", ")) (typescript-indent-line))))) ;;; Auto completion (defun tide-completion-annotation (name) (-if-let (meta (and tide-completion-detailed (tide-completion-meta name))) ;; Get everything before the first newline, if any, because company-mode ;; wants single-line annotations. (car (split-string meta "\n")) (if tide-completion-show-source (tide-completion-append-source (tide-completion-annotation-trans-mark name) name nil) (tide-completion-annotation-trans-mark name)))) (defun tide-completion-annotation-trans-mark (name) (pcase (plist-get (get-text-property 0 'completion name) :kind) ("keyword" " k") ("module" " M") ("class" " C") ("interface" " I") ("type" " T") ("enum" " E") ("var" " v") ("local var" " v") ("function" " ƒ") ("local function" " ƒ") ("method" " m") ("getter" " m") ("setter" " m") ("property" " p") ("constructor" " c") ("call" " m") ("index" " i") ("construct" " m") ("parameter" " p") ("type parameter" " T") ("primitive type" " T") ("label" " l") ("alias" " A") ("const" " c") ("let" " l") (_ nil))) (defun tide-completion-kind (name) (pcase (plist-get (get-text-property 0 'completion name) :kind) ((or "primitive type" "keyword") 'keyword) ((or "const" "let" "var" "local var" "alias" "parameter") 'variable) ((or "property" "getter" "setter") 'field) ((or "function" "local function") 'function) ((or "method" "construct" "call" "index") 'method) ("enum" 'enum) ("enum member" 'enum-member) ((or "module" "external module name") 'module) ((or "class" "type") 'class) ("interface" 'interface) ("warning" 'text) ("script" 'file) ("directory" 'folder) ("string" 'constant) (_ 'property))) (defun tide-completion-rank (completion) "Get the sorting order of a COMPLETION candidate." (or (-elem-index (plist-get completion :kind) '("parameter" "local function" "local var" "let" "var" "const" "function" "class" "method" "getter" "setter" )) 100)) (defun tide-compose-comparators (cmp1 cmp2) "A helper function that composes two comparators CMP1 and CMP2." (lambda (a b) (cond ((funcall cmp1 a b) t) ((funcall cmp1 b a) nil) (t (funcall cmp2 a b))))) (defun tide-compare-completions-basic (completion-a completion-b) "Compare COMPLETION-A and COMPLETION-B based on their `sortText' property. This function is used for the basic completions sorting." (let ((sort-text-a (plist-get completion-a :sortText)) (sort-text-b (plist-get completion-b :sortText))) (string< sort-text-a sort-text-b))) (defun tide-compare-completions-by-kind (completion-a completion-b) "Compare COMPLETION-A and COMPLETION-B based on their kind." (let ((modifier-a (plist-get completion-a :kindModifiers)) (modifier-b (plist-get completion-b :kindModifiers))) (if (string-equal modifier-a modifier-b) (< (tide-completion-rank completion-a) (tide-completion-rank completion-b)) ;; Rank declarations lower than variables (string-equal modifier-b "declare")))) (defun tide-completion-prefix () (if (and (tide-in-string-p) (looking-back (rx (or (and "import" (1+ space) (or ?\" ?') (0+ (not (any ?\" ?')))) (and "from" (1+ space) (or ?\" ?') (0+ (not (any ?\" ?')))) (and "import(" (or ?\" ?') (0+ (not (any ?\" ?')))) (and "require(" (or ?\" ?') (0+ (not (any ?\" ?')))))) nil)) (cons (company-grab (rx (or ?/ ?\" ?') (group (0+ (not (any ?\" ?'))))) 1) t) (company-grab-symbol-cons "\\." 1))) (defun tide-member-completion-p (prefix) (save-excursion (backward-char (length prefix)) (and (> (point) (point-min)) (equal (string (char-before (point))) ".")))) (defun tide-completion-filter-candidates (completions prefix) (-filter (lambda (completion) (and (if tide-completion-fuzzy (let ((case-fold-search tide-completion-ignore-case)) (string-match-p (regexp-quote prefix) (plist-get completion :name))) (string-prefix-p prefix (plist-get completion :name) tide-completion-ignore-case)) (or (not tide-filter-out-warning-completions) (not (equal (plist-get completion :kind) "warning"))))) completions)) (defun tide-annotate-completions (completions prefix file-location) (-map (lambda (completion) (let ((name (plist-get completion :name))) (put-text-property 0 1 'file-location file-location name) (put-text-property 0 1 'completion completion name) (put-text-property 0 1 'prefix prefix name) name)) (let ((filtered (tide-completion-filter-candidates completions prefix))) (let ((completions-comparator (if tide-sort-completions-by-kind (tide-compose-comparators 'tide-compare-completions-basic 'tide-compare-completions-by-kind) 'tide-compare-completions-basic))) (-sort completions-comparator filtered))))) (defun tide-command:completions (prefix cb) (let ((file-location `(:file ,(tide-buffer-file-name) :line ,(tide-line-number-at-pos) :offset ,(- (tide-current-offset) (length prefix)) :includeExternalModuleExports ,tide-completion-enable-autoimport-suggestions :includeInsertTextCompletions t))) (when (and (not (tide-in-string-p)) (not (tide-member-completion-p prefix))) (setq file-location (-concat file-location `(:prefix ,prefix)))) (tide-send-command "completions" file-location (lambda (response) (funcall cb (when (tide-response-success-p response) (tide-annotate-completions (plist-get response :body) prefix file-location))))))) (defun tide-command:completionEntryDetails (name) (let* ((source (plist-get (get-text-property 0 'completion name) :source)) (entry-names (if source `(:entryNames [(:name ,name :source ,source)]) `(:entryNames [,name]))) (arguments (-concat (get-text-property 0 'file-location name) entry-names))) (-when-let (response (tide-send-command-sync "completionEntryDetails" arguments)) (when (tide-response-success-p response) response)))) (defun tide-completion-entry-details (name) (-if-let (detail-response (get-text-property 0 'completion-detail name)) detail-response (let ((detail-response (tide-command:completionEntryDetails name))) (put-text-property 0 1 'completion-detail detail-response name) detail-response))) (defun tide-completion-meta (name) (-when-let* ((response (tide-completion-entry-details name)) (detail (car (plist-get response :body)))) (tide-completion-append-source (tide-doc-text detail) name t))) (defun tide-completion-append-source (text name detailed) (if detailed (-if-let* ((response (tide-completion-entry-details name)) (detail (car (plist-get response :body))) (raw-source (plist-get detail :source))) (tide-join (list text " " (tide-normalize-source (tide-annotate-display-parts raw-source)))) text) (-if-let* ((completion (get-text-property 0 'completion name)) (raw-source (plist-get completion :source))) (tide-join (list text " " (tide-normalize-source raw-source))) text))) (defun tide-normalize-source (source) "Normalize tsserver returned source: 1. Transform to relative path 2. Cleanup path components before last `/node_modules/` 3. Cleanup `/index` if it is the last path component 4. Transform `@types/namespace__pkgname` to `@namespace/pkgname`" (--> source (if (file-name-absolute-p it) (file-relative-name it (buffer-file-name)) it) (if (s-contains? "/node_modules/" it) (->> it (s-split "/node_modules/") (-last-item)) it) (s-chop-suffix "/index" it) (if (s-starts-with? "@types/" it) (-as-> (s-chop-prefix "@types/" it) itt (if (s-contains? "__" itt) (->> itt (s-replace "__" "/") (s-concat "@")) itt)) it))) (defun tide-completion-doc-buffer (name) (-when-let* ((response (tide-completion-entry-details name)) (detail (car (plist-get response :body)))) (tide-make-help-buffer "documentation" (tide-construct-documentation detail)))) (defun tide-post-completion (name) (let ((completion (get-text-property 0 'completion name)) (prefix (get-text-property 0 'prefix name))) (-when-let (insert-text (plist-get completion :insertText)) (when (looking-back (rx-to-string name) nil) (backward-delete-char (length name)) (-if-let (span (plist-get completion :replacementSpan)) (progn (when (string-prefix-p prefix (plist-get completion :name) tide-completion-ignore-case) (insert prefix)) ;; tsserver assumes the prefix text is already inserted for non-fuzzy completion. (tide-apply-edit (tide-combine-plists span `(:newText ,insert-text)))) (insert insert-text)))) (-when-let* ((has-action (plist-get completion :hasAction)) (response (tide-completion-entry-details name)) (detail (car (plist-get response :body))) (fixes (plist-get detail :codeActions))) (tide-apply-codefixes fixes #'tide-apply-codefix)))) ;;;###autoload (defun company-tide (command &optional arg &rest ignored) (interactive (list 'interactive)) (cl-case command ((interactive) (company-begin-backend 'company-tide)) ((prefix) (and (bound-and-true-p tide-mode) (-any-p #'derived-mode-p tide-supported-modes) (tide-current-server) (not (nth 4 (syntax-ppss))) (or (tide-completion-prefix) 'stop))) ((candidates) (cons :async (lambda (cb) (tide-command:completions arg cb)))) ((sorted) t) ((no-cache) tide-completion-fuzzy) ((ignore-case) tide-completion-ignore-case) ((meta) (tide-completion-meta arg)) ((match) (let* ((completion (get-text-property 0 'completion arg)) (prefix (get-text-property 0 'prefix arg)) (start (if tide-completion-fuzzy (let ((case-fold-search tide-completion-ignore-case)) (string-match-p (regexp-quote prefix) (plist-get completion :name))) 0))) `((,start . ,(+ start (length prefix)))))) ((annotation) (tide-completion-annotation arg)) ((kind) (tide-completion-kind arg)) ((doc-buffer) (tide-completion-doc-buffer arg)) ((post-completion) (tide-post-completion arg)) ((pre-render) (let ((name arg) (annotation-p (car ignored))) (if (and (not annotation-p) (s-contains? "deprecated" (or (plist-get (get-text-property 0 'completion name) :kindModifiers) ""))) (propertize name 'face '(:strike-through t)) name))))) (with-eval-after-load 'company (when tide-completion-setup-company-backend (cl-pushnew 'company-tide company-backends))) ;;; References (defun tide--next-reference (pos arg &optional cyclep) "Move to the ARG-th next reference from pos, negative values go to previous ones. Cycle around if CYCLEP is non-nil." (let* ((nextp (>= arg 0)) (next (if nextp #'next-single-property-change #'previous-single-property-change)) (n (* 2 (abs arg)))) (when (get-text-property pos 'tide-reference) (setq pos (or (funcall next pos 'tide-reference) pos))) (when (or nextp (and (> pos (point-min)) (get-text-property (1- pos) 'tide-reference))) (setq n (1- n))) (dotimes (_i n) (setq pos (funcall next pos 'tide-reference)) (unless pos (unless cyclep (error "Moved %s reference" (if nextp "past last" "before first"))) (setq pos (funcall next (if nextp (point-min) (point-max)) 'tide-reference)))) (goto-char pos))) (defun tide-next-reference-function (n &optional reset) "Override for `next-error-function' for use in tide-reference-mode buffers." (interactive "p") (-when-let (buffer (get-buffer "*tide-references*")) (with-current-buffer buffer (when reset (goto-char (point-min))) (tide--next-reference (point) (or n 1)) (-when-let (window (get-buffer-window buffer)) ;; actually move to the point if the refs are shown (set-window-point window (point))) (tide-goto-reference)))) (defun tide-find-next-reference (pos arg) "Move to next reference." (interactive "d\np") (tide--next-reference pos arg)) (defun tide-cycle-next-reference (pos arg) "Move to next reference, cycling back when reaching the last. Move back when used from a shifted key binding." (interactive "d\np") (tide--next-reference pos arg t)) (defun tide-find-previous-reference (pos arg) "Move back to previous reference." (interactive "d\np") (tide--next-reference pos (- arg))) (defun tide-cycle-previous-reference (pos arg) (interactive "d\np") (tide--next-reference pos (- arg) t)) (defun tide-goto-reference () "Jump to reference location in the file." (interactive) (-when-let (reference (get-text-property (point) 'tide-reference)) (tide-jump-to-filespan reference nil t))) (defun tide-goto-line-reference () "Jump to the corresponding location in the referenced file." (interactive) (-when-let* ((ref (get-text-property (point) 'tide-line-reference)) (col (- (point) (line-beginning-position)))) (tide-jump-to-filespan ref nil t) (goto-char (+ (line-beginning-position) col)))) (defvar tide-references-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "n") #'tide-find-next-reference) (define-key map (kbd "p") #'tide-find-previous-reference) (define-key map (kbd "TAB") #'tide-cycle-next-reference) (define-key map (kbd "") #'tide-cycle-previous-reference) (define-key map (kbd "RET") #'tide-goto-line-reference) (define-key map [mouse-1] #'tide-goto-reference) ;; taken from grep.el (define-key map (kbd "SPC") #'scroll-up-command) (define-key map (kbd "S-SPC") #'scroll-down-command) (define-key map (kbd "DEL") #'scroll-down-command) map)) (define-derived-mode tide-references-mode special-mode "tide-references" "Major mode for tide references list. \\{tide-references-mode-map}" (setq next-error-function #'tide-next-reference-function)) (defun tide-command:references (&optional location) (let ((location (or location `(:file ,(tide-buffer-file-name) :line ,(tide-line-number-at-pos) :offset ,(tide-current-offset))))) (tide-send-command-sync "references" location))) (defun tide-insert-references (references) "Create a buffer with the given REFERENCES. Assumes references are grouped by file name and sorted by line number." (let* ((buffer (get-buffer-create "*tide-references*")) (inhibit-read-only t) (width tab-width) (project-root (tide-project-root)) (last-file-name nil) (prefix-len (length (number-to-string (--reduce (max acc (tide-plist-get it :start :line)) (cons 0 references))))) (linenum-fmt (format "%%%dd" prefix-len)) (wrap-prefix (make-string prefix-len ?\ ))) (with-current-buffer buffer (erase-buffer) (tide-references-mode) (setq-local tab-width width) (while references (let* ((reference (car references)) (full-file-name (plist-get reference :file)) (file-name (file-relative-name full-file-name project-root)) (line-number (tide-plist-get reference :start :line)) (line-text (concat (plist-get reference :lineText) "\n")) (line-prefix (concat (propertize (format linenum-fmt line-number) 'face 'tide-line-number) ": "))) ;; file (unless (equal last-file-name file-name) (setq last-file-name file-name) (insert (propertize "\n" 'line-prefix (propertize file-name 'face 'tide-file)))) ;; line text (while (and references (equal full-file-name (plist-get (car references) :file)) (equal line-number (tide-plist-get (car references) :start :line))) (let* ((reference (pop references)) (start (1- (tide-plist-get reference :start :offset))) (end (1- (tide-plist-get reference :end :offset)))) (dolist (p `((tide-reference ,reference) (face tide-match) (mouse-face highlight) (help-echo "mouse-1: Visit the reference."))) (put-text-property start end (car p) (cadr p) line-text)))) (insert (propertize line-text 'line-prefix line-prefix 'wrap-prefix wrap-prefix 'tide-line-reference reference)))) (goto-char (point-min)) (forward-line 1) (set-buffer-modified-p nil) (current-buffer)))) (defun tide-is-identical-reference (original second) (and (equal (plist-get original :file) (plist-get second :file)) (eq (tide-plist-get original :start :line) (tide-plist-get second :start :line)))) (defun tide-find-single-usage (references) (let ((definition nil) (usage nil) (multiple nil)) (-each references #'(lambda (reference) (if (eq t (plist-get reference :isDefinition)) (if (or (eq definition nil) (tide-is-identical-reference definition reference)) (setq definition reference) (setq multiple t)) (if (or (eq usage nil) (tide-is-identical-reference usage reference)) (setq usage reference) (setq multiple t))))) (and (not multiple) definition usage))) (defun tide-references () "List all references to the symbol at point." (interactive) (let ((response (tide-command:references))) (tide-on-response-success response (let ((references (tide-plist-get response :body :refs))) (-if-let (usage (tide-find-single-usage references)) (progn (message "This is the only usage.") (tide-jump-to-filespan usage nil t)) (display-buffer (tide-insert-references references))))))) ;;; Imenu (defun tide-build-flat-imenu-index (navtree &optional parent) (let* ((child-items (plist-get navtree :childItems)) (text (plist-get navtree :text)) (new-text (if parent (concat parent imenu-level-separator text) text)) (node (cons (concat new-text " " (propertize (plist-get navtree :kind) 'face 'tide-imenu-type-face)) (tide-span-to-position (plist-get (car (plist-get navtree :spans)) :start))))) (if child-items (-concat (list node) (-flatten (-map (lambda (i) (tide-build-flat-imenu-index i new-text)) child-items))) (list node)))) (defun tide-build-imenu-index (navtree) (let* ((child-items (plist-get navtree :childItems)) (text (plist-get navtree :text)) (node (cons (concat text " " (propertize (plist-get navtree :kind) 'face 'tide-imenu-type-face)) (tide-span-to-position (plist-get (car (plist-get navtree :spans)) :start))))) (if child-items (cons text (-concat (list node) (-map #'tide-build-imenu-index child-items))) node))) (defun tide-command:navbar () (tide-send-command-sync "navtree" `(:file ,(tide-buffer-file-name)))) (defun tide-imenu-index () (let ((response (tide-command:navbar))) (tide-on-response-success response (let ((children (tide-plist-get response :body :childItems))) (if tide-imenu-flatten (-flatten (-map #'tide-build-flat-imenu-index children)) (mapcar #'tide-build-imenu-index children)))))) ;;; Rename (defun tide-command:rename () (tide-send-command-sync "rename" `(:file ,(tide-buffer-file-name) :line ,(tide-line-number-at-pos) :offset ,(tide-current-offset)))) (defun tide-rename-symbol-at-location (location new-symbol) (let ((file (plist-get location :file))) (save-excursion (with-current-buffer (tide-get-file-buffer file) (-each (-map (lambda (filespan) (tide-move-to-location (plist-get filespan :start)) (cons (point-marker) filespan)) (plist-get location :locs)) (lambda (pointer) (let* ((marker (car pointer)) (filespan (cdr pointer))) (goto-char marker) (delete-char (- (tide-plist-get filespan :end :offset) (tide-plist-get filespan :start :offset))) (insert new-symbol)))) ;; Saving won't work if the current buffer is temporary or an indirect ;; buffer (when (equal buffer-file-name file) (basic-save-buffer)) (length (plist-get location :locs)))))) (defun tide-read-new-symbol (old-symbol) (let ((new-symbol (read-from-minibuffer (format "Rename %s to: " old-symbol) old-symbol))) (if (string-match-p "\\`[ \t\n\r]*\\'" new-symbol) (error "Invalid name") new-symbol))) (defun tide-can-rename-symbol-p () (let ((response (tide-command:rename))) (and (tide-response-success-p response) (eq (tide-plist-get response :body :info :canRename) t)))) (defun tide-rename-symbol () "Rename symbol at point." (interactive) (let ((response (tide-command:rename))) (tide-on-response-success response (if (eq (tide-plist-get response :body :info :canRename) :json-false) (message "%s" (tide-plist-get response :body :info :localizedErrorMessage)) (let* ((old-symbol (tide-plist-get response :body :info :displayName)) (new-symbol (tide-read-new-symbol old-symbol)) (locs (tide-plist-get response :body :locs)) (count 0) (current-file-p (lambda (loc) (file-equal-p (expand-file-name (tide-buffer-file-name)) (plist-get loc :file))))) ;; Saving current file will trigger a compilation ;; check. So make sure all the other files are saved ;; before saving current file. (-each (nconc (-reject current-file-p locs) (-select current-file-p locs)) (lambda (loc) (cl-incf count (tide-rename-symbol-at-location loc new-symbol)))) (message "Renamed %d occurrences." count)))))) (defun tide-command:getEditsForFileRename (old new) (tide-send-command-sync "getEditsForFileRename" `(:oldFilePath ,old :newFilePath ,new :file ,old))) (defun tide-do-rename-file (old new edits) (let* ((code-edit-for-old-p (lambda (code-edit) (string= (plist-get code-edit :fileName) old))) (before-rename-edits (-select code-edit-for-old-p edits)) (after-rename-edits (-reject code-edit-for-old-p edits))) (tide-apply-code-edits before-rename-edits) (tide-cleanup-buffer) (mkdir (file-name-directory new) t) (rename-file old new) (rename-buffer (file-name-nondirectory new) t) (set-visited-file-name new) (set-buffer-modified-p nil) (tide-apply-code-edits after-rename-edits) (revert-buffer t t t))) (defun tide-rename-file () "Rename current file and all it's references in other files." (interactive) (let* ((name (buffer-name)) (old (tide-buffer-file-name)) (basename (file-name-nondirectory old))) (unless (and old (file-exists-p old)) (error "Buffer '%s' is not visiting a file." name)) (let ((new (read-file-name "New name: " (file-name-directory old) basename nil basename))) (when (get-file-buffer new) (error "A buffer named '%s' already exists." new)) (when (file-exists-p new) (error "A file named '%s' already exists." new)) (let* ((old (expand-file-name old)) (new (expand-file-name new)) (response (tide-command:getEditsForFileRename old new))) (tide-on-response-success response (:min-version "2.9") (tide-do-rename-file old new (plist-get response :body)) (message "Renamed '%s' to '%s'." name (file-name-nondirectory new))))))) ;;; Format ;;;###autoload (defun tide-format-before-save () "Before save hook to format the buffer before each save." (interactive) (when (bound-and-true-p tide-mode) (tide-format))) ;;;###autoload (defun tide-format () "Format the current region or buffer." (interactive) (if (use-region-p) (tide-format-region (region-beginning) (region-end)) (tide-format-region (point-min) (point-max)))) (defun tide-normalize-lineshift (str) "Reformat `STR' to only contain line-shift formats expected by Emacs. When inserting text in an Emacs-buffer Emacs only ever expects \n for newlines, no matter what the actual encoding of the file is. Inserting anything else causes issues with formatting and code-analysis." ;; convert DOS CR+LF to LF (setq str (replace-regexp-in-string "\r\n" "\n" str)) ;; convert Mac CR to LF (setq str (subst-char-in-string ?\r ?\n str)) str) (defun tide-insert (str) "Insert `STR' into the buffer, but normalize the line-enings." (insert (tide-normalize-lineshift str))) (defun tide-apply-edit (edit) (goto-char (tide-location-to-point (plist-get edit :start))) (delete-region (point) (tide-location-to-point (plist-get edit :end))) (let ((start (point-marker))) (tide-insert (plist-get edit :newText)) (cons start (point-marker)))) (defun tide-do-apply-edits (edits) (save-excursion (-map (lambda (edit) (tide-apply-edit edit)) (nreverse edits)))) (defun tide-apply-edits (edits) (if (and (fboundp 'combine-change-calls) (> (length edits) 2)) (combine-change-calls (point-min) (point-max) (tide-do-apply-edits edits)) (tide-do-apply-edits edits))) (defun tide-format-region (start end) (let ((response (tide-send-command-sync "format" `(:file ,(tide-buffer-file-name) :line ,(tide-line-number-at-pos start) :offset ,(tide-offset start) :endLine ,(tide-line-number-at-pos end) :endOffset ,(tide-offset end))))) (tide-on-response-success response (tide-apply-edits (plist-get response :body))))) (defun tide-format-regions (ranges) (let ((positions (->> ranges (-mapcat (lambda (range) (list (marker-position (car range)) (marker-position (cdr range))))) (-sort '<)))) (tide-format-region (-min positions) (-max positions)))) ;;; JSDoc Comment Template (defun tide-command:docCommentTemplate () (tide-send-command-sync "docCommentTemplate" `(:file ,buffer-file-name :line ,(tide-line-number-at-pos) :offset ,(tide-current-offset)))) (defun tide-jsdoc-template () "Insert JSDoc comment template at point" (interactive) (let ((response (tide-command:docCommentTemplate))) (tide-on-response-success response (progn (save-excursion (tide-insert (tide-plist-get response :body :newText))) (forward-char (tide-plist-get response :body :caretOffset)))))) ;;; Mode (defvar tide-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "M-.") #'tide-jump-to-definition) (define-key map (kbd "M-,") #'tide-jump-back) map)) (defun tide-configure-buffer () (setq tide-active-buffer-file-name (tide-buffer-file-name)) (tide-command:openfile) (tide-command:configure) ;; tsserver requires non-.ts files to be manually added to the files array in ;; tsconfig.json, otherwise the file will be loaded as part of an 'inferred ;; project'. This won't be necessary anymore after TypeScript allows defining ;; custom file extensions. https://github.com/Microsoft/TypeScript/issues/8328 (when (and tide-require-manual-setup (tide-buffer-file-name)) (tide-command:projectInfo (lambda (response) (tide-on-response-success response (when (string-prefix-p "/dev/null/inferredProject" (tide-plist-get response :body :configFileName)) (message (format "'%s' is not part of a project, add it to the files array in tsconfig.json" (tide-buffer-file-name))))))))) (defun tide-configure-buffer-if-server-exists () "Invoke `tide-configure-buffer' only if there is a server running for the current buffer." (when (tide-current-server) (tide-configure-buffer))) (defun tide-cleanup-buffer () (ignore-errors (tide-command:closefile)) (ignore-errors (tide-remove-tmp-file))) ;;;###autoload (defun tide-setup () "Setup `tide-mode' in current buffer." (interactive) (when (version< emacs-version tide--minimal-emacs) (display-warning 'tide (format "Tide requires Emacs >= %s, you are using %s." tide--minimal-emacs emacs-version) :error)) ;; Indirect buffers embedded in other major modes such as those in org-mode or ;; template languages have to be manually synchronized to tsserver. This might ;; cause problems in files with lots of small blocks of TypeScript. In that ;; case we should either add an ignore list or don't do anything at all when ;; there are more than a certain amount of snippets. (unless (stringp buffer-file-name) (setq tide-require-manual-setup t)) (set (make-local-variable 'eldoc-documentation-function) 'tide-eldoc-function) (set (make-local-variable 'imenu-auto-rescan) t) (set (make-local-variable 'imenu-create-index-function) 'tide-imenu-index) (tide-mode 1) (if (tide-current-server) ;; ;; We want to issue tide-configure-buffer here if the server exists. We ;; cannot rely on hack-local-variable-hook in tide-mode because the hook ;; may not run at all, or run too late. ;; ;; It may happen for some use-case scenarios that tide-configure-buffer ;; runs more than once with the same data for the same buffer, but that's ;; not a big deal. ;; (tide-configure-buffer) (when (eq tide-tsserver-start-method 'immediate) (tide-start-server)))) ;;;###autoload (define-minor-mode tide-mode "Minor mode for Typescript Interactive Development Environment. \\{tide-mode-map}" :lighter " tide" :keymap tide-mode-map (if tide-mode (progn (add-hook 'after-save-hook 'tide-sync-buffer-contents nil t) (add-hook 'after-save-hook 'tide-auto-compile-file nil t) (add-hook 'after-change-functions 'tide-handle-change nil t) (add-hook 'kill-buffer-hook 'tide-cleanup-buffer nil t) (add-hook 'kill-buffer-hook 'tide-schedule-dead-projects-cleanup nil t) (add-hook 'hack-local-variables-hook 'tide-configure-buffer-if-server-exists nil t) (when tide-enable-xref (add-hook 'xref-backend-functions #'xref-tide-xref-backend nil t)) (when (commandp 'typescript-insert-and-indent) (eldoc-add-command 'typescript-insert-and-indent))) (remove-hook 'after-save-hook 'tide-sync-buffer-contents t) (remove-hook 'after-save-hook 'tide-auto-compile-file t) (remove-hook 'after-change-functions 'tide-handle-change t) (remove-hook 'kill-buffer-hook 'tide-cleanup-buffer t) (remove-hook 'kill-buffer-hook 'tide-schedule-dead-projects-cleanup t) (remove-hook 'hack-local-variables-hook 'tide-configure-buffer-if-server-exists t) (remove-hook 'xref-backend-functions #'xref-tide-xref-backend t) (tide-cleanup-buffer))) ;;; Error highlighting (defun tide-command:geterr (cb) (let* ((result '()) (resolved nil) (err nil)) (cl-flet ((resolve () (cond (resolved nil) (err (setq resolved t) (funcall cb err)) ((and (plist-member result :syntaxDiag) (plist-member result :semanticDiag) (plist-member result :suggestionDiag)) (setq resolved t) (funcall cb `(:body (,result) :success t)))))) (tide-send-command "syntacticDiagnosticsSync" `(:file ,(tide-buffer-file-name)) (lambda (response) (if (tide-response-success-p response) (setq result (plist-put result :syntaxDiag (plist-get response :body))) (setq err response)) (resolve))) (tide-send-command "semanticDiagnosticsSync" `(:file ,(tide-buffer-file-name)) (lambda (response) (if (tide-response-success-p response) (setq result (plist-put result :semanticDiag (plist-get response :body))) (setq err response)) (resolve))) (if tide-disable-suggestions (setq result (plist-put result :suggestionDiag '())) (tide-send-command "suggestionDiagnosticsSync" `(:file ,(tide-buffer-file-name)) (lambda (response) (cond ((tide-response-success-p response) (setq result (plist-put result :suggestionDiag (plist-get response :body)))) ((tide-command-unknown-p response) (setq result (plist-put result :suggestionDiag '()))) (t (setq err response))) (resolve))))))) (defun tide-parse-error (response checker) (-map (lambda (diagnostic) (let* ((start (plist-get diagnostic :start)) (line (plist-get start :line)) (column (plist-get start :offset)) (level (if (string= (plist-get diagnostic :category) "suggestion") 'info 'error)) (text (plist-get diagnostic :text))) (when (plist-get diagnostic :relatedInformation) (setq text (concat text (propertize " ⮐" 'face 'font-lock-warning-face)))) (put-text-property 0 1 'diagnostic diagnostic text) (flycheck-error-new-at line column level text :checker checker :id (plist-get diagnostic :code)))) (let ((diagnostic (car (tide-plist-get response :body)))) (-concat (plist-get diagnostic :syntaxDiag) (plist-get diagnostic :semanticDiag) (plist-get diagnostic :suggestionDiag))))) (defun tide-flycheck-send-response (callback checker response) (condition-case err (funcall callback 'finished (tide-parse-error response checker)) (error (funcall callback 'errored (error-message-string err))))) (defun tide-flycheck-start (checker callback) (tide-command:geterr (lambda (response) (when (tide-command-unknown-p response) (tide-tsserver-version-not-supported)) (if (tide-response-success-p response) (tide-flycheck-send-response callback checker response) (funcall callback 'errored (plist-get response :message)))))) (defun tide-make-clickable-filespan (filespan) (propertize (concat (file-name-nondirectory (plist-get filespan :file)) ":" (number-to-string (tide-plist-get filespan :start :line))) 'face 'link 'help-echo "mouse-2: go to this location" 'keymap (let ((map (make-sparse-keymap))) (define-key map [mouse-2] 'tide-goto-error) (define-key map [mouse-1] 'tide-goto-error) (define-key map (kbd "RET") 'tide-goto-error) (define-key map [follow-link] 'mouse-face) map) 'tide-error filespan)) (defun tide-format-related-information (related) (concat (tide-make-clickable-filespan (plist-get related :span)) " " (plist-get related :message) " [" (number-to-string (plist-get related :code)) "]")) (defun tide-explain-error (err) (let* ((diagnostic (get-text-property 0 'diagnostic (flycheck-error-message err))) (related (plist-get diagnostic :relatedInformation))) (concat (propertize "Code: " 'face 'bold) (number-to-string (plist-get diagnostic :code)) " " (propertize "Category: " 'face 'bold) (plist-get diagnostic :category) "\n\n" (plist-get diagnostic :text) "\n\n" (when related (concat (propertize "Related Information: \n\n" 'face 'bold) (mapconcat 'tide-format-related-information related "\n\n")))))) (defun tide-error-at-point () "Show the details of the error at point." (interactive) (-if-let (errors (flycheck-overlay-errors-at (point))) (tide-display-help-buffer "error" (mapconcat #'tide-explain-error errors "\n\n--------\n\n")) (message "No errors available."))) (defun tide-flycheck-verify (_checker) (list (flycheck-verification-result-new :label "Typescript server" :message (if (tide-current-server) "running" "not running") :face (if (tide-current-server) 'success '(bold error))) (flycheck-verification-result-new :label "Tide mode" :message (if (bound-and-true-p tide-mode) "enabled" "disabled") :face (if (bound-and-true-p tide-mode) 'success '(bold warning))))) (defun tide-flycheck-predicate () (and (bound-and-true-p tide-mode) (tide-current-server) (not (file-equal-p (tide-project-root) tide-tsserver-directory)))) (defun tide-file-extension-p (ext) (and buffer-file-name (string-equal ext (file-name-extension buffer-file-name)))) (flycheck-define-generic-checker 'typescript-tide "A TypeScript syntax checker using tsserver." :start #'tide-flycheck-start :verify #'tide-flycheck-verify :modes '(typescript-mode) :predicate #'tide-flycheck-predicate) (add-to-list 'flycheck-checkers 'typescript-tide) (flycheck-define-generic-checker 'javascript-tide "A Javascript syntax checker using tsserver." :start #'tide-flycheck-start :verify #'tide-flycheck-verify :modes '(js-mode js2-mode js3-mode) :predicate #'tide-flycheck-predicate) (add-to-list 'flycheck-checkers 'javascript-tide t) (flycheck-define-generic-checker 'jsx-tide "A JSX syntax checker using tsserver." :start #'tide-flycheck-start :verify #'tide-flycheck-verify :modes '(web-mode js2-jsx-mode rjsx-mode) :predicate (lambda () (and (tide-file-extension-p "jsx") (tide-flycheck-predicate)))) (add-to-list 'flycheck-checkers 'jsx-tide t) (flycheck-define-generic-checker 'tsx-tide "A TSX syntax checker using tsserver." :start #'tide-flycheck-start :verify #'tide-flycheck-verify :modes '(web-mode) :predicate (lambda () (and (tide-file-extension-p "tsx") (tide-flycheck-predicate)))) (add-to-list 'flycheck-checkers 'tsx-tide) ;;; Project errors (defun tide-command:geterrForProject (file) (tide-send-command "geterrForProject" `(:file ,file :delay 0))) (defun tide-project-errors-buffer-name () (format "*%s-errors*" (tide-project-name))) (defun tide-display-errors (file-names origin-buffer-file-name) (let ((origin-buffer (current-buffer))) (with-current-buffer (get-buffer-create (tide-project-errors-buffer-name)) (tide-project-errors-mode) (setq-local tide-origin-buffer-file-name origin-buffer-file-name) (let ((inhibit-read-only t)) (erase-buffer)) (when (not (equal origin-buffer (current-buffer))) (display-buffer (current-buffer) t)) (let* ((project-files (-reject (lambda (file-name) (or (string-match-p "node_modules" file-name) (string-match-p "tsconfig.json$" file-name))) file-names)) (syntax-remaining-files (cl-copy-list project-files)) (semantic-remaining-files (cl-copy-list project-files)) (suggestion-remaining-files (if tide-disable-suggestions '() (cl-copy-list project-files))) (syntax-errors 0) (semantic-errors 0) (suggestion-errors 0) (last-file-name nil)) (tide-set-event-listener (lambda (response) (save-excursion (goto-char (point-max)) (let ((inhibit-read-only t) (file-name (tide-plist-get response :body :file)) (diagnostics (tide-plist-get response :body :diagnostics)) (event-type (plist-get response :event))) (when (and (not (and (string-equal event-type "suggestionDiag") tide-disable-suggestions)) (member file-name project-files)) (pcase event-type ("syntaxDiag" (setq syntax-remaining-files (remove file-name syntax-remaining-files)) (cl-incf syntax-errors (length diagnostics))) ("semanticDiag" (setq semantic-remaining-files (remove file-name semantic-remaining-files)) (cl-incf semantic-errors (length diagnostics))) ("suggestionDiag" (setq suggestion-remaining-files (remove file-name suggestion-remaining-files)) (cl-incf suggestion-errors (length diagnostics)))) (when diagnostics (-each diagnostics (lambda (diagnostic) (let ((line-number (tide-plist-get diagnostic :start :line))) (unless (equal last-file-name file-name) (setq last-file-name file-name) (insert (propertize (file-relative-name file-name (tide-project-root)) 'face 'tide-file) "\n")) (insert (propertize (format "%5d" line-number) 'face 'tide-line-number 'tide-error (plist-put diagnostic :file file-name)) ": " (plist-get diagnostic :text) "\n"))))) (when (and (null syntax-remaining-files) (null semantic-remaining-files) (null suggestion-remaining-files)) (insert (format "\n%d syntax error(s), %d semantic error(s), %d suggestion error(s)\n" syntax-errors semantic-errors suggestion-errors)) (goto-char (point-min)) (tide-clear-event-listener)))))))))) (tide-command:geterrForProject origin-buffer-file-name)) (defun tide-next-error-function (n &optional reset) "Override for `next-error-function' for use in tide-project-errors-mode buffers." (interactive "p") (-when-let (buffer (get-buffer (tide-project-errors-buffer-name))) (with-current-buffer buffer (when reset (goto-char (point-min))) (if (> n 0) (tide-find-next-error (point) n) (tide-find-previous-error (point) (- n))) (tide-goto-error)))) (defun tide-find-next-error (pos arg) "Move to next error." (interactive "d\np") (setq arg (* 2 arg)) (unless (get-text-property pos 'tide-error) (setq arg (1- arg))) (dotimes (_i arg) (setq pos (next-single-property-change pos 'tide-error)) (unless pos (error "Moved past last error"))) (goto-char pos)) (defun tide-find-previous-error (pos arg) "Move back to previous error." (interactive "d\np") (dotimes (_i (* 2 arg)) (setq pos (previous-single-property-change pos 'tide-error)) (unless pos (error "Moved back before first error"))) (goto-char pos)) (defun tide-goto-error () "Jump to error location in the file." (interactive) (-when-let (error (get-text-property (point) 'tide-error)) (tide-jump-to-filespan error nil t))) (defvar tide-project-errors-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "n") #'tide-find-next-error) (define-key map (kbd "p") #'tide-find-previous-error) (define-key map (kbd "g") #'tide-project-errors) (define-key map (kbd "RET") #'tide-goto-error) map)) (define-derived-mode tide-project-errors-mode special-mode "tide-project-errors" "Major mode for tide project-errors list. \\{tide-project-errors-mode-map}" (setq next-error-function #'tide-next-error-function)) ;;;###autoload (defun tide-project-errors () (interactive) (let ((file (if (string= major-mode "tide-project-errors-mode") tide-origin-buffer-file-name (tide-buffer-file-name)))) (tide-command:projectInfo (lambda (response) (tide-on-response-success response (tide-display-errors (tide-plist-get response :body :fileNames) file))) t file))) ;;; Identifier highlighting (defun tide-command:documentHighlights (cb) (tide-send-command "documentHighlights" `(:file ,(tide-buffer-file-name) :line ,(tide-line-number-at-pos) :offset ,(tide-current-offset) :filesToSearch [,(tide-buffer-file-name)]) cb)) (defface tide-hl-identifier-face '((t (:inherit highlight))) "Face used for highlighting identifiers in `tide-hl-identifier'." :group 'tide) (defcustom tide-hl-identifier-idle-time 0.50 "How long to wait after user input before highlighting the current identifier." :type 'float :group 'tide) (tide-def-permanent-buffer-local tide--hl-last-token 0) (defun tide--hl-new-token () "Invalidate all existing tokens to get document highlights and create a new token" (cl-incf tide--hl-last-token)) (defvar tide--current-hl-identifier-idle-time 0 "The current delay for hl-identifier-mode.") (defvar tide--hl-identifier-timer nil "The global timer used for highlighting identifiers.") ;;;###autoload (defun tide-unhighlight-identifiers () "Remove highlights from previously highlighted identifier." (tide--hl-new-token) (remove-overlays nil nil 'tide-overlay 'sameid)) ;;;###autoload (defun tide-hl-identifier () "Highlight all instances of the identifier under point. Removes highlights from previously highlighted identifier." (interactive) (tide-unhighlight-identifiers) (tide--hl-identifier)) (defun tide--hl-identifier () "Highlight all instances of the identifier under point." (let ((token (tide--hl-new-token))) (tide-command:documentHighlights (lambda (response) (when (and (equal token tide--hl-last-token) (tide-response-success-p response)) (tide--hl-highlight response)))))) (defun tide--hl-highlight (response) "Highlight all instances of the identifier under point." (-when-let* ((item (-first (lambda (item) (equal (tide-buffer-file-name) (plist-get item :file))) (plist-get response :body))) (references (plist-get item :highlightSpans))) ;; `overlay-recenter' provide a modest speed improvement when creating lots ;; of overlays from a list. See the documentation on this function for ;; details. (overlay-recenter (point-max)) ;; The point computations in this loop *could* be replaced with ;; `tide-location-to-point' but `tide-location-to-point' is extremely slow ;; when it comes to processing large lists of locations because it returns ;; to `point-min' with each call. (save-excursion (save-restriction (widen) (goto-char (point-min)) (let ((current-line 1)) (dolist (reference references) (when (member (plist-get reference :kind) '("reference" "writtenReference")) (let* ((start (plist-get reference :start)) (end (plist-get reference :end)) (start-line (plist-get start :line)) (end-line (plist-get end :line)) (ostart (progn (forward-line (- start-line current-line)) (forward-char (1- (plist-get start :offset))) (point))) (oend (if (= start-line end-line) (progn (forward-char (- (plist-get end :offset) (plist-get start :offset))) (point)) (forward-line (- end-line start-line)) (forward-char (1- (plist-get end :offset))) (point))) (overlay (make-overlay ostart oend))) (setq current-line end-line) (overlay-put overlay 'tide-overlay 'sameid) (overlay-put overlay 'face 'tide-hl-identifier-face))))))))) ;;;###autoload (define-minor-mode tide-hl-identifier-mode "Highlight instances of the identifier at point after a short timeout." :group 'tide (if tide-hl-identifier-mode (progn (tide--hl-set-timer) ;; Unhighlight if point moves off identifier (add-hook 'post-command-hook #'tide--hl-identifiers-post-command-hook nil t) ;; Unhighlight any time the buffer changes (add-hook 'before-change-functions #'tide--hl-identifiers-before-change-function nil t)) (remove-hook 'post-command-hook #'tide--hl-identifiers-post-command-hook t) (remove-hook 'before-change-functions #'tide--hl-identifiers-before-change-function t) (tide-unhighlight-identifiers))) (defun tide--hl-identifiers-function () "Function run after an idle timeout, highlighting the identifier at point, if necessary." (when tide-hl-identifier-mode (unless (tide--on-overlay-p 'sameid) (tide-hl-identifier)) (unless (eq tide--current-hl-identifier-idle-time tide-hl-identifier-idle-time) (tide--hl-set-timer)))) (defun tide--hl-set-timer () (when tide--hl-identifier-timer (cancel-timer tide--hl-identifier-timer)) (setq tide--current-hl-identifier-idle-time tide-hl-identifier-idle-time) (setq tide--hl-identifier-timer (run-with-idle-timer tide-hl-identifier-idle-time t #'tide--hl-identifiers-function))) (defun tide--on-overlay-p (id) "Return whether point is on a tide overlay of type ID." (cl-find-if (lambda (el) (eq (overlay-get el 'tide-overlay) id)) (overlays-at (point)))) (defun tide--hl-identifiers-post-command-hook () (when (and tide-hl-identifier-mode (not (tide--on-overlay-p 'sameid))) (tide-unhighlight-identifiers))) (defun tide--hl-identifiers-before-change-function (_beg _end) (tide-unhighlight-identifiers)) ;;; Compile On Save (defun tide-command:compileOnSaveEmitFile () (tide-send-command "compileOnSaveEmitFile" `(:file ,(tide-buffer-file-name)))) (defun tide-compile-file () "Compiles the current file" (interactive) (tide-command:compileOnSaveEmitFile)) (defun tide-auto-compile-file () "Compiles the current file if compileOnSave is set" (interactive) (-when-let (config (tide-project-config)) ;; Pre-v3.6.2 tsc converts compileOnSave to compilerOnSave (when (or (eq (plist-get config :compilerOnSave) t) (eq (plist-get config :compileOnSave) t)) (tide-command:compileOnSaveEmitFile)))) (defun tide-project-config () (let ((config (gethash (tide-project-name) tide-project-configs :not-loaded))) (if (eq config :not-loaded) (let* ((default-directory (tide-project-root)) (tscjs (tide-locate-tscompiler-executable))) (if tscjs (let ((config (tide-safe-json-read-string (tide-command-to-string tide-node-executable (list tscjs "--showConfig"))))) (puthash (tide-project-name) config tide-project-configs)) (puthash (tide-project-name) '() tide-project-configs))) config))) ;;; Utility commands (defun tide-kill-server () "Kill the server in the current buffer." (interactive) (-when-let (server (tide-current-server)) (delete-process server))) (defun tide-restart-server () "Restarts tsserver." (interactive) (tide-kill-server) (tide-start-server)) (defvar-local tide--server-list-mode-last-column nil) (defun tide--list-servers-verify-setup (button) "Invoke `tide-verify-setup' on a tsserver displayed in the list of server." (tide-first-buffer (button-get button 'project-name) #'tide-verify-setup)) ;; This is modeled after list-process--refresh but we do not call delete-process ;; on exited or signaled processe. That seems inappropriate for a function ;; designed to *report* information. (defun tide--list-servers-refresh () "Recompute the list of processes for the buffer displayed by `tide-list-servers'." (setq tabulated-list-entries nil) (let ((tsservers (hash-table-values tide-servers))) (dolist (p tsservers) (let* ((project-name (process-get p 'project-name)) (pid (process-id p)) (cpu (if (tide--emacs-at-least "25") (alist-get 'pcpu (process-attributes pid)) (cdr (assq 'pcpu (process-attributes pid)))))) (push (list p (vector `(,project-name face link help-echo ,(if (tide--emacs-at-least "25") (format-message "Verify setup of `%s'" project-name) (concat "Verify setup of `" project-name "'")) follow-link t project-name ,project-name action tide--list-servers-verify-setup) ;; Sometimes the CPU usage value is NaN (which Emacs represents ;; as 0.0e+NaN), for whatever reason. We cannot pass this value ;; to round so we put "--" for the column value. ;; Other times, it is nil, so do the same then. (if (or (not cpu) (isnan cpu)) "--" (number-to-string (round cpu))) (cl-case tide--server-list-mode-last-column ((project-root) (or (process-get p 'project-root) "")) ((command) (mapconcat 'identity (process-command p) " ")) (t (error "unknown column %s" tide--server-list-mode-last-column))))) tabulated-list-entries))))) (defun tide--server-list-kill-server () "Kill a tsserver instance." (interactive) (let ((process (tabulated-list-get-id))) (delete-process process) (revert-buffer))) (defvar tide--server-list-last-column-choice-list '(project-root command) "The possible choices for the last column, as a circular list.") (defun tide--server-list-cycle-last-column () "Cycle through the possible values for the last column." (interactive) (setq tide--server-list-mode-last-column (or (cadr (or (memq tide--server-list-mode-last-column tide--server-list-last-column-choice-list) (error "%s is not a possible choice of last column." tide--server-list-mode-last-column))) (car tide--server-list-last-column-choice-list))) (tide--setup-list-mode) (revert-buffer)) (defvar tide-server-list-mode-map (let ((map (make-sparse-keymap))) (define-key map [?d] 'tide--server-list-kill-server) (define-key map [?/] 'tide--server-list-cycle-last-column) map)) (defun tide--setup-list-mode () (setq tabulated-list-format (vector '("Project Name" 20 t) `("CPU" 5 ,(lambda (a b) (let* ((cpu-a (elt (cadr a) 1)) (cpu-b (elt (cadr b) 1))) ;; The CPU usage value in the column can be "--" if Emacs ;; produced a NaN value. We consider "--" to be less than numbers. (cond ((string= cpu-a "--") (not (string= cpu-b "--"))) ((string= cpu-b "--") nil) (t (< (string-to-number cpu-a) (string-to-number cpu-b))))))) (list (cl-case tide--server-list-mode-last-column ((project-root) "Project Root") ((command) "Project Command") (t (error "unknown column %s" tide--server-list-mode-last-column))) 0 t))) (setq tabulated-list-sort-key (cons "Project Name" nil)) (tabulated-list-init-header)) (define-derived-mode tide-server-list-mode tabulated-list-mode "tide-server-list-mode" "Major mode for listing tsserver processes." (setq-local tide--server-list-mode-last-column 'project-root) (add-hook 'tabulated-list-revert-hook 'tide--list-servers-refresh nil t) (tide--setup-list-mode)) ;; This is modeled after list-processes. (defun tide-list-servers (&optional buffer) "Lists the tsserver processes known to tide." (interactive) (unless (bufferp buffer) (setq buffer (get-buffer-create "*Tide Server List*"))) (with-current-buffer buffer (tide-server-list-mode) (tide--list-servers-refresh) (tabulated-list-print)) (display-buffer buffer) nil) (defun tide-command:status () (tide-send-command-sync "status" '())) (defun tide-show-project-info (version config-file-name) (with-current-buffer (get-buffer-create "*tide-project-info*") (let ((inhibit-read-only t)) (erase-buffer) (insert "tsserver version: " (propertize version 'face '(success bold)) "\n\n" "config file path: " (propertize config-file-name 'face 'success))) (special-mode) (display-buffer (current-buffer) t))) (defun tide-verify-setup () "Show the version of tsserver." (interactive) (let ((response (tide-command:status))) (tide-on-response-success response (:min-version "2.7") (let ((version (tide-plist-get response :body :version))) (tide-command:projectInfo (lambda (response) (tide-on-response-success response (let ((config-file-name (tide-plist-get response :body :configFileName))) (tide-show-project-info version config-file-name))))))))) ;;; xref integration (defun xref-tide-xref-backend () "Xref-tide backend for xref." 'xref-tide) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql xref-tide))) (let ((symbol (tide-get-symbol-at-point))) (when (and symbol (not (string-equal symbol ""))) (put-text-property 0 1 'location `(:file ,(tide-buffer-file-name) :line ,(tide-line-number-at-pos) :offset ,(tide-current-offset)) symbol)) symbol)) (defvar tide-xref--last-completion-table '()) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql xref-tide))) (completion-table-dynamic (lambda (prefix) (let ((response (tide-command:navto prefix t))) (tide-on-response-success response (-when-let (navto-items (plist-get response :body)) (setq tide-xref--last-completion-table navto-items) (-map (lambda (navto-item) (plist-get navto-item :name)) navto-items))))) t)) (cl-defmethod xref-backend-identifier-completion-ignore-case ((_backend (eql xref-tide))) tide-completion-ignore-case) (cl-defmethod xref-backend-references ((_backend (eql xref-tide)) symbol) (tide-xref--find-references symbol)) (cl-defmethod xref-backend-definitions ((_backend (eql xref-tide)) symbol) (tide-xref--find-definitions symbol)) (cl-defmethod xref-backend-apropos ((_backend (eql xref-tide)) pattern) (tide-xref--find-apropos pattern)) (defun tide-xref--make-reference (reference) "Make xref object from RERERENCE." (let ((full-file-name (plist-get reference :file)) (line-number (tide-plist-get reference :start :line)) (line-text (plist-get reference :lineText)) (start (1- (tide-plist-get reference :start :offset))) (end (1- (tide-plist-get reference :end :offset)))) (put-text-property start end 'face 'tide-match line-text) (xref-make line-text (xref-make-file-location full-file-name line-number start)))) (defun tide-xref--make-definition (symbol definition) "Make xref object from DEFINITION." (let ((full-file-name (plist-get definition :file)) (line-number (tide-plist-get definition :start :line)) (start (1- (tide-plist-get definition :start :offset)))) (xref-make symbol (xref-make-file-location full-file-name line-number start)))) (defun tide-xref--make-navto (pattern navto) "Make xref object from NAVTO." (let ((full-file-name (plist-get navto :file)) (line-text (tide-file-span-first-line-text navto pattern)) (line-number (tide-plist-get navto :start :line)) (start (1- (tide-plist-get navto :start :offset)))) (xref-make line-text (xref-make-file-location full-file-name line-number start)))) (defun tide-xref--symbol-location (symbol) (-if-let (location (get-text-property 0 'location symbol)) location (-when-let (navto-item (-find (lambda (navto-item) (string-equal symbol (plist-get navto-item :name))) tide-xref--last-completion-table)) (save-restriction (save-excursion (widen) (tide-move-to-location (plist-get navto-item :start)) (search-forward symbol (tide-location-to-point (plist-get navto-item :end))) `(:file ,(plist-get navto-item :file) :line ,(tide-line-number-at-pos) :offset ,(tide-current-offset))))))) (defun tide-xref--find-definitions (symbol) "Return xref definition objects." (let ((response (tide-command:definition nil (tide-xref--symbol-location symbol)))) (tide-on-response-success response (let ((definitions (tide-plist-get response :body))) (-map (apply-partially #'tide-xref--make-definition symbol) definitions))))) (defun tide-xref--find-references (symbol) "Return xref reference objects." (let ((response (tide-command:references (tide-xref--symbol-location symbol)))) (tide-on-response-success response (let ((references (tide-plist-get response :body :refs))) (-map #'tide-xref--make-reference references))))) (defun tide-xref--find-apropos (pattern) "Return xref navto objects." (let ((response (tide-command:navto pattern))) (tide-on-response-success response (-when-let (navto-items (plist-get response :body)) (-map (apply-partially #'tide-xref--make-navto pattern) navto-items))))) (provide 'tide) ;;; tide.el ends here