Skip to content

Commit f513288

Browse files
feat: 增强 LSP 服务并发安全性并优化多文件诊断逻辑
- **LSP 服务增强**: - **多文件诊断推送**:重构 `UpdateSession` 和 `rebuildPackage` 逻辑,支持跨文件聚合错误信息。现在当包内任一文件发生变化时,会全量更新并推送该包下所有相关文件的诊断信息。 - **并发安全写入**:在 `lsp-server` 中引入 `sync.Mutex` 保护标准输出,并采用 Goroutine 异步处理消息,提升服务器响应性能与稳定性。 - **异常恢复**:在消息处理循环中增加 `recover` 机制,防止特定脚本解析异常导致整个 LSP 进程崩溃。 - **补全逻辑优化**: - **切片安全性增强**:在 `core/ast/query.go` 中进行成员补全时,对 `Params` 切片头进行克隆操作,确保剥离接收者(Receiver)的操作仅影响局部副本,避免污染原始类型定义。 - **重构与清理**: - 简化了 `UpdateSession` 的内部流程,移除冗余的诊断过滤逻辑,统一采用包级重新构建模式。
1 parent 32f57b2 commit f513288

3 files changed

Lines changed: 57 additions & 60 deletions

File tree

cmd/lsp-server/main.go

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"os"
1010
"strings"
11+
"sync"
1112

