Tide: JavaScript智能补全插件

LSP (Language Server Protocol) 是微软领导开发的编程语言语法补全和代码分析框架, 好处是全世界黑客都一起开发 LSP 后端, 不论你用的是 Emacs, Vim 还是 Sublime, VSCode, Elicpse, IntelliJ 等编程工具, 都可以享受同等智能的语法补全后端.

Emacs 的 lsp-mode 是LSP协议在Emacs的客户端实现. lsp-mode现在能够很好的支持 C++, Python, Ruby, Golang, Haskell, OCamel, Rust, PHP等语言. 当然也包括 JavaScript.

lsp-mode 对 JavaScript 的默认补全主要靠 javascript-typescript-langserver 这个后端来实现.

tide 使用的是 tsserver, 也就是微软给 VSCode 开发的JavaScript智能补全后端, 可以很好的支持 JavaScript 和 TypeScript.

安装方法

1. 安装 tsserver 补全后端

npm install -g typescript
npm install jquery --save
  1. typescript 这个包会安装 tsserver
  2. jquery --save 这个包安装以后, tsserver 就不会抱怨找不到 JQuery 的 $ 符号了 :wink:

2. 安装 tide

GitHub - ananthakumaran/tide: Tide - TypeScript Interactive Development Environment for Emacs 下载源码后放到 Emacs 的加载目录后, 把下面代码写入 ~/.emacs 中:

