强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

LSP 开发指南 / 第 11 章:编辑器集成

11.1 编辑器集成概览

将 Language Server 接入编辑器是 LSP 工作流的最终环节。不同编辑器的 LSP 客户端实现各异,但核心配置逻辑相同:

编辑器 LSP 客户端  ←──JSON-RPC──→  Language Server 进程
      │                                    │
  配置项:                              启动参数:
  - 如何启动 Server                    - stdio / TCP / WebSocket
  - 文件关联                           - 工作区路径
  - 快捷键映射                         - 初始化选项

11.2 VS Code 集成

11.2.1 使用 vscode-languageclient

VS Code 扩展是集成 LSP 最成熟的方式:

mkdir my-lsp-extension && cd my-lsp-extension
npm init -y
npm install vscode-languageclient
npm install -D @types/vscode

client.ts

import * as path from "path";
import {
  workspace,
  ExtensionContext,
  languages,
} from "vscode";
import {
  LanguageClient,
  LanguageClientOptions,
  ServerOptions,
  TransportKind,
} from "vscode-languageclient/node";

let client: LanguageClient;

export function activate(context: ExtensionContext) {
  // Server 路径
  const serverModule = context.asAbsolutePath(
    path.join("dist", "server.js")
  );

  // Server 选项
  const serverOptions: ServerOptions = {
    run: {
      module: serverModule,
      transport: TransportKind.ipc,
    },
    debug: {
      module: serverModule,
      transport: TransportKind.ipc,
      options: {
        execArgv: ["--nolazy", "--inspect=6009"],
      },
    },
  };

  // Client 选项
  const clientOptions: LanguageClientOptions = {
    documentSelector: [
      { scheme: "file", language: "python" },
      { scheme: "file", language: "javascript" },
    ],
    synchronize: {
      fileEvents: workspace.createFileSystemWatcher("**/*.{py,js}"),
    },
    initializationOptions: {
      maxNumberOfProblems: 100,
      enableTypeChecking: true,
    },
  };

  client = new LanguageClient(
    "myLspClient",
    "My LSP Server",
    serverOptions,
    clientOptions
  );

  client.start();
}

export function deactivate(): Thenable<void> | undefined {
  if (!client) return undefined;
  return client.stop();
}

package.json(扩展清单)

{
  "name": "my-lsp-extension",
  "displayName": "My LSP Extension",
  "version": "1.0.0",
  "engines": { "vscode": "^1.75.0" },
  "main": "./dist/client.js",
  "activationEvents": [
    "onLanguage:python",
    "onLanguage:javascript"
  ],
  "contributes": {
    "configuration": {
      "title": "My LSP",
      "properties": {
        "myLsp.maxNumberOfProblems": {
          "type": "number",
          "default": 100,
          "description": "Maximum number of diagnostics"
        },
        "myLsp.enableTypeChecking": {
          "type": "boolean",
          "default": true,
          "description": "Enable type checking"
        }
      }
    }
  }
}

11.2.2 VS Code 配置常用 LSP Server

// settings.json
{
  // Python - pylsp
  "python.languageServer": "Pylance",

  // TypeScript - 内置
  "typescript.server": "typescript-language-server",

  // Go - gopls
  "go.useLanguageServer": true,
  "gopls": {
    "ui.semanticTokens": true
  }
}

11.3 Neovim 集成

11.3.1 内置 LSP(nvim-lspconfig)

Neovim 0.5+ 内置 LSP 客户端,配合 nvim-lspconfig 插件使用:

安装插件(使用 lazy.nvim):

-- ~/.config/nvim/lua/plugins/lsp.lua
return {
  {
    "neovim/nvim-lspconfig",
    dependencies = {
      "williamboman/mason.nvim",
      "williamboman/mason-lspconfig.nvim",
    },
    config = function()
      require("mason").setup()
      require("mason-lspconfig").setup({
        ensure_installed = {
          "pyright",
          "ts_ls",
          "gopls",
          "lua_ls",
          "rust_analyzer",
        },
      })

      local lspconfig = require("lspconfig")
      local capabilities = require("cmp_nvim_lsp").default_capabilities()

      -- Python
      lspconfig.pyright.setup({
        capabilities = capabilities,
        settings = {
          python = {
            analysis = {
              typeCheckingMode = "basic",
              autoSearchPaths = true,
            },
          },
        },
      })

      -- TypeScript
      lspconfig.ts_ls.setup({
        capabilities = capabilities,
      })

      -- Go
      lspconfig.gopls.setup({
        capabilities = capabilities,
        settings = {
          gopls = {
            analyses = {
              unusedparams = true,
              shadow = true,
            },
            staticcheck = true,
          },
        },
      })

      -- Rust
      lspconfig.rust_analyzer.setup({
        capabilities = capabilities,
        settings = {
          ["rust-analyzer"] = {
            checkOnSave = {
              command = "clippy",
            },
          },
        },
      })
    end,
  },
}

11.3.2 自定义 LSP Server

-- 注册自定义 Language Server
local lspconfig = require("lspconfig")
local configs = require("lspconfig.configs")

if not configs.my_lsp then
  configs.my_lsp = {
    default_config = {
      cmd = { "node", "/path/to/my-lsp-server/dist/server.js", "--stdio" },
      filetypes = { "python", "javascript" },
      root_dir = lspconfig.util.root_pattern(".git", "pyproject.toml", "package.json"),
      settings = {
        myLsp = {
          maxNumberOfProblems = 100,
        },
      },
    },
  }
end

