252 lines
5.9 KiB
Go
252 lines
5.9 KiB
Go
package lsp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
const idleTimeout = 5 * time.Minute
|
|
|
|
// BridgeRequest is the JSON structure sent to the bridge by tool clients.
|
|
type BridgeRequest struct {
|
|
Method string `json:"method"`
|
|
File string `json:"file"`
|
|
Line int `json:"line"`
|
|
Col int `json:"col"`
|
|
NewName string `json:"new_name,omitempty"`
|
|
Query string `json:"query,omitempty"`
|
|
Workspace bool `json:"workspace,omitempty"`
|
|
Cwd string `json:"cwd,omitempty"`
|
|
}
|
|
|
|
// BridgeResponse is the JSON structure returned by the bridge.
|
|
type BridgeResponse struct {
|
|
Result json.RawMessage `json:"result,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// Bridge manages LSP server processes and handles tool requests.
|
|
type Bridge struct {
|
|
mu sync.Mutex
|
|
servers map[string]*Server // key: "adapter:root"
|
|
lastActivity time.Time
|
|
}
|
|
|
|
// NewBridge creates a new bridge instance.
|
|
func NewBridge() *Bridge {
|
|
return &Bridge{
|
|
servers: make(map[string]*Server),
|
|
lastActivity: time.Now(),
|
|
}
|
|
}
|
|
|
|
func (b *Bridge) getServer(adapter *Adapter, root string) (*Server, error) {
|
|
key := adapter.Name + ":" + root
|
|
b.mu.Lock()
|
|
srv := b.servers[key]
|
|
b.mu.Unlock()
|
|
if srv != nil {
|
|
return srv, nil
|
|
}
|
|
|
|
srv, err := StartServer(adapter, root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b.mu.Lock()
|
|
b.servers[key] = srv
|
|
b.mu.Unlock()
|
|
return srv, nil
|
|
}
|
|
|
|
// Handle processes a single bridge request.
|
|
func (b *Bridge) Handle(req *BridgeRequest) *BridgeResponse {
|
|
b.mu.Lock()
|
|
b.lastActivity = time.Now()
|
|
b.mu.Unlock()
|
|
|
|
if req.File == "" {
|
|
return &BridgeResponse{Error: "missing 'file' parameter"}
|
|
}
|
|
|
|
adapter := Detect(req.File)
|
|
if adapter == nil {
|
|
return &BridgeResponse{Error: fmt.Sprintf("no adapter for file: %s", req.File)}
|
|
}
|
|
|
|
root := adapter.WorkspaceRoot(req.File, req.Cwd)
|
|
srv, err := b.getServer(adapter, root)
|
|
if err != nil {
|
|
return &BridgeResponse{Error: fmt.Sprintf("start server: %v", err)}
|
|
}
|
|
|
|
var result json.RawMessage
|
|
|
|
switch req.Method {
|
|
case "definition":
|
|
srv.EnsureOpen(req.File)
|
|
result, err = srv.Request("textDocument/definition", map[string]any{
|
|
"textDocument": map[string]any{"uri": FileURI(req.File)},
|
|
"position": map[string]any{"line": req.Line, "character": req.Col},
|
|
}, 30*time.Second)
|
|
|
|
case "references":
|
|
srv.EnsureOpen(req.File)
|
|
result, err = srv.Request("textDocument/references", map[string]any{
|
|
"textDocument": map[string]any{"uri": FileURI(req.File)},
|
|
"position": map[string]any{"line": req.Line, "character": req.Col},
|
|
"context": map[string]any{"includeDeclaration": true},
|
|
}, 30*time.Second)
|
|
|
|
case "hover":
|
|
srv.EnsureOpen(req.File)
|
|
result, err = srv.Request("textDocument/hover", map[string]any{
|
|
"textDocument": map[string]any{"uri": FileURI(req.File)},
|
|
"position": map[string]any{"line": req.Line, "character": req.Col},
|
|
}, 30*time.Second)
|
|
|
|
case "completion":
|
|
srv.EnsureOpen(req.File)
|
|
result, err = srv.Request("textDocument/completion", map[string]any{
|
|
"textDocument": map[string]any{"uri": FileURI(req.File)},
|
|
"position": map[string]any{"line": req.Line, "character": req.Col},
|
|
}, 30*time.Second)
|
|
|
|
case "rename":
|
|
srv.EnsureOpen(req.File)
|
|
result, err = srv.Request("textDocument/rename", map[string]any{
|
|
"textDocument": map[string]any{"uri": FileURI(req.File)},
|
|
"position": map[string]any{"line": req.Line, "character": req.Col},
|
|
"newName": req.NewName,
|
|
}, 30*time.Second)
|
|
|
|
case "symbols":
|
|
if req.Workspace {
|
|
result, err = srv.Request("workspace/symbol", map[string]any{
|
|
"query": req.Query,
|
|
}, 30*time.Second)
|
|
} else {
|
|
srv.EnsureOpen(req.File)
|
|
result, err = srv.Request("textDocument/documentSymbol", map[string]any{
|
|
"textDocument": map[string]any{"uri": FileURI(req.File)},
|
|
}, 30*time.Second)
|
|
}
|
|
|
|
case "diagnostics":
|
|
srv.EnsureOpen(req.File)
|
|
// Wait for push diagnostics
|
|
time.Sleep(1 * time.Second)
|
|
diags := srv.Diagnostics(FileURI(req.File))
|
|
if diags == nil {
|
|
diags = json.RawMessage("[]")
|
|
}
|
|
return &BridgeResponse{Result: diags}
|
|
|
|
default:
|
|
return &BridgeResponse{Error: fmt.Sprintf("unknown method: %s", req.Method)}
|
|
}
|
|
|
|
if err != nil {
|
|
return &BridgeResponse{Error: err.Error()}
|
|
}
|
|
return &BridgeResponse{Result: result}
|
|
}
|
|
|
|
// Shutdown stops all managed LSP servers.
|
|
func (b *Bridge) Shutdown() {
|
|
b.mu.Lock()
|
|
servers := make([]*Server, 0, len(b.servers))
|
|
for _, s := range b.servers {
|
|
servers = append(servers, s)
|
|
}
|
|
b.servers = make(map[string]*Server)
|
|
b.mu.Unlock()
|
|
|
|
for _, s := range servers {
|
|
s.Stop()
|
|
}
|
|
}
|
|
|
|
// RunBridge starts the bridge daemon listening on the Unix socket.
|
|
// Blocks until idle timeout or signal.
|
|
func RunBridge() error {
|
|
sock := SockPath()
|
|
os.Remove(sock)
|
|
|
|
ln, err := net.Listen("unix", sock)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer ln.Close()
|
|
os.Chmod(sock, 0600)
|
|
|
|
// Write PID file
|
|
pidFile := PidPath()
|
|
os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0600)
|
|
defer os.Remove(pidFile)
|
|
defer os.Remove(sock)
|
|
|
|
bridge := NewBridge()
|
|
defer bridge.Shutdown()
|
|
|
|
// Idle watchdog
|
|
done := make(chan struct{})
|
|
go func() {
|
|
for {
|
|
time.Sleep(30 * time.Second)
|
|
bridge.mu.Lock()
|
|
idle := time.Since(bridge.lastActivity) > idleTimeout
|
|
bridge.mu.Unlock()
|
|
if idle {
|
|
ln.Close()
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Signal handler
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
go func() {
|
|
<-sigCh
|
|
ln.Close()
|
|
}()
|
|
|
|
// Accept loop
|
|
go func() {
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
close(done)
|
|
return
|
|
}
|
|
go handleConn(bridge, conn)
|
|
}
|
|
}()
|
|
|
|
<-done
|
|
return nil
|
|
}
|
|
|
|
func handleConn(b *Bridge, conn net.Conn) {
|
|
defer conn.Close()
|
|
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
|
|
|
var req BridgeRequest
|
|
dec := json.NewDecoder(conn)
|
|
if err := dec.Decode(&req); err != nil {
|
|
resp := BridgeResponse{Error: err.Error()}
|
|
json.NewEncoder(conn).Encode(resp)
|
|
return
|
|
}
|
|
|
|
resp := b.Handle(&req)
|
|
json.NewEncoder(conn).Encode(resp)
|
|
}
|