# 基础设施设计 — LSP 集成 ## 文档元数据 - 项目名称: free-code - 文档类型: 基础设施设计 - 原始代码来源: `../../src/services/lsp/`(7个文件) - 原始设计意图: 在 .NET 中封装语言服务器生命周期、文件同步与 9 种核心 LSP 操作,并支持按扩展名路由与诊断基线比较 - 交叉引用: [基础设施设计总览](基础设施设计.md) | [核心模块设计-工具系统](../核心模块设计/核心模块设计-工具系统.md) ## 设计目标 LSP 层用于承载代码补全、诊断、跳转与重命名等 IDE 能力,为编辑体验提供统一协议封装。它既要兼容多语言服务器,又要支持懒启动、文件同步和跨文件诊断聚合。 ## 11.1 ILspClientManager 接口定义 ```csharp /// /// LSP 客户端管理器 — 管理 LSP 服务器实例 /// 对应原始 LSPServerManager.ts /// public interface ILspClientManager { /// 初始化 (加载配置, 懒启动) Task InitializeAsync(CancellationToken ct = default); /// 关闭所有服务器 Task ShutdownAsync(); /// 获取文件对应的 LSP 服务器 ILspServerInstance? GetServerForFile(string filePath); /// 确保文件对应的 LSP 服务器已启动 Task EnsureServerStartedAsync(string filePath); /// 发送请求到文件对应的 LSP 服务器 Task SendRequestAsync(string filePath, string method, object? parameters); // LSP 操作 (对应原始 LSPTool 的 9 种操作) Task GoToDefinitionAsync(string filePath, int line, int character); Task FindReferencesAsync(string filePath, int line, int character); Task HoverAsync(string filePath, int line, int character); Task DocumentSymbolsAsync(string filePath); Task WorkspaceSymbolsAsync(string query); Task GetDiagnosticsAsync(string filePath); Task PrepareRenameAsync(string filePath, int line, int character); Task RenameAsync(string filePath, int line, int character, string newName); Task GetCodeActionsAsync(string filePath, int line, int character); // 文件同步 Task OpenFileAsync(string filePath, string content); Task ChangeFileAsync(string filePath, string content); Task SaveFileAsync(string filePath); Task CloseFileAsync(string filePath); /// 所有运行中的服务器 IReadOnlyDictionary GetAllServers(); /// 是否至少有一个服务器已连接 bool IsConnected { get; } } ``` ## 11.2 LspServerInstance 实现 ```csharp /// /// 单个 LSP 服务器实例 — 管理子进程生命周期 /// 对应原始 LSPServerInstance.ts /// public interface ILspServerInstance { string Name { get; } string Command { get; } IReadOnlyDictionary ExtensionToLanguage { get; } LspServerState State { get; } Task StartAsync(); Task StopAsync(); Task SendRequestAsync(string method, object? parameters); Task SendNotificationAsync(string method, object? parameters); void OnRequest(string method, Func handler); } public enum LspServerState { Stopped, Starting, Running, Error } /// /// LSP 服务器实例实现 /// public sealed class LspServerInstance : ILspServerInstance, IAsyncDisposable { private Process? _process; private JsonRpc? _rpc; // 使用 StreamJsonRpc 库 private readonly string _name; private readonly ScopedLspServerConfig _config; private readonly ILogger _logger; private readonly Dictionary> _requestHandlers = new(); public LspServerState State { get; private set; } = LspServerState.Stopped; public async Task StartAsync() { if (State == LspServerState.Running) return; State = LspServerState.Starting; var psi = new ProcessStartInfo { FileName = _config.Command, UseShellExecute = false, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, }; foreach (var arg in _config.Args) psi.ArgumentList.Add(arg); if (_config.Env != null) foreach (var (k, v) in _config.Env) psi.Environment[k] = v; _process = Process.Start(psi) ?? throw new InvalidOperationException($"Failed to start LSP server {_name}"); // 使用 StreamJsonRpc 建立 LSP 连接 _rpc = JsonRpc.Attach(_process.StandardInput.BaseStream, _process.StandardOutput.BaseStream); _rpc.Disconnected += (_, _) => State = LspServerState.Stopped; // 发送 initialize 请求 var initResult = await _rpc.InvokeAsync("initialize", new { processId = Environment.ProcessId, rootUri = PathToFileUri(Directory.GetCurrentDirectory()), capabilities = new { textDocument = new { definition = new { dynamicRegistration = false }, references = new { dynamicRegistration = false }, hover = new { dynamicRegistration = false, contentFormat = new[] { "markdown", "plaintext" } }, publishDiagnostics = new { relatedInformation = true }, rename = new { prepareSupport = true } } } }); // 发送 initialized 通知 await _rpc.NotifyAsync("initialized", new { }); State = LspServerState.Running; } public Task SendRequestAsync(string method, object? parameters) => _rpc!.InvokeAsync(method, parameters); public Task SendNotificationAsync(string method, object? parameters) => _rpc!.NotifyAsync(method, parameters); public async ValueTask DisposeAsync() { if (_rpc != null) { try { await _rpc.NotifyAsync("shutdown", null); } catch { } try { await _rpc.NotifyAsync("exit", null); } catch { } } if (_process?.HasExited == false) { _process.WaitForExit(5000); if (!_process.HasExited) _process.Kill(entireProcessTree: true); } State = LspServerState.Stopped; } } ``` ## 11.3 LspClientManager 实现 ```csharp public sealed class LspClientManager : ILspClientManager { private readonly ConcurrentDictionary _servers = new(); private readonly Dictionary> _extensionMap = new(); // ext → [serverNames] private readonly HashSet _openedFiles = new(); // URI 跟踪 private readonly ILogger _logger; public bool IsConnected => _servers.Values.Any(s => s.State == LspServerState.Running); /// /// 初始化: 加载 LSP 配置,构建扩展映射,不启动服务器(懒启动) /// 对应原始 initializeLspServerManager() /// public async Task InitializeAsync(CancellationToken ct = default) { var configs = await LoadAllLspServerConfigsAsync(); foreach (var (name, config) in configs) { if (string.IsNullOrEmpty(config.Command)) { _logger.LogWarning("LSP server {Name} missing command, skipping", name); continue; } // 构建扩展 → 服务器映射 foreach (var ext in config.ExtensionToLanguage.Keys) { var normalized = ext.ToLowerInvariant(); if (!_extensionMap.ContainsKey(normalized)) _extensionMap[normalized] = new(); _extensionMap[normalized].Add(name); } _servers[name] = new LspServerInstance(name, config, _logger); } } public ILspServerInstance? GetServerForFile(string filePath) { var ext = Path.GetExtension(filePath).ToLowerInvariant(); var serverNames = _extensionMap.GetValueOrDefault(ext); return serverNames?.Count > 0 ? _servers.GetValueOrDefault(serverNames[0]) : null; } public async Task EnsureServerStartedAsync(string filePath) { var server = GetServerForFile(filePath); if (server == null) return null; if (server.State is LspServerState.Stopped or LspServerState.Error) await server.StartAsync(); return server; } public async Task SendRequestAsync(string filePath, string method, object? parameters) { var server = await EnsureServerStartedAsync(filePath); if (server == null) return default; return await server.SendRequestAsync(method, parameters); } // === LSP 操作实现 === public async Task GoToDefinitionAsync(string filePath, int line, int character) => await SendRequestAsync(filePath, "textDocument/definition", new { textDocument = new { uri = PathToFileUri(filePath) }, position = new { line = line - 1, character } }); public async Task FindReferencesAsync(string filePath, int line, int character) => await SendRequestAsync(filePath, "textDocument/references", new { textDocument = new { uri = PathToFileUri(filePath) }, position = new { line = line - 1, character }, context = new { includeDeclaration = true } }) ?? []; public async Task HoverAsync(string filePath, int line, int character) => await SendRequestAsync(filePath, "textDocument/hover", new { textDocument = new { uri = PathToFileUri(filePath) }, position = new { line = line - 1, character } }); public async Task DocumentSymbolsAsync(string filePath) => await SendRequestAsync(filePath, "textDocument/documentSymbol", new { textDocument = new { uri = PathToFileUri(filePath) } }) ?? []; public async Task WorkspaceSymbolsAsync(string query) { // 遍历所有已启动的服务器 var results = new List(); foreach (var server in _servers.Values.Where(s => s.State == LspServerState.Running)) { try { var symbols = await server.SendRequestAsync( "workspace/symbol", new { query }); if (symbols != null) results.AddRange(symbols); } catch { /* 继续尝试其他服务器 */ } } return results.ToArray(); } // === 文件同步 === public async Task OpenFileAsync(string filePath, string content) { var server = await EnsureServerStartedAsync(filePath); if (server == null) return; var uri = PathToFileUri(filePath); if (_openedFiles.Contains(uri)) return; // 已打开 var ext = Path.GetExtension(filePath).ToLowerInvariant(); var languageId = server.ExtensionToLanguage.GetValueOrDefault(ext) ?? "plaintext"; await server.SendNotificationAsync("textDocument/didOpen", new { textDocument = new { uri, languageId, version = 1, text = content } }); _openedFiles.Add(uri); } public async Task ChangeFileAsync(string filePath, string content) { var server = GetServerForFile(filePath); if (server?.State != LspServerState.Running) return; // 先 OpenFile var uri = PathToFileUri(filePath); if (!_openedFiles.Contains(uri)) return; // 先 OpenFile await server.SendNotificationAsync("textDocument/didChange", new { textDocument = new { uri, version = 1 }, contentChanges = new[] { new { text = content } } }); } public async Task ShutdownAsync() { var tasks = _servers.Values .Where(s => s.State is LspServerState.Running or LspServerState.Error) .Select(s => s.DisposeAsync().AsTask()); await Task.WhenAll(tasks); _servers.Clear(); _extensionMap.Clear(); _openedFiles.Clear(); } private static string PathToFileUri(string path) => new Uri(Path.GetFullPath(path)).AbsoluteUri; } ``` ## 11.4 LspDiagnosticRegistry 基线比较 ```csharp /// /// LSP 诊断收集与基线对比 /// 对应原始 LSPDiagnosticRegistry.ts /// public sealed class LspDiagnosticRegistry { private readonly Dictionary> _baseline = new(); private readonly Dictionary> _current = new(); /// 保存当前诊断为基线(编辑前快照) public void SaveBaseline(string filePath) { if (_current.TryGetValue(filePath, out var diagnostics)) _baseline[filePath] = new(diagnostics); } /// 更新诊断(来自 textDocument/publishDiagnostics 通知) public void UpdateDiagnostics(string filePath, IReadOnlyList diagnostics) { _current[filePath] = diagnostics.ToList(); } /// 获取新增的诊断(相对于基线) public IReadOnlyList GetNewDiagnostics(string filePath) { var baseline = _baseline.GetValueOrDefault(filePath) ?? []; var current = _current.GetValueOrDefault(filePath) ?? []; var baselineSet = new HashSet(baseline); return current.Where(d => !baselineSet.Contains(d)).ToList(); } /// 清除文件基线 public void ClearBaseline(string filePath) => _baseline.Remove(filePath); } ```