lspconfig.my_lsp.setup({
  capabilities = require("cmp_nvim_lsp").default_capabilities(),
  on_attach = function(client, bufnr)
    local opts = { buffer = bufnr }

    -- 快捷键映射
    vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts)
    vim.keymap.set("n", "gr", vim.lsp.buf.references, opts)
    vim.keymap.set("n", "K", vim.lsp.buf.hover, opts)
    vim.keymap.set("n", "<leader>rn", vim.lsp.buf.rename, opts)
    vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, opts)
    vim.keymap.set("n", "[d", vim.diagnostic.goto_prev, opts)
    vim.keymap.set("n", "]d", vim.diagnostic.goto_next, opts)
    vim.keymap.set("n", "<leader>f", function()
      vim.lsp.buf.format({ async = true })
    end, opts)
  end,
})

11.3.3 诊断配置

-- 诊断显示配置
vim.diagnostic.config({
  virtual_text = {
    prefix = "●",
    severity_sort = true,
  },
  signs = true,
  underline = true,
  update_in_insert = false,
  severity_sort = true,
  float = {
    border = "rounded",
    source = "always",
    header = "",
    prefix = "",
  },
})

-- 诊断符号
local signs = {
  Error = " ",
  Warn = " ",
  Hint = " ",
  Info = " ",
}
for type, icon in pairs(signs) do
  local hl = "DiagnosticSign" .. type
  vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = hl })
end

11.3.4 Telescope LSP 集成

-- 使用 Telescope 浏览 LSP 结果
vim.keymap.set("n", "gr", "<cmd>Telescope lsp_references<CR>")
vim.keymap.set("n", "gd", "<cmd>Telescope lsp_definitions<CR>")
vim.keymap.set("n", "gi", "<cmd>Telescope lsp_implementations<CR>")
vim.keymap.set("n", "gs", "<cmd>Telescope lsp_document_symbols<CR>")
vim.keymap.set("n", "gS", "<cmd>Telescope lsp_workspace_symbols<CR>")

11.4 Emacs 集成

11.4.1 Eglot(内置,Emacs 29+)

Eglot 是 Emacs 29+ 内置的 LSP 客户端:

;; ~/.emacs.d/init.el

;; 启用 eglot
(require 'eglot)

;; 注册自定义 Server
(add-to-list 'eglot-server-programs
             '((python-mode) "my-lsp-server" "--stdio"))

;; 自动启动
(add-hook 'python-mode-hook 'eglot-ensure)
(add-hook 'typescript-mode-hook 'eglot-ensure)
(add-hook 'go-mode-hook 'eglot-ensure)

;; 配置项
(setq eglot-autoshutdown t)        ;; 关闭文件时自动关闭 Server
(setq eglot-events-buffer-size 0)  ;; 禁用事件日志(性能)
(setq eglot-send-changes-idle-time 0.5)  ;; 变更延迟

;; 快捷键
(define-key eglot-mode-map (kbd "C-c l r") 'eglot-rename)
(define-key eglot-mode-map (kbd "C-c l f") 'eglot-format)
(define-key eglot-mode-map (kbd "C-c l a") 'eglot-code-actions)
(define-key eglot-mode-map (kbd "C-c l h") 'eglot-hover)
(define-key eglot-mode-map (kbd "C-c l d") 'eglot-find-declaration)

11.4.2 lsp-mode(第三方插件,功能更丰富)

;; 安装 lsp-mode
(use-package lsp-mode
  :ensure t
  :hook ((python-mode . lsp-deferred)
         (typescript-mode . lsp-deferred)
         (go-mode . lsp-deferred))
  :commands (lsp lsp-deferred)
  :config
  (setq lsp-idle-delay 0.5)
  (setq lsp-log-io nil)
  (setq lsp-keymap-prefix "C-c l"))

;; UI 增强
(use-package lsp-ui
  :ensure t
  :after lsp-mode
  :config
  (setq lsp-ui-doc-enable t)
  (setq lsp-ui-doc-position 'at-point)
  (setq lsp-ui-sideline-enable t)
  (setq lsp-ui-sideline-show-diagnostics t)
  (setq lsp-ui-sideline-show-hover nil))

;; 补全
(use-package company
  :ensure t
  :hook (after-init . global-company-mode)
  :config
  (setq company-minimum-prefix-length 1)
  (setq company-idle-delay 0.1))

;; 使用 consult-lsp 浏览结果
(use-package consult-lsp
  :ensure t
  :after lsp-mode)

11.5 Helix 编辑器

Helix 内置 LSP 支持,配置简单:

# ~/.config/helix/languages.toml

# Python
[[language]]
name = "python"
language-servers = ["pyright"]

[language-server.pyright]
command = "pyright-langserver"
args = ["--stdio"]

# 自定义 Server
[[language]]
name = "python"
language-servers = ["my-lsp"]

[language-server.my-lsp]
command = "my-lsp-server"
args = ["--stdio"]

[language-server.my-lsp.config]
maxNumberOfProblems = 100

11.6 编辑器集成对比

特性VS CodeNeovimEmacsHelix
LSP 客户端内置内置 (0.5+)eglot (29+)内置
配置方式JSON/LuaLuaElispTOML
自动安装扩展管理器mason.nvimlsp-install手动
语义高亮✅ (Tree-sitter)✅ (Tree-sitter)
Code Lens
学习曲线中高

⚠️ 常见问题

问题解决方案
Server 启动失败检查 PATH 环境变量和 Node/Python 版本
无诊断信息确认 didOpen 已发送且 Server 正常运行
补全无响应检查 triggerCharacters 配置
格式化不生效确认 formatOnSave 已启用
根目录检测失败配置 root_dir 检测规则(如 .gitpyproject.toml

🔗 扩展阅读


下一章第 12 章:测试策略 — 协议测试、模拟客户端、集成测试。