(require 'tide)

(dolist (hook (list
               'js2-mode-hook
               'rjsx-mode-hook
               'typescript-mode-hook
               ))
  (add-hook hook (lambda ()
                   ;; 初始化 tide
                   (tide-setup)
                   ;; 当 tsserver 服务没有启动时自动重新启动
                   (unless (tide-current-server)
                     (tide-restart-server))
                   )))

体验

打开 *.js 文件后, tide 会自动把当前目录识别为 root 目录 (这对于大部分的 WebPack 项目来说已经是足够了的)

其他比较有用的命令:

  1. tide-jump-to-definition: 跳转到函数或变量定义的地方
  2. tide-jump-back: 跳转定义以后再跳转回来
  3. tide-rename-symbol: 语法重命名符号
  4. tide-rename-file: 重命名JS文件

也可以像这样用 tide-references 命令查找符号所有语法引用的地方.

更多命令请参考 Tide github

lsp-mode 使用 typescript-language-server 后端

前面说的 lsp-mode 默认使用 javascript-typescript-langserver 这个后端, 其实换成 typescript-language-server (tsserver 的封装) 也可以达到 tide 的同样效果, 类似的配置如下:

(require 'lsp-typescript)

;; Javascript, Typescript and Flow support for lsp-mode
;;
;; Install:
;;
;; npm install -g typescript
;; npm install -g typescript-language-server
;;
;; Fixed error "[tsserver] /bin/sh: /usr/local/Cellar/node/10.5.0_1/bin/npm: No such file or directory" :
;; 
;; sudo ln -s /usr/local/bin/npm /usr/local/Cellar/node/10.5.0_1/bin/npm
;;
(add-hook 'js-mode-hook #'lsp-typescript-enable)
(add-hook 'typescript-mode-hook #'lsp-typescript-enable) ;; for typescript support
(add-hook 'js3-mode-hook #'lsp-typescript-enable) ;; for js3-mode support
(add-hook 'rjsx-mode #'lsp-typescript-enable) ;; for rjsx-mode support

(defun lsp-company-transformer (candidates)
  (let ((completion-ignore-case t))
    (all-completions (company-grab-symbol) candidates)))

(defun lsp-js-hook nil
  (make-local-variable 'company-transformers)
  (push 'lsp-company-transformer company-transformers))

(add-hook 'js-mode-hook 'lsp-js-hook)

备注

请直接使用js-mode, 不要使用 js2-mode 或者基于 js2-mode 的插件(比如 rjsx-mode), js2-mode 会导致编辑 JS 文件时非常非常卡顿.

4 个赞

早就知道这个包了,但却没发现能给javascipt用,真的是太好用了。好奇怪为什么lsp-javascript不用tide。

;; Javascript, Typescript and Flow support for lsp-mode
;;
;; Install:
;;
;; npm install -g typescript
;; npm install -g typescript-language-server
;;
;; Fixed error "[tsserver] /bin/sh: /usr/local/Cellar/node/10.5.0_1/bin/npm: No such file or directory" :
;; 
;; sudo ln -s /usr/local/bin/npm /usr/local/Cellar/node/10.5.0_1/bin/npm
;;
(add-hook 'js-mode-hook #'lsp-typescript-enable)
(add-hook 'typescript-mode-hook #'lsp-typescript-enable) ;; for typescript support
(add-hook 'js3-mode-hook #'lsp-typescript-enable) ;; for js3-mode support
(add-hook 'rjsx-mode #'lsp-typescript-enable) ;; for rjsx-mode support

(defun lsp-company-transformer (candidates)
  (let ((completion-ignore-case t))
    (all-completions (company-grab-symbol) candidates)))

(defun lsp-js-hook nil
  (make-local-variable 'company-transformers)
  (push 'lsp-company-transformer company-transformers))

(add-hook 'js-mode-hook 'lsp-js-hook)

其实这样做, 就和 tide 实现的是一模一样的效果.

1 个赞

@MaskRay 不知道为啥 lsp-mode 还是 tide 在我机器上只要执行 kill-line 都会卡一下, 我设置

(setq lsp-print-io t) 

用于调试 language server 返回的消息, 发现 kill-line 的时候, language server 返回:

lsp--send-no-wait: Content-Length: 512

{
  "jsonrpc": "2.0",
  "method": "textDocument/didChange",
  "params": {
    "textDocument": {
      "uri": "file:///Users/andy/tower-ng/app/javascript/controllers/mission_controller.js",
      "version": 121
    },
    "contentChanges": [
      {
        "range": {
          "start": {
            "line": 19,
            "character": 1
          },
          "end": {
            "line": 19,
            "character": 12
          }
        },
        "rangeLength": 11,
        "text": ""
      }
    ]
  }
}
Output from language server: {
  "jsonrpc": "2.0",
  "method": "textDocument/publishDiagnostics",
  "params": {
    "uri": "file:///Users/andy/tower-ng/app/javascript/controllers/mission_controller.js",
    "diagnostics": [
      {
        "range": {
          "start": {
            "line": 247,
            "character": 34
          },
          "end": {
            "line": 247,
            "character": 40
          }
        },
        "message": "'result' is declared but its value is never read.",
        "severity": 4,
        "code": 6133,
        "source": "typescript"
      },
      {
        "range": {
          "start": {
            "line": 287,
            "character": 18
          },
          "end": {
            "line": 287,
            "character": 23
          }
        },
        "message": "'event' is declared but its value is never read.",
        "severity": 4,
        "code": 6133,
        "source": "typescript"
      },
      {
        "range": {
          "start": {
            "line": 297,
            "character": 23
          },
          "end": {
            "line": 297,
            "character": 29
          }
        },
        "message": "'result' is declared but its value is never read.",
        "severity": 4,
        "code": 6133,
        "source": "typescript"
      },
      {
        "range": {
          "start": {
            "line": 374,
            "character": 34
          },
          "end": {
            "line": 374,
            "character": 40
          }
        },
        "message": "'result' is declared but its value is never read.",
        "severity": 4,
        "code": 6133,
        "source": "typescript"
      },
      {
        "range": {
          "start": {
            "line": 479,
            "character": 34
          },
          "end": {
            "line": 479,
            "character": 40
          }
        },
        "message": "'result' is declared but its value is never read.",
        "severity": 4,
        "code": 6133,
        "source": "typescript"
      }
    ]
  }
}

Wrote /Users/andy/tower-ng/app/javascript/controllers/mission_controller.js
lsp--send-no-wait: Content-Length: 237

{
  "jsonrpc": "2.0",
  "method": "textDocument/didSave",
  "params": {
    "textDocument": {
      "uri": "file:///Users/andy/tower-ng/app/javascript/controllers/mission_controller.js",
      "version": 121
    },
    "text": null
  }
}
Mark set

@MaskRay 有没有改进卡顿的建议 ?

1 个赞

试过

  • Tide
  • Eglot (javascript-typescript-stdio)
  • Tern (目前在用这个)

我在研究 lsp-mode 的代码, 发现只要 LSP 服务器 (JS 对应的是 typesciprt) 返回 textDocument/publishDiagnostics 消息, 就会导致 Emacs 卡住.

这里有一个类似的 issue.

我看看我能否把这个问题修复好.

@xuchunyang

tern 不好用, 其实问题不出在 lsp client, 而是要用 typescript-language-server 替换 javascript-typescript-language-server 这个后端.

typescript-language-server 这个后端是包装的 tsserver (VSCode 的后端), 补全要智能的多.

之前看到有人说这个langserver卡,不知道你是用下来感觉跟tide相比,速度怎么样?

我之前也是觉得 tide 不卡, 但是经过反复测试, 我觉得 bug 在更底层的位置.

现在只要 server 返回 textDocument/publishDiagnostics 信息, tide 和 lsp-mode 都卡.

试了下这个lsp-typescript,果然还是不太成熟啊,得强行ignore-errors,不然没法玩。。。

lsp-typescript 不会报错啊, 我估计是你的 lsp 配置有问题:

(setq lsp-enable-eldoc nil) ;we will got error "Wrong type argument: sequencep" from `eldoc-message' if `lsp-enable-eldoc' is non-nil
(setq lsp-message-project-root-warning t) ;avoid popup warning buffer if lsp can't found root directory (such as edit simple *.py file)
(setq create-lockfiles nil) ;we will got error "Error from the Language Server: FileNotFoundError" if `create-lockfiles' is non-nil

试一下上面的代码, 这三行代码专治 lsp 启动就挂的问题.

2 个赞

之前试的时候发现在{}还是<>里面编辑attribute的时候,补全会报null content的错,然后我就 revert了我的setting。

话说大神怎么开始写js了?

emacs -Q 试一下

研究了1个小时的 lsp-mode 代码, 确定是 typescript-language-server 的问题了, 报了个 issue : How to disable textDocument/publishDiagnostics message from tsserver? · Issue #76 · typescript-language-server/typescript-language-server · GitHub

发现lsp-javascript-typescript只要加了jsconfig.json也可以实现比较完美的跳转之类的lol

typescript-language-server 的跳转更好, 建议用 lsp-typescript

如果你要用 lsp-javascript-typescript, 建议结合 xref-js2 , lsp-javascript-typescript 跨文件跳转不准, 一般只能跳到 Import的位置.

lsp-typescript 或者 xref-js2 都能直接跳转到源文件的定义处.

可我加了jsconfig.json后,连import React, { Component } from 'react';都能跳转到准确到源文件位置了诶。感觉挺准的啊。

在另外一台电脑上试了下,发现又不行了。。。看来确实不行。

最开始用的 tern-mode ,发现这个时不时有问题,后面看到了 tide,目前就一直用 tide 了,用着还不错。tide + web-mode。

你的tide不卡吗?我打几个字符就会卡一下

和项目大小有关系的吧

得有一个标准的项目才能测试