LSP 开发指南 / 第 7 章:工作区管理
7.1 工作区概述
工作区(Workspace)是 LSP Server 运行的上下文环境,通常对应一个项目目录。工作区管理涉及三大核心功能:
| 功能 | 说明 |
|---|---|
| 配置管理 | 读取/监听编辑器和项目配置 |
| 文件事件 | 监听文件创建、修改、删除 |
| 工作区符号 | 全局符号搜索 |
7.2 工作区文件夹
7.2.1 初始化时的工作区信息
Client 在 initialize 请求中提供工作区信息:
{
"params": {
"rootUri": "file:///home/user/my-project",
"workspaceFolders": [
{
"uri": "file:///home/user/my-project",
"name": "my-project"
},
{
"uri": "file:///home/user/my-project/packages/core",
"name": "core"
}
]
}
}
7.2.2 动态变更工作区文件夹
Client 可以在运行时添加/移除工作区文件夹:
// Server 监听工作区文件夹变更
connection.onNotification("workspace/didChangeWorkspaceFolders", (params) => {
for (const folder of params.event.added) {
console.error(`Workspace added: ${folder.name} (${folder.uri})`);
indexWorkspace(folder.uri);
}
for (const folder of params.event.removed) {
console.error(`Workspace removed: ${folder.name} (${folder.uri})`);
removeWorkspaceIndex(folder.uri);
}
});
Server 声明支持工作区文件夹变更:
{
"capabilities": {
"workspace": {
"workspaceFolders": {
"supported": true,
"changeNotifications": true
}
}
}
}
7.3 配置管理
7.3.1 配置读取
Server 通过 workspace/configuration 请求读取配置:
// Server 请求配置
const configs = await connection.sendRequest("workspace/configuration", {
items: [
{
section: "myLsp", // 配置节
},
{
section: "myLsp.maxLineLength",
},
{
scopeUri: "file:///src/main.py",
section: "myLsp.pythonPath",
},
],
});
// configs = [全局配置, maxLineLength值, 特定文件的pythonPath]
7.3.2 配置变更监听
// Server 声明支持配置变更
capabilities: {
workspace: {
didChangeConfiguration: {
dynamicRegistration: true,
},
},
}
// Server 监听配置变更
connection.onNotification("workspace/didChangeConfiguration", (params) => {
// params.settings 可能包含新配置(取决于 Client 实现)
reloadConfiguration();
});
// 更可靠的方式:主动重新请求配置
connection.onNotification("workspace/didChangeConfiguration", async () => {
const newConfig = await connection.sendRequest("workspace/configuration", {
items: [{ section: "myLsp" }],
});
applyConfiguration(newConfig[0]);
});
7.3.3 配置结构设计
interface ServerConfiguration {
/** 最大行长度 */
maxLineLength: number;
/** 分析器设置 */
analysis: {
/** 是否启用类型检查 */
enableTypeChecking: boolean;
/** 最大分析深度 */
maxDepth: number;
};
/** 诊断设置 */
diagnostics: {
/** 启用的规则列表 */
enabledRules: string[];
/** 禁用的规则列表 */
disabledRules: string[];
};
}
const defaultConfig: ServerConfiguration = {
maxLineLength: 120,
analysis: {
enableTypeChecking: true,
maxDepth: 10,
},
diagnostics: {
enabledRules: [],
disabledRules: [],
},
};
async function getConfiguration(): Promise<ServerConfiguration> {
try {
const config = await connection.sendRequest("workspace/configuration", {
items: [{ section: "myLsp" }],
});
return { ...defaultConfig, ...config[0] };
} catch {
return defaultConfig;
}
}
7.3.4 在编辑器中注册配置
VS Code 的 package.json 配置声明:
{
"contributes": {
"configuration": {
"title": "My LSP Server",
"properties": {
"myLsp.maxLineLength": {
"type": "number",
"default": 120,
"description": "Maximum line length"
},
"myLsp.analysis.enableTypeChecking": {
"type": "boolean",
"default": true,
"description": "Enable type checking"
}
}
}
}
}
7.4 文件事件
7.4.1 文件观察器
Server 可以注册文件观察器,监听工作区内文件变更:
// 动态注册文件观察器
connection.sendRequest("client/registerCapability", {
registrations: [
{
id: "file-watcher-config",
method: "workspace/didChangeWatchedFiles",
registerOptions: {
watchers: [
{
globPattern: "**/*.py", // Python 文件
},
{
globPattern: "**/pyproject.toml", // 项目配置文件
},
{
globPattern: "**/.mylsp*", // 自定义配置
},
],
},
},
],
});
7.4.2 文件变更通知
当工作区内的文件发生变更时,Client 发送通知:
{
"jsonrpc": "2.0",
"method": "workspace/didChangeWatchedFiles",
"params": {
"changes": [
{
"uri": "file:///home/user/project/src/utils.py",
"type": 2
},
{
"uri": "file:///home/user/project/src/new_file.py",
"type": 1
},
{
"uri": "file:///home/user/project/src/old_file.py",
"type": 3
}
]
}
}
文件变更类型:
| 类型 | 值 | 说明 |
|---|---|---|
| Created | 1 | 文件创建 |
| Changed | 2 | 文件修改 |
| Deleted | 3 | 文件删除 |
7.4.3 Server 端处理
connection.onNotification("workspace/didChangeWatchedFiles", (params) => {
for (const change of params.changes) {
switch (change.type) {
case FileChangeType.Created:
handleFileCreated(change.uri);
break;
case FileChangeType.Changed:
handleFileChanged(change.uri);
break;
case FileChangeType.Deleted:
handleFileDeleted(change.uri);
break;
}
}
});
function handleFileChanged(uri: string): void {
// 重新索引该文件的符号
reindexFile(uri);
// 重新发布该文件的诊断
diagnostics.publishDiagnostics(uri);
}
function handleFileDeleted(uri: string): void {
// 清理缓存
symbolIndex.removeFile(uri);
// 清除诊断
diagnostics.clearDiagnostics(uri);
}
function handleFileCreated(uri: string): void {
// 新建索引
indexFile(uri);
}
7.5 工作区编辑
7.5.1 applyEdit 请求
Server 可以请求 Client 执行工作区编辑:
// Server 请求 Client 执行工作区编辑
connection.sendRequest("workspace/applyEdit", {
edit: {
documentChanges: [
{
textDocument: { uri: "file:///src/main.py", version: null },
edits: [
{
range: {
start: { line: 0, character: 0 },
end: { line: 0, character: 0 },
},
newText: "import json\n",
},
],
},
],
},
});
7.5.2 WorkspaceEdit 类型
interface WorkspaceEdit {
// 方式 1:按 URI 分组的编辑
changes?: { [uri: string]: TextEdit[] };
// 方式 2:文档变更列表(推荐,支持版本控制)
documentChanges?: (
| TextDocumentEdit
| CreateFile
| RenameFile
| DeleteFile
)[];
}
// 创建文件
interface CreateFile {
kind: "create";
uri: DocumentUri;
options?: { overwrite?: boolean; ignoreIfExists?: boolean };
}
// 重命名文件
interface RenameFile {
kind: "rename";
oldUri: DocumentUri;
newUri: DocumentUri;
options?: { overwrite?: boolean; ignoreIfExists?: boolean };
}
// 删除文件
interface DeleteFile {
kind: "delete";
uri: DocumentUri;
options?: { recursive?: boolean; ignoreIfNotExists?: boolean };
}
7.6 工作区符号
7.6.1 工作区符号搜索
Client 发送 workspace/symbol 请求搜索全局符号:
{
"jsonrpc": "2.0",
"id": 20,
"method": "workspace/symbol",
"params": {
"query": "UserService"
}
}
Server 响应:
{
"jsonrpc": "2.0",
"id": 20,
"result": [
{
"name": "UserService",
"kind": 5,
"location": {
"uri": "file:///src/services/user.ts",
"range": {
"start": { "line": 12, "character": 0 },
"end": { "line": 12, "character": 40 }
}
},
"containerName": "services"
},
{
"name": "UserServiceError",
"kind": 12,
"location": {
"uri": "file:///src/errors.ts",
"range": {
"start": { "line": 45, "character": 0 },
"end": { "line": 45, "character": 30 }
}
},
"containerName": "errors"
}
]
}
7.6.2 符号索引实现
interface SymbolEntry {
name: string;
kind: SymbolKind;
uri: string;
range: Range;
containerName?: string;
}
class SymbolIndex {
private symbols: SymbolEntry[] = [];
private fileSymbols = new Map<string, SymbolEntry[]>();
// 索引一个文件的符号
indexFile(uri: string, text: string): void {
// 移除旧索引
this.removeFile(uri);
const lines = text.split("\n");
const newSymbols: SymbolEntry[] = [];
let currentContainer = "";
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 类定义
const classMatch = line.match(/^class\s+(\w+)/);
if (classMatch) {
const sym: SymbolEntry = {
name: classMatch[1],
kind: SymbolKind.Class,
uri,
range: { start: { line: i, character: 0 }, end: { line: i, character: line.length } },
};
newSymbols.push(sym);
currentContainer = classMatch[1];
continue;
}
// 函数/方法定义
const funcMatch = line.match(/^(?:async\s+)?(?:def|function)\s+(\w+)/);
if (funcMatch) {
newSymbols.push({
name: funcMatch[1],
kind: SymbolKind.Function,
uri,
range: { start: { line: i, character: 0 }, end: { line: i, character: line.length } },
containerName: currentContainer || undefined,
});
continue;
}
}
this.fileSymbols.set(uri, newSymbols);
this.symbols = [...this.symbols, ...newSymbols];
}
removeFile(uri: string): void {
const oldSymbols = this.fileSymbols.get(uri);
if (oldSymbols) {
this.symbols = this.symbols.filter((s) => !oldSymbols.includes(s));
this.fileSymbols.delete(uri);
}
}
search(query: string): SymbolEntry[] {
const lower = query.toLowerCase();
return this.symbols.filter((s) =>
s.name.toLowerCase().includes(lower)
);
}
}
// 在 Server 中使用
const symbolIndex = new SymbolIndex();
connection.onRequest("workspace/symbol", (params) => {
return symbolIndex.search(params.query).map((entry) => ({
name: entry.name,
kind: entry.kind,
location: { uri: entry.uri, range: entry.range },
containerName: entry.containerName,
}));
});
7.7 工作区文件操作
7.7.1 文件操作注册
Server 可以在文件重命名/创建/删除时自动调整 import 路径:
capabilities: {
workspace: {
fileOperations: {
willRename: {
filters: [
{ pattern: { glob: "**/*.py" } },
{ pattern: { glob: "**/*.ts" } },
],
},
didRename: {
filters: [
{ pattern: { glob: "**/*.py" } },
{ pattern: { glob: "**/*.ts" } },
],
},
},
},
}
7.7.2 willRename 处理
connection.onRequest("workspace/willRenameFiles", async (params) => {
const edits: WorkspaceEdit = { documentChanges: [] };
for (const fileOp of params.files) {
// 更新所有引用该文件的 import 语句
const affectedFiles = findFilesImporting(fileOp.oldUri);
for (const affectedUri of affectedFiles) {
const doc = docManager.get(affectedUri);
if (!doc) continue;
const importEdits = updateImportPaths(doc, fileOp.oldUri, fileOp.newUri);
if (importEdits.length > 0) {
edits.documentChanges!.push({
textDocument: { uri: affectedUri, version: doc.version },
edits: importEdits,
});
}
}
}
return edits;
});
⚠️ 注意事项
| 问题 | 建议 |
|---|---|
| 配置读取失败 | 始终提供默认配置作为 fallback |
| 文件观察器过多 | 只注册必要的 glob pattern |
| 符号索引内存 | 大项目应使用增量索引 |
| 工作区多根目录 | 检查 workspaceFolders 是否为空 |
🔗 扩展阅读
下一章:第 8 章:代码动作 — Quick Fix、重构、Code Lens。