1213
engine "gopkg.d7z.net/go-mini/core"
1314
"gopkg.d7z.net/go-mini/core/lspserv"
@@ -33,6 +34,14 @@ func main() {
3334
server := lspserv.NewLSPServer(executor)
3435
reader := bufio.NewReader(os.Stdin)
3536

37+
var mu sync.Mutex
38+
writeMessage := func(msg interface{}) {
39+
mu.Lock()
40+
defer mu.Unlock()
41+
body, _ := json.Marshal(msg)
42+
fmt.Printf("Content-Length: %d\r\n\r\n%s", len(body), body)
43+
}
44+
3645
for {
3746
msg, err := readMessage(reader)
3847
if err != nil {
@@ -43,7 +52,14 @@ func main() {
4352
continue
4453
}
4554

46-
handleMessage(server, msg)
55+
go func(m *rpcMessage) {
56+
defer func() {
57+
if r := recover(); r != nil {
58+
fmt.Fprintf(os.Stderr, "LSP Panic recovered: %v\n", r)
59+
}
60+
}()
61+
handleMessage(server, m, writeMessage)
62+
}(msg)
4763
}
4864
}
4965

@@ -80,12 +96,7 @@ func readMessage(r *bufio.Reader) (*rpcMessage, error) {
8096
return &msg, nil
8197
}
8298

83-
func writeMessage(msg interface{}) {
84-
body, _ := json.Marshal(msg)
85-
fmt.Printf("Content-Length: %d\r\n\r\n%s", len(body), body)
86-
}
87-
88-
func handleMessage(server *lspserv.LSPServer, msg *rpcMessage) {
99+
func handleMessage(server *lspserv.LSPServer, msg *rpcMessage, writeMessage func(interface{})) {
89100
switch msg.Method {
90101
case "initialize":
91102
writeMessage(rpcMessage{
@@ -125,14 +136,16 @@ func handleMessage(server *lspserv.LSPServer, msg *rpcMessage) {
125136
code = params.ContentChanges[0].Text
126137
}
127138

128-
diagnostics, _ := server.UpdateSession(uri, code)
139+
allDiagnostics, _ := server.UpdateSession(uri, code)
129140

130-
// 发送异步诊断推送
131-
writeMessage(rpcMessage{
132-
JSONRPC: "2.0",
133-
Method: "textDocument/publishDiagnostics",
134-
Params: json.RawMessage(fmt.Sprintf(`{"uri":"%s","diagnostics":%s}`, uri, mustMarshal(diagnostics))),
135-
})
141+
// 为包内所有受影响的文件发布诊断
142+
for fURI, diags := range allDiagnostics {
143+
writeMessage(rpcMessage{
144+
JSONRPC: "2.0",
145+
Method: "textDocument/publishDiagnostics",
146+
Params: json.RawMessage(fmt.Sprintf(`{"uri":"%s","diagnostics":%s}`, fURI, mustMarshal(diags))),
147+
})
148+
}
136149

137150
case "textDocument/completion":
138151
var params struct {

core/ast/query.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,10 +833,13 @@ func getMemberCompletions(ctx *ValidContext, obj Expr) []CompletionItem {
833833
items = append(items, CompletionItem{Label: string(f), Kind: "field", Type: t})
834834
}
835835
for m, t := range st.Methods {
836+
// t 是结构体副本,但 Params 是切片(引用)。
837+
// 为了绝对安全,我们克隆一份切片头。
836838
sig := t
837839
// 剥离接收者以便在补全中显示正确的参数列表
838840
if objType != "Package" && objType != TypeModule {
839841
if len(sig.Params) > 0 {
842+
// 这里的赋值只修改 local sig 的切片头,不会写回 st.Methods
840843
sig.Params = sig.Params[1:]
841844
}
842845
}

core/lspserv/server.go

Lines changed: 27 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,21 @@ func NewLSPServer(e *engine.MiniExecutor) *LSPServer {
3737
}
3838
}
3939

40-
// UpdateSession 更新或创建脚本会话并返回诊断信息
41-
func (s *LSPServer) UpdateSession(uri, code string) ([]Diagnostic, error) {
42-
// 1. 尝试获取包名(简单正则或初步解析)
40+
// UpdateSession 更新或创建脚本会话并返回诊断信息。
41+
func (s *LSPServer) UpdateSession(uri, code string) (map[string][]Diagnostic, error) {
4342
pkgName := "main"
4443
converter := ffigo.NewGoToASTConverter()
4544
node, _ := converter.ConvertSourceTolerant(uri, code)
4645
if prog, ok := node.(*ast.ProgramStmt); ok && prog.Package != "" {
4746
pkgName = prog.Package
4847
}
4948

50-
// 2. 存储文件会话
5149
s.sessions.Store(uri, &fileSession{pkgName: pkgName, code: code})
5250

53-
// 3. 触发包级重新聚合分析
54-
return s.rebuildPackage(pkgName, uri)
51+
return s.rebuildPackage(pkgName)
5552
}
5653

57-
func (s *LSPServer) rebuildPackage(pkgName, targetURI string) ([]Diagnostic, error) {
54+
func (s *LSPServer) rebuildPackage(pkgName string) (map[string][]Diagnostic, error) {
5855
val, _ := s.packages.LoadOrStore(pkgName, &packageState{files: make(map[string]string)})
5956
pkg := val.(*packageState)
6057
pkg.mu.Lock()
@@ -69,10 +66,9 @@ func (s *LSPServer) rebuildPackage(pkgName, targetURI string) ([]Diagnostic, err
6966
return true
7067
})
7168

72-
// 聚合所有文件到单个 ProgramStmt
7369
var combinedNode *ast.ProgramStmt
7470
converter := ffigo.NewGoToASTConverter()
75-
diagnostics := make([]Diagnostic, 0)
71+
allDiagnostics := make(map[string][]Diagnostic)
7672

7773
for uri, code := range pkg.files {
7874
node, errs := converter.ConvertSourceTolerant(uri, code)
@@ -81,19 +77,17 @@ func (s *LSPServer) rebuildPackage(pkgName, targetURI string) ([]Diagnostic, err
8177
for _, err := range errs {
8278
var scanErr scanner.Error
8379
if errors.As(err, &scanErr) {
84-
if scanErr.Pos.Filename == targetURI {
85-
diagnostics = append(diagnostics, Diagnostic{
86-
Range: Range{
87-
Start: Position{Line: scanErr.Pos.Line - 1, Character: scanErr.Pos.Column - 1},
88-
End: Position{Line: scanErr.Pos.Line - 1, Character: scanErr.Pos.Column},
89-
},
90-
Severity: 1,
91-
Source: "go-mini-syntax",
92-
Message: scanErr.Msg,
93-
})
94-
}
95-
} else if err != nil && uri == targetURI {
96-
diagnostics = append(diagnostics, Diagnostic{
80+
allDiagnostics[uri] = append(allDiagnostics[uri], Diagnostic{
81+
Range: Range{
82+
Start: Position{Line: scanErr.Pos.Line - 1, Character: scanErr.Pos.Column - 1},
83+
End: Position{Line: scanErr.Pos.Line - 1, Character: scanErr.Pos.Column},
84+
},
85+
Severity: 1,
86+
Source: "go-mini-syntax",
87+
Message: scanErr.Msg,
88+
})
89+
} else if err != nil {
90+
allDiagnostics[uri] = append(allDiagnostics[uri], Diagnostic{
9791
Range: FromInternalPos(&ast.Position{L: 1, C: 1}),
9892
Severity: 1,
9993
Source: "go-mini-syntax",
@@ -106,69 +100,56 @@ func (s *LSPServer) rebuildPackage(pkgName, targetURI string) ([]Diagnostic, err
106100
if combinedNode == nil {
107101
combinedNode = prog
108102
} else {
109-
// 合并符号(借用我们在 cmd/exec 中定义的逻辑思路)
110103
mergeProgramStmts(combinedNode, prog)
111104
}
112105
}
113106
}
114107

115108
if combinedNode == nil {
116-
return diagnostics, nil
109+
return allDiagnostics, nil
117110
}
118111

119112
// 运行校验以生成持久化 Scope
120113
prog, errs := s.executor.NewMiniProgramByProgramTolerant(combinedNode)
121114
pkg.combined = prog
122115

123-
// 将当前触发文件的诊断信息返回
116+
// 收集所有文件的诊断信息
124117
for _, err := range errs {
125118
if astErr, ok := err.(*ast.MiniAstError); ok {
126119
for _, log := range astErr.Logs {
127120
loc := log.Node.GetBase().Loc
128-
// 严格过滤文件路径,确保错误显示在正确的文件中
129-
if loc != nil && loc.F == targetURI {
121+
if loc != nil && loc.F != "" {
130122
diag := Diagnostic{
131123
Range: FromInternalPos(loc),
132124
Severity: 1,
133125
Source: "go-mini",
134126
Message: log.Message,
135127
}
136-
diagnostics = append(diagnostics, diag)
128+
allDiagnostics[loc.F] = append(allDiagnostics[loc.F], diag)
137129
}
138130
}
139131
} else {
140-
// 处理其他类型的运行时或执行错误
132+
// 处理运行时错误
141133
var vme *runtime.VMError
142134
if errors.As(err, &vme) {
143-
if len(vme.Frames) > 0 && vme.Frames[0].Filename == targetURI {
135+
if len(vme.Frames) > 0 {
136+
f := vme.Frames[0]
144137
diag := Diagnostic{
145138
Range: FromInternalPos(&ast.Position{
146-
L: vme.Frames[0].Line,
147-
C: vme.Frames[0].Column,
139+
L: f.Line,
140+
C: f.Column,
148141
}),
149142
Severity: 1,
150143
Source: "go-mini-runtime",
151144
Message: vme.Message,
152145
}
153-
for _, f := range vme.Frames {
154-
diag.RelatedInformation = append(diag.RelatedInformation, DiagnosticRelatedInformation{
155-
Location: Location{
156-
URI: f.Filename,
157-
Range: FromInternalPos(&ast.Position{
158-
L: f.Line, C: f.Column,
159-
EL: f.Line, EC: f.Column + 1,
160-
}),
161-
},
162-
Message: fmt.Sprintf("at %s()", f.Function),
163-
})
164-
}
165-
diagnostics = append(diagnostics, diag)
146+
allDiagnostics[f.Filename] = append(allDiagnostics[f.Filename], diag)
166147
}
167148
}
168149
}
169150
}
170151

171-
return diagnostics, nil
152+
return allDiagnostics, nil
172153
}
173154

174155
func mergeProgramStmts(dest, src *ast.ProgramStmt) {

0 commit comments

Comments
 (0)