From b07628dddf418d4f47b858e6c35fd3520fbaeed2 Mon Sep 17 00:00:00 2001 From: mattkae Date: Tue, 7 Jun 2022 08:21:30 -0400 Subject: Flycheck mode and typescript --- elpa/tide-20220514.614/tide.el | 3138 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3138 insertions(+) create mode 100644 elpa/tide-20220514.614/tide.el (limited to 'elpa/tide-20220514.614/tide.el') diff --git a/elpa/tide-20220514.614/tide.el b/elpa/tide-20220514.614/tide.el new file mode 100644 index 0000000..588f6b8 --- /dev/null +++ b/elpa/tide-20220514.614/tide.el @@ -0,0 +1,3138 @@ +;;; 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 -- cgit v1.2.1