14 KiB
14 KiB
基础设施设计 — LSP 集成
文档元数据
- 项目名称: free-code
- 文档类型: 基础设施设计
- 原始代码来源:
../../src/services/lsp/(7个文件) - 原始设计意图: 在 .NET 中封装语言服务器生命周期、文件同步与 9 种核心 LSP 操作,并支持按扩展名路由与诊断基线比较
- 交叉引用: 基础设施设计总览 | 核心模块设计-工具系统
设计目标
LSP 层用于承载代码补全、诊断、跳转与重命名等 IDE 能力,为编辑体验提供统一协议封装。它既要兼容多语言服务器,又要支持懒启动、文件同步和跨文件诊断聚合。
11.1 ILspClientManager 接口定义
/// <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 实现
/// <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 实现
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 基线比较
/// <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);
}