free-code-dotnet/docs/基础设施设计/基础设施设计-LSP集成.md
应文浩wenhao.ying@xiaobao100.com e25ac591a7 init easy-code
2026-04-06 07:24:24 +08:00

369 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 基础设施设计 — LSP 集成
## 文档元数据
- 项目名称: free-code
- 文档类型: 基础设施设计
- 原始代码来源: `../../src/services/lsp/`7个文件
- 原始设计意图: 在 .NET 中封装语言服务器生命周期、文件同步与 9 种核心 LSP 操作,并支持按扩展名路由与诊断基线比较
- 交叉引用: [基础设施设计总览](基础设施设计.md) | [核心模块设计-工具系统](../核心模块设计/核心模块设计-工具系统.md)
## 设计目标
LSP 层用于承载代码补全、诊断、跳转与重命名等 IDE 能力,为编辑体验提供统一协议封装。它既要兼容多语言服务器,又要支持懒启动、文件同步和跨文件诊断聚合。
## 11.1 ILspClientManager 接口定义
```csharp
/// <summary>
/// LSP 客户端管理器 — 管理 LSP 服务器实例
/// 对应原始 LSPServerManager.ts
/// </summary>
public interface ILspClientManager
{
/// <summary>初始化 (加载配置, 懒启动)</summary>
Task InitializeAsync(CancellationToken ct = default);
/// <summary>关闭所有服务器</summary>
Task ShutdownAsync();
/// <summary>获取文件对应的 LSP 服务器</summary>
ILspServerInstance? GetServerForFile(string filePath);
/// <summary>确保文件对应的 LSP 服务器已启动</summary>
Task<ILspServerInstance?> EnsureServerStartedAsync(string filePath);
/// <summary>发送请求到文件对应的 LSP 服务器</summary>
Task<T?> SendRequestAsync<T>(string filePath, string method, object? parameters);
// LSP 操作 (对应原始 LSPTool 的 9 种操作)
Task<Location?> GoToDefinitionAsync(string filePath, int line, int character);
Task<Location[]> FindReferencesAsync(string filePath, int line, int character);
Task<Hover?> HoverAsync(string filePath, int line, int character);
Task<Symbol[]> DocumentSymbolsAsync(string filePath);
Task<Symbol[]> WorkspaceSymbolsAsync(string query);
Task<Diagnostic[]> GetDiagnosticsAsync(string filePath);
Task<PrepareRenameResult?> PrepareRenameAsync(string filePath, int line, int character);
Task<WorkspaceEdit?> RenameAsync(string filePath, int line, int character, string newName);
Task<CodeAction[]> 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);
/// <summary>所有运行中的服务器</summary>
IReadOnlyDictionary<string, ILspServerInstance> GetAllServers();
/// <summary>是否至少有一个服务器已连接</summary>
bool IsConnected { get; }
}
```
## 11.2 LspServerInstance 实现
```csharp
/// <summary>
/// 单个 LSP 服务器实例 — 管理子进程生命周期
/// 对应原始 LSPServerInstance.ts
/// </summary>
public interface ILspServerInstance
{
string Name { get; }
string Command { get; }
IReadOnlyDictionary<string, string> ExtensionToLanguage { get; }
LspServerState State { get; }
Task StartAsync();
Task StopAsync();
Task<T> SendRequestAsync<T>(string method, object? parameters);
Task SendNotificationAsync(string method, object? parameters);
void OnRequest(string method, Func<object, object?> handler);
}
public enum LspServerState { Stopped, Starting, Running, Error }
/// <summary>
/// LSP 服务器实例实现
/// </summary>
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<string, Func<object, object?>> _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<LspInitializeResult>("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<T> SendRequestAsync<T>(string method, object? parameters)
=> _rpc!.InvokeAsync<T>(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<string, ILspServerInstance> _servers = new();
private readonly Dictionary<string, List<string>> _extensionMap = new(); // ext → [serverNames]
private readonly HashSet<string> _openedFiles = new(); // URI 跟踪
private readonly ILogger<LspClientManager> _logger;
public bool IsConnected => _servers.Values.Any(s => s.State == LspServerState.Running);
/// <summary>
/// 初始化: 加载 LSP 配置,构建扩展映射,不启动服务器(懒启动)
/// 对应原始 initializeLspServerManager()
/// </summary>
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<ILspServerInstance?> 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<T?> SendRequestAsync<T>(string filePath, string method, object? parameters)
{
var server = await EnsureServerStartedAsync(filePath);
if (server == null) return default;
return await server.SendRequestAsync<T>(method, parameters);
}
// === LSP 操作实现 ===
public async Task<Location?> GoToDefinitionAsync(string filePath, int line, int character)
=> await SendRequestAsync<Location?>(filePath, "textDocument/definition",
new { textDocument = new { uri = PathToFileUri(filePath) },
position = new { line = line - 1, character } });
public async Task<Location[]> FindReferencesAsync(string filePath, int line, int character)
=> await SendRequestAsync<Location[]>(filePath, "textDocument/references",
new { textDocument = new { uri = PathToFileUri(filePath) },
position = new { line = line - 1, character },
context = new { includeDeclaration = true } }) ?? [];
public async Task<Hover?> HoverAsync(string filePath, int line, int character)
=> await SendRequestAsync<Hover?>(filePath, "textDocument/hover",
new { textDocument = new { uri = PathToFileUri(filePath) },
position = new { line = line - 1, character } });
public async Task<Symbol[]> DocumentSymbolsAsync(string filePath)
=> await SendRequestAsync<Symbol[]>(filePath, "textDocument/documentSymbol",
new { textDocument = new { uri = PathToFileUri(filePath) } }) ?? [];
public async Task<Symbol[]> WorkspaceSymbolsAsync(string query)
{
// 遍历所有已启动的服务器
var results = new List<Symbol>();
foreach (var server in _servers.Values.Where(s => s.State == LspServerState.Running))
{
try
{
var symbols = await server.SendRequestAsync<Symbol[]>(
"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
/// <summary>
/// LSP 诊断收集与基线对比
/// 对应原始 LSPDiagnosticRegistry.ts
/// </summary>
public sealed class LspDiagnosticRegistry
{
private readonly Dictionary<string, List<Diagnostic>> _baseline = new();
private readonly Dictionary<string, List<Diagnostic>> _current = new();
/// <summary>保存当前诊断为基线(编辑前快照)</summary>
public void SaveBaseline(string filePath)
{
if (_current.TryGetValue(filePath, out var diagnostics))
_baseline[filePath] = new(diagnostics);
}
/// <summary>更新诊断(来自 textDocument/publishDiagnostics 通知)</summary>
public void UpdateDiagnostics(string filePath, IReadOnlyList<Diagnostic> diagnostics)
{
_current[filePath] = diagnostics.ToList();
}
/// <summary>获取新增的诊断(相对于基线)</summary>
public IReadOnlyList<Diagnostic> GetNewDiagnostics(string filePath)
{
var baseline = _baseline.GetValueOrDefault(filePath) ?? [];
var current = _current.GetValueOrDefault(filePath) ?? [];
var baselineSet = new HashSet<Diagnostic>(baseline);
return current.Where(d => !baselineSet.Contains(d)).ToList();
}
/// <summary>清除文件基线</summary>
public void ClearBaseline(string filePath) => _baseline.Remove(filePath);
}
```