feat: enhance API and session management with Nacos and Redis integration
- Add Nacos registry for service registration and deregistration. - Implement Redis registry for session management with heartbeat and session claiming. - Improve completion service with session handling and request validation. - Enhance WebSocket handling for completion requests with JSON-RPC support. - Add tests for new registry implementations and completion manager functionalities. - Refactor existing code for better readability and maintainability.
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
||||
|
||||
var errClientClosed = errors.New("lsp client closed")
|
||||
|
||||
// Client 封装与 Language Server 的 JSON-RPC/LSP 通信。
|
||||
type Client struct {
|
||||
cmd *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
@@ -49,12 +50,14 @@ type rpcResponse struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// rpcError 对应 JSON-RPC 错误对象。
|
||||
type rpcError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// incomingEnvelope 表示从服务端读到的响应/通知外层结构。
|
||||
type incomingEnvelope struct {
|
||||
ID *json.RawMessage `json:"id,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
@@ -76,14 +79,16 @@ type lspCompletionList struct {
|
||||
Items []lspCompletionItem `json:"items"`
|
||||
}
|
||||
|
||||
// Config 定义 LSP 子进程启动参数及客户端标识。
|
||||
type Config struct {
|
||||
Command string
|
||||
Args []string
|
||||
RootPath string
|
||||
LanguageID string
|
||||
ClientName string
|
||||
Command string // LSP 可执行命令。
|
||||
Args []string // 启动参数(通常包含 --stdio)。
|
||||
RootPath string // 工作区根路径。
|
||||
LanguageID string // didOpen/didChange 使用的 languageId。
|
||||
ClientName string // initialize.clientInfo.name。
|
||||
}
|
||||
|
||||
// NewClient 启动语言服务器进程并完成 initialize 握手。
|
||||
func NewClient(parent context.Context, cfg Config) (*Client, error) {
|
||||
if cfg.Command == "" {
|
||||
cfg.Command = "gopls"
|
||||
@@ -130,6 +135,7 @@ func NewClient(parent context.Context, cfg Config) (*Client, error) {
|
||||
go func() {
|
||||
client.exitCh <- cmd.Wait()
|
||||
}()
|
||||
// 独立协程持续读取 stdout 并分发响应。
|
||||
go client.readLoop(stdout)
|
||||
|
||||
initCtx, cancel := context.WithTimeout(parent, 10*time.Second)
|
||||
@@ -143,6 +149,7 @@ func NewClient(parent context.Context, cfg Config) (*Client, error) {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// initialize 完成 LSP initialize/initialized 流程。
|
||||
func (c *Client) initialize(ctx context.Context, rootPath string) error {
|
||||
rootURI, err := pathToURI(rootPath)
|
||||
if err != nil {
|
||||
@@ -183,6 +190,7 @@ func (c *Client) initialize(ctx context.Context, rootPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DidOpen 发送 textDocument/didOpen,告知服务端首次打开文档。
|
||||
func (c *Client) DidOpen(ctx context.Context, uri, text string, version int) error {
|
||||
normalizedURI, err := c.normalizeURI(uri)
|
||||
if err != nil {
|
||||
@@ -200,6 +208,7 @@ func (c *Client) DidOpen(ctx context.Context, uri, text string, version int) err
|
||||
return c.notifyWithContext(ctx, "textDocument/didOpen", params)
|
||||
}
|
||||
|
||||
// DidChange 发送 textDocument/didChange,推送文档全文与版本号。
|
||||
func (c *Client) DidChange(ctx context.Context, uri, text string, version int) error {
|
||||
normalizedURI, err := c.normalizeURI(uri)
|
||||
if err != nil {
|
||||
@@ -220,6 +229,7 @@ func (c *Client) DidChange(ctx context.Context, uri, text string, version int) e
|
||||
return c.notifyWithContext(ctx, "textDocument/didChange", params)
|
||||
}
|
||||
|
||||
// Completion 调用 textDocument/completion,并兼容两种返回形态。
|
||||
func (c *Client) Completion(ctx context.Context, uri string, line, character int) (completion.Response, error) {
|
||||
normalizedURI, err := c.normalizeURI(uri)
|
||||
if err != nil {
|
||||
@@ -247,6 +257,7 @@ func (c *Client) Completion(ctx context.Context, uri string, line, character int
|
||||
}
|
||||
|
||||
if body[0] == '[' {
|
||||
// 部分 LSP 服务端直接返回 CompletionItem[]。
|
||||
var items []lspCompletionItem
|
||||
if err := json.Unmarshal(body, &items); err != nil {
|
||||
return completion.Response{}, fmt.Errorf("decode completion items: %w", err)
|
||||
@@ -266,6 +277,7 @@ func (c *Client) Completion(ctx context.Context, uri string, line, character int
|
||||
|
||||
var windowsDrivePattern = regexp.MustCompile(`^[A-Za-z]:`)
|
||||
|
||||
// normalizeURI 将相对 file URI 重写为工作区绝对路径 URI。
|
||||
func (c *Client) normalizeURI(rawURI string) (string, error) {
|
||||
if rawURI == "" {
|
||||
return "", errors.New("empty uri")
|
||||
@@ -296,6 +308,7 @@ func (c *Client) normalizeURI(rawURI string) (string, error) {
|
||||
return pathToURI(localPath)
|
||||
}
|
||||
|
||||
// Close 优雅关闭 LSP 进程并失败通知所有未完成请求。
|
||||
func (c *Client) Close() error {
|
||||
c.closeOnce.Do(func() {
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
@@ -320,6 +333,7 @@ func (c *Client) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// request 发送 JSON-RPC 请求并阻塞等待对应响应。
|
||||
func (c *Client) request(ctx context.Context, method string, params any, out any) error {
|
||||
if c.closed.Load() {
|
||||
return errClientClosed
|
||||
@@ -365,6 +379,7 @@ func (c *Client) notify(method string, params any) error {
|
||||
return c.notifyWithContext(context.Background(), method, params)
|
||||
}
|
||||
|
||||
// notifyWithContext 发送 JSON-RPC 通知(无响应)。
|
||||
func (c *Client) notifyWithContext(ctx context.Context, method string, params any) error {
|
||||
if c.closed.Load() {
|
||||
return errClientClosed
|
||||
@@ -383,6 +398,7 @@ func (c *Client) notifyWithContext(ctx context.Context, method string, params an
|
||||
return c.writeMessage(msg)
|
||||
}
|
||||
|
||||
// writeMessage 按 LSP framing 写入消息头和消息体。
|
||||
func (c *Client) writeMessage(msg any) error {
|
||||
payload, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
@@ -403,6 +419,7 @@ func (c *Client) writeMessage(msg any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// readLoop 持续读取 LSP 消息并交给 handleIncoming 分发。
|
||||
func (c *Client) readLoop(stdout io.Reader) {
|
||||
reader := bufio.NewReader(stdout)
|
||||
for {
|
||||
@@ -415,6 +432,7 @@ func (c *Client) readLoop(stdout io.Reader) {
|
||||
}
|
||||
}
|
||||
|
||||
// readMessage 按 Content-Length 协议边界读取单条 JSON-RPC 消息。
|
||||
func readMessage(reader *bufio.Reader) ([]byte, error) {
|
||||
contentLength := 0
|
||||
for {
|
||||
@@ -451,6 +469,7 @@ func readMessage(reader *bufio.Reader) ([]byte, error) {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// handleIncoming 将响应按 id 投递给对应等待中的请求通道。
|
||||
func (c *Client) handleIncoming(body []byte) {
|
||||
var envelope incomingEnvelope
|
||||
if err := json.Unmarshal(body, &envelope); err != nil {
|
||||
@@ -485,6 +504,7 @@ func (c *Client) handleIncoming(body []byte) {
|
||||
wait <- rpcResponse{result: envelope.Result}
|
||||
}
|
||||
|
||||
// normalizeID 统一处理数字/字符串两种 JSON-RPC id。
|
||||
func normalizeID(raw json.RawMessage) string {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
if len(raw) == 0 {
|
||||
@@ -506,6 +526,7 @@ func (c *Client) removePending(key string) {
|
||||
c.pendingMu.Unlock()
|
||||
}
|
||||
|
||||
// failPending 在连接异常时让所有挂起请求立即失败返回。
|
||||
func (c *Client) failPending(err error) {
|
||||
c.pendingMu.Lock()
|
||||
defer c.pendingMu.Unlock()
|
||||
@@ -516,6 +537,7 @@ func (c *Client) failPending(err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// mapCompletionItems 将 LSP 项结构映射为网关统一输出结构。
|
||||
func mapCompletionItems(items []lspCompletionItem) []completion.Item {
|
||||
out := make([]completion.Item, 0, len(items))
|
||||
for _, it := range items {
|
||||
@@ -532,6 +554,7 @@ func mapCompletionItems(items []lspCompletionItem) []completion.Item {
|
||||
return out
|
||||
}
|
||||
|
||||
// decodeDocumentation 兼容 string 和 MarkupContent 两种文档字段。
|
||||
func decodeDocumentation(raw json.RawMessage) string {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
|
||||
@@ -556,6 +579,7 @@ func decodeDocumentation(raw json.RawMessage) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// pathToURI 将本地路径转换为标准 file URI。
|
||||
func pathToURI(path string) (string, error) {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user