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

14 KiB
Raw Blame History

基础设施设计 — 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);
}