From 74635b8e0080f384de9863ea6a5334592720b94b Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Sun, 9 Aug 2026 11:32:31 +0200 Subject: [PATCH] refactor: items 8-10, 12, 13 from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move parseRetryAfter to backend.go (where it's called) - Extract paths.RuntimeDir() replacing 3 divergent XDG_RUNTIME_DIR impls - Rename CodeWhispererBackend → KiroBackend, NewCodeWhisperer → NewKiro - Export RPCRequest/RPCError/OutputNotification from toolsrv/rpcwire.go - Factor tools/lsp/cmdutil: Run() and RunCustom() for 7 LSP binaries --- backend/backend.go | 20 ++++ backend/{codewhisperer.go => kiro.go} | 28 ++--- ...whisperer_internal.go => kiro_internal.go} | 2 +- backend/new.go | 2 +- backend/openai.go | 20 ---- cmd/ollie-remote/main.go | 50 +++------ cmd/olliesrv/main.go | 6 +- paths/paths.go | 9 ++ tools/lsp/adapter.go | 9 +- tools/lsp/cmd/lsp_completion/main.go | 75 ++++--------- tools/lsp/cmd/lsp_definition/main.go | 103 ++++++------------ tools/lsp/cmd/lsp_diagnostics/main.go | 72 ++++++------ tools/lsp/cmd/lsp_hover/main.go | 73 +++---------- tools/lsp/cmd/lsp_references/main.go | 75 ++++--------- tools/lsp/cmd/lsp_rename/main.go | 79 +++++++------- tools/lsp/cmd/lsp_symbols/main.go | 96 ++++++++-------- tools/lsp/cmdutil/cmdutil.go | 85 +++++++++++++++ toolsrv/rpcwire.go | 22 ++++ toolsrv/transport.go | 12 +- 19 files changed, 381 insertions(+), 457 deletions(-) rename backend/{codewhisperer.go => kiro.go} (97%) rename backend/{codewhisperer_internal.go => kiro_internal.go} (99%) create mode 100644 tools/lsp/cmdutil/cmdutil.go create mode 100644 toolsrv/rpcwire.go diff --git a/backend/backend.go b/backend/backend.go index f22d6b9..679ef16 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "net/http" + "strconv" "strings" "time" ) @@ -240,3 +241,22 @@ func isContextOverflow(body string) bool { } return false } + +// parseRetryAfter parses the Retry-After header value, which may be an integer +// number of seconds or an HTTP-date. Returns zero if the header is absent or +// unparseable. +func parseRetryAfter(header string) time.Duration { + if header == "" { + return 0 + } + header = strings.TrimSpace(header) + if secs, err := strconv.Atoi(header); err == nil { + return time.Duration(secs) * time.Second + } + if t, err := http.ParseTime(header); err == nil { + if d := time.Until(t); d > 0 { + return d + } + } + return 0 +} diff --git a/backend/codewhisperer.go b/backend/kiro.go similarity index 97% rename from backend/codewhisperer.go rename to backend/kiro.go index d3f30c6..f51dcae 100644 --- a/backend/codewhisperer.go +++ b/backend/kiro.go @@ -1,12 +1,12 @@ package backend -// CodeWhispererBackend implements Backend for Amazon CodeWhisperer / Kiro. +// KiroBackend implements Backend for the Kiro streaming API. // // COVERAGE: Intentionally untested. Reverse-engineered Kiro streaming API // client requiring a live session to test. // // -// Auth is configured via the apiKey parameter to NewCodeWhisperer: +// Auth is configured via the apiKey parameter to NewKiro: // - Empty string → read from Kiro CLI SQLite database at the default path // ($XDG_DATA_HOME/kiro-cli/data.sqlite3 on Linux). // - "sqlite:///path/to/data.sqlite3" → read from the specified SQLite file. @@ -35,7 +35,7 @@ import ( // ── Public constructor ──────────────────────────────────────────────────────── -type CodeWhispererBackend struct { +type KiroBackend struct { baseBackend endpoint string // overrides auth-derived endpoint if non-empty extraHeaders map[string]string @@ -49,13 +49,13 @@ type CodeWhispererBackend struct { modelsCacheAt time.Time } -// NewCodeWhisperer returns a CodeWhisperer backend. See package doc for apiKey semantics. -func NewCodeWhisperer(apiKey string) (*CodeWhispererBackend, error) { +// NewKiro returns a Kiro backend. See package doc for apiKey semantics. +func NewKiro(apiKey string) (*KiroBackend, error) { if strings.TrimSpace(apiKey) == "" { apiKey = kiroDefaultSQLiteKey() } authSource, err := newKiroAuthSource(apiKey) - b := &CodeWhispererBackend{ + b := &KiroBackend{ baseBackend: baseBackend{name: "kiro"}, authSource: authSource, authInitErr: err, @@ -65,9 +65,9 @@ func NewCodeWhisperer(apiKey string) (*CodeWhispererBackend, error) { return b, nil } -func (b *CodeWhispererBackend) DefaultModel() string { return "auto" } +func (b *KiroBackend) DefaultModel() string { return "auto" } -func (b *CodeWhispererBackend) Models(ctx context.Context) []string { +func (b *KiroBackend) Models(ctx context.Context) []string { b.modelsMu.Lock() if b.modelsCache != nil && time.Since(b.modelsCacheAt) < 24*time.Hour { result := b.modelsCache @@ -92,7 +92,7 @@ func (b *CodeWhispererBackend) Models(ctx context.Context) []string { return ids } -func (b *CodeWhispererBackend) fetchModels(ctx context.Context) *kiroListModelsResponse { +func (b *KiroBackend) fetchModels(ctx context.Context) *kiroListModelsResponse { if b.authInitErr != nil { return nil } @@ -113,7 +113,7 @@ func (b *CodeWhispererBackend) fetchModels(ctx context.Context) *kiroListModelsR return resp } -func (b *CodeWhispererBackend) ContextLength(ctx context.Context) int { +func (b *KiroBackend) ContextLength(ctx context.Context) int { if b.ctxLen > 0 { return b.ctxLen } @@ -136,7 +136,7 @@ func (b *CodeWhispererBackend) ContextLength(ctx context.Context) int { // ── Backend interface ───────────────────────────────────────────────────────── -func (b *CodeWhispererBackend) ChatStream( +func (b *KiroBackend) ChatStream( ctx context.Context, messages []Message, tools []Tool, @@ -172,7 +172,7 @@ const kiroMaxThrottleRetries = 5 // Tests can override this. var kiroThrottleBaseDelay = 5 * time.Second -func (b *CodeWhispererBackend) runStream(ctx context.Context, req *kiroGenerateRequest, ch chan<- StreamEvent) { +func (b *KiroBackend) runStream(ctx context.Context, req *kiroGenerateRequest, ch chan<- StreamEvent) { var throttleAttempts int for { token, err := b.authSource.AccessToken(ctx) @@ -258,14 +258,14 @@ func (b *CodeWhispererBackend) runStream(ctx context.Context, req *kiroGenerateR } } -func (b *CodeWhispererBackend) resolveEndpoint(ctx context.Context) (string, error) { +func (b *KiroBackend) resolveEndpoint(ctx context.Context) (string, error) { if ep := strings.TrimSpace(b.endpoint); ep != "" { return ep, nil } return b.authSource.DefaultEndpoint(ctx) } -func (b *CodeWhispererBackend) shouldRefresh(err error) bool { +func (b *KiroBackend) shouldRefresh(err error) bool { if !b.authSource.CanRefresh() { return false } diff --git a/backend/codewhisperer_internal.go b/backend/kiro_internal.go similarity index 99% rename from backend/codewhisperer_internal.go rename to backend/kiro_internal.go index c1f0fa7..c830966 100644 --- a/backend/codewhisperer_internal.go +++ b/backend/kiro_internal.go @@ -1,6 +1,6 @@ package backend -// Low-level plumbing for the Amazon CodeWhisperer / Kiro backend. +// Low-level plumbing for the Kiro backend. // COVERAGE: Intentionally untested. See codewhisperer.go. // // - Wire types for the GenerateAssistantResponse API diff --git a/backend/new.go b/backend/new.go index fda2768..f3318ce 100644 --- a/backend/new.go +++ b/backend/new.go @@ -95,7 +95,7 @@ func newBackendFromConfig(which string, cfg configFile) (Backend, error) { if token == "" { token = os.Getenv("OLLIE_KIRO_TOKEN") } - b, err = NewCodeWhisperer(token) + b, err = NewKiro(token) case "gemini": key := bc.Key diff --git a/backend/openai.go b/backend/openai.go index 9a84c9e..48eb0fe 100644 --- a/backend/openai.go +++ b/backend/openai.go @@ -9,9 +9,7 @@ import ( "io" "net/http" "net/url" - "strconv" "strings" - "time" ) // OpenAIBackend speaks the OpenAI /v1/chat/completions wire format. @@ -549,21 +547,3 @@ func (b *OpenAIBackend) ChatStream(ctx context.Context, messages []Message, tool return streamRequest(b.client, httpReq, "openai", streamOpenAISSE) } -// parseRetryAfter parses the Retry-After header value, which may be an integer -// number of seconds or an HTTP-date. Returns zero if the header is absent or -// unparseable. -func parseRetryAfter(header string) time.Duration { - if header == "" { - return 0 - } - header = strings.TrimSpace(header) - if secs, err := strconv.Atoi(header); err == nil { - return time.Duration(secs) * time.Second - } - if t, err := http.ParseTime(header); err == nil { - if d := time.Until(t); d > 0 { - return d - } - } - return 0 -} diff --git a/cmd/ollie-remote/main.go b/cmd/ollie-remote/main.go index c96943f..9730b77 100644 --- a/cmd/ollie-remote/main.go +++ b/cmd/ollie-remote/main.go @@ -158,30 +158,14 @@ func serveSocket(ctx context.Context, srv *toolsrv.Server, sockPath string) { wg.Wait() } -type rpcRequest struct { - JSONRPC string `json:"jsonrpc"` - ID json.RawMessage `json:"id"` - Method string `json:"method"` - Params json.RawMessage `json:"params"` -} - type rpcResponse struct { JSONRPC string `json:"jsonrpc"` ID json.RawMessage `json:"id"` Result any `json:"result,omitempty"` - Error *rpcError `json:"error,omitempty"` + Error *toolsrv.RPCError `json:"error,omitempty"` Stream bool `json:"stream,omitempty"` } -type rpcError struct { - Code int `json:"code"` - Message string `json:"message"` -} - -type outputNotif struct { - Data string `json:"data"` -} - type lockedEncoder struct { mu sync.Mutex enc *json.Encoder @@ -198,7 +182,7 @@ func serveRPC(ctx context.Context, srv *toolsrv.Server, in io.Reader, out io.Wri enc := &lockedEncoder{enc: json.NewEncoder(out)} for { - var req rpcRequest + var req toolsrv.RPCRequest if err := dec.Decode(&req); err != nil { if err == io.EOF || ctx.Err() != nil { return @@ -206,7 +190,7 @@ func serveRPC(ctx context.Context, srv *toolsrv.Server, in io.Reader, out io.Wri enc.Encode(rpcResponse{ JSONRPC: "2.0", ID: req.ID, - Error: &rpcError{Code: -32700, Message: "parse error: " + err.Error()}, + Error: &toolsrv.RPCError{Code: -32700, Message: "parse error: " + err.Error()}, }) continue } @@ -215,7 +199,7 @@ func serveRPC(ctx context.Context, srv *toolsrv.Server, in io.Reader, out io.Wri } } -func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lockedEncoder) { +func handleRPC(ctx context.Context, srv *toolsrv.Server, req toolsrv.RPCRequest, enc *lockedEncoder) { switch req.Method { case "list_tools": tools, err := srv.ListTools() @@ -223,7 +207,7 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo enc.Encode(rpcResponse{ JSONRPC: "2.0", ID: req.ID, - Error: &rpcError{Code: -32000, Message: err.Error()}, + Error: &toolsrv.RPCError{Code: -32000, Message: err.Error()}, }) } else { enc.Encode(rpcResponse{ @@ -258,7 +242,7 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo Value string `json:"value"` } if err := json.Unmarshal(req.Params, ¶ms); err != nil { - enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}}) + enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32602, Message: err.Error()}}) return } srv.SetEnv(params.Key, params.Value) @@ -269,7 +253,7 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo Dir string `json:"dir"` } if err := json.Unmarshal(req.Params, ¶ms); err != nil { - enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}}) + enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32602, Message: err.Error()}}) return } srv.SetCWD(params.Dir) @@ -280,7 +264,7 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo Names []string `json:"names"` } if err := json.Unmarshal(req.Params, ¶ms); err != nil { - enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}}) + enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32602, Message: err.Error()}}) return } srv.SetAllowTools(params.Names) @@ -299,11 +283,11 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo Signal int `json:"signal"` } if err := json.Unmarshal(req.Params, ¶ms); err != nil { - enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}}) + enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32602, Message: err.Error()}}) return } if err := srv.SignalDetached(params.PID, syscall.Signal(params.Signal)); err != nil { - enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32000, Message: err.Error()}}) + enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32000, Message: err.Error()}}) } else { enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: true}) } @@ -313,12 +297,12 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo PID int `json:"pid"` } if err := json.Unmarshal(req.Params, ¶ms); err != nil { - enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}}) + enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32602, Message: err.Error()}}) return } out, err := srv.GetDetachedOutput(params.PID) if err != nil { - enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32000, Message: err.Error()}}) + enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32000, Message: err.Error()}}) } else { enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: out}) } @@ -328,7 +312,7 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo PID int `json:"pid"` } if err := json.Unmarshal(req.Params, ¶ms); err != nil { - enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}}) + enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32602, Message: err.Error()}}) return } ok := srv.DismissDetached(params.PID) @@ -339,11 +323,11 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo Name string `json:"name"` } if err := json.Unmarshal(req.Params, ¶ms); err != nil { - enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}}) + enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32602, Message: err.Error()}}) return } if err := srv.LoadTool(params.Name); err != nil { - enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32000, Message: err.Error()}}) + enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32000, Message: err.Error()}}) } else { res, _ := json.Marshal(map[string]string{"result": "loaded: " + params.Name}) enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(res)}) @@ -354,7 +338,7 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo enc.Encode(rpcResponse{ JSONRPC: "2.0", ID: req.ID, - Result: outputNotif{Data: data}, + Result: toolsrv.OutputNotification{Data: data}, Stream: true, }) }) @@ -363,7 +347,7 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo enc.Encode(rpcResponse{ JSONRPC: "2.0", ID: req.ID, - Error: &rpcError{Code: -32000, Message: err.Error()}, + Error: &toolsrv.RPCError{Code: -32000, Message: err.Error()}, }) } else { enc.Encode(rpcResponse{ diff --git a/cmd/olliesrv/main.go b/cmd/olliesrv/main.go index a1eb4f2..2302ad8 100644 --- a/cmd/olliesrv/main.go +++ b/cmd/olliesrv/main.go @@ -84,11 +84,7 @@ func runServer(sockPath string) { // Server creation deferred until after bypass broker is ready (see below) // Start bypass broker - xdg := os.Getenv("XDG_RUNTIME_DIR") - if xdg == "" { - xdg = fmt.Sprintf("/run/user/%d", os.Getuid()) - } - bypassSocket := filepath.Join(xdg, "ollie", "bypass.sock") + bypassSocket := filepath.Join(paths.RuntimeDir(), "ollie", "bypass.sock") os.Setenv("OLLIE_BYPASS_SOCKET", bypassSocket) //nolint:errcheck policyPath := filepath.Join(paths.DataDir(), "bypass-policy.yaml") diff --git a/paths/paths.go b/paths/paths.go index 6f276a7..6815434 100644 --- a/paths/paths.go +++ b/paths/paths.go @@ -1,6 +1,7 @@ package paths import ( + "fmt" "os" "path/filepath" "strings" @@ -16,6 +17,14 @@ func ExpandHome(path string) string { return path } +// RuntimeDir returns $XDG_RUNTIME_DIR or the standard fallback /run/user/UID. +func RuntimeDir() string { + if d := os.Getenv("XDG_RUNTIME_DIR"); d != "" { + return d + } + return fmt.Sprintf("/run/user/%d", os.Getuid()) +} + // xdgConfigHome returns $XDG_CONFIG_HOME or ~/.config. func xdgConfigHome() string { if p := os.Getenv("XDG_CONFIG_HOME"); p != "" { diff --git a/tools/lsp/adapter.go b/tools/lsp/adapter.go index 4bdd57d..0f5aac9 100644 --- a/tools/lsp/adapter.go +++ b/tools/lsp/adapter.go @@ -1,8 +1,9 @@ package lsp import ( - "os" "path/filepath" + + "ollie/paths" ) // Adapter describes an LSP server for a specific language. @@ -108,11 +109,7 @@ func LanguageID(path string) string { // SockPath returns the path to the bridge Unix socket. func SockPath() string { - xdg := os.Getenv("XDG_RUNTIME_DIR") - if xdg == "" { - xdg = "/tmp" - } - return filepath.Join(xdg, "ollie-lsp.sock") + return filepath.Join(paths.RuntimeDir(), "ollie-lsp.sock") } // PidPath returns the path to the bridge PID file. diff --git a/tools/lsp/cmd/lsp_completion/main.go b/tools/lsp/cmd/lsp_completion/main.go index d45f589..c99add7 100644 --- a/tools/lsp/cmd/lsp_completion/main.go +++ b/tools/lsp/cmd/lsp_completion/main.go @@ -4,69 +4,32 @@ package main import ( "encoding/json" "fmt" - "os" - "path/filepath" - "strconv" - "ollie/tools/lsp" + "ollie/tools/lsp/cmdutil" ) func main() { - if len(os.Args) > 1 && os.Args[1] == "--bridge" { - lsp.RunBridge() - return - } + cmdutil.Run("completion", func(result json.RawMessage) { + var list struct { + Items []completionItem `json:"items"` + } + if json.Unmarshal(result, &list) == nil && len(list.Items) > 0 { + for _, item := range list.Items { + printItem(item) + } + return + } - var args struct { - Path string `json:"path"` - Line string `json:"line"` - Col string `json:"col"` - } - if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } + var items []completionItem + if json.Unmarshal(result, &items) == nil { + for _, item := range items { + printItem(item) + } + return + } - file, _ := filepath.Abs(args.Path) - line, _ := strconv.Atoi(args.Line) - col, _ := strconv.Atoi(args.Col) - - result, err := lsp.ClientRequest(&lsp.BridgeRequest{ - Method: "completion", - File: file, - Line: line - 1, - Col: col - 1, - }) - if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } - - if result == nil || string(result) == "null" { fmt.Println("(no completions)") - return - } - - // CompletionList or []CompletionItem - var list struct { - Items []completionItem `json:"items"` - } - if json.Unmarshal(result, &list) == nil && len(list.Items) > 0 { - for _, item := range list.Items { - printItem(item) - } - return - } - - var items []completionItem - if json.Unmarshal(result, &items) == nil { - for _, item := range items { - printItem(item) - } - return - } - - fmt.Println("(no completions)") + }) } func printItem(item completionItem) { diff --git a/tools/lsp/cmd/lsp_definition/main.go b/tools/lsp/cmd/lsp_definition/main.go index ee82a83..ac1c9bc 100644 --- a/tools/lsp/cmd/lsp_definition/main.go +++ b/tools/lsp/cmd/lsp_definition/main.go @@ -4,91 +4,50 @@ package main import ( "encoding/json" "fmt" - "os" - "path/filepath" - "strconv" "ollie/tools/lsp" + "ollie/tools/lsp/cmdutil" ) func main() { - if len(os.Args) > 1 && os.Args[1] == "--bridge" { - if err := lsp.RunBridge(); err != nil { - fmt.Fprintf(os.Stderr, "bridge: %v\n", err) - os.Exit(1) - } - return - } - - var args struct { - Path string `json:"path"` - Line string `json:"line"` - Col string `json:"col"` - } - if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } - - file, _ := filepath.Abs(args.Path) - line, _ := strconv.Atoi(args.Line) - col, _ := strconv.Atoi(args.Col) - - result, err := lsp.ClientRequest(&lsp.BridgeRequest{ - Method: "definition", - File: file, - Line: line - 1, // 1-indexed → 0-indexed - Col: col - 1, // 1-indexed → 0-indexed - }) - if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } - - if result == nil || string(result) == "null" { - fmt.Println("(no definition found)") - return - } - - // Parse location(s) - var locations []struct { - URI string `json:"uri"` - TargetURI string `json:"targetUri"` - Range *rang `json:"range"` - TargetRange *rang `json:"targetRange"` - } - // Try as array first - if err := json.Unmarshal(result, &locations); err != nil { - // Try as single location - var single struct { + cmdutil.Run("definition", func(result json.RawMessage) { + var locations []struct { URI string `json:"uri"` TargetURI string `json:"targetUri"` Range *rang `json:"range"` TargetRange *rang `json:"targetRange"` } - if err := json.Unmarshal(result, &single); err != nil { - fmt.Println("(no definition found)") - return + if err := json.Unmarshal(result, &locations); err != nil { + var single struct { + URI string `json:"uri"` + TargetURI string `json:"targetUri"` + Range *rang `json:"range"` + TargetRange *rang `json:"targetRange"` + } + if err := json.Unmarshal(result, &single); err != nil { + fmt.Println("(no definition found)") + return + } + locations = append(locations, single) } - locations = append(locations, single) - } - for _, loc := range locations { - uri := loc.URI - if uri == "" { - uri = loc.TargetURI + for _, loc := range locations { + uri := loc.URI + if uri == "" { + uri = loc.TargetURI + } + path := lsp.URIToPath(uri) + r := loc.Range + if r == nil { + r = loc.TargetRange + } + if r == nil { + fmt.Println(path) + continue + } + fmt.Printf("%s:%d:%d\n", path, r.Start.Line+1, r.Start.Character+1) } - path := lsp.URIToPath(uri) - r := loc.Range - if r == nil { - r = loc.TargetRange - } - if r == nil { - fmt.Println(path) - continue - } - fmt.Printf("%s:%d:%d\n", path, r.Start.Line+1, r.Start.Character+1) - } + }) } type rang struct { diff --git a/tools/lsp/cmd/lsp_diagnostics/main.go b/tools/lsp/cmd/lsp_diagnostics/main.go index 4f2c54a..b70aa4b 100644 --- a/tools/lsp/cmd/lsp_diagnostics/main.go +++ b/tools/lsp/cmd/lsp_diagnostics/main.go @@ -8,50 +8,48 @@ import ( "path/filepath" "ollie/tools/lsp" + "ollie/tools/lsp/cmdutil" ) func main() { - if len(os.Args) > 1 && os.Args[1] == "--bridge" { - lsp.RunBridge() - return - } + cmdutil.RunCustom(func() { + var args struct { + Path string `json:"path"` + } + if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } - var args struct { - Path string `json:"path"` - } - if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } + file, _ := filepath.Abs(args.Path) - file, _ := filepath.Abs(args.Path) + result, err := lsp.ClientRequest(&lsp.BridgeRequest{ + Method: "diagnostics", + File: file, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } - result, err := lsp.ClientRequest(&lsp.BridgeRequest{ - Method: "diagnostics", - File: file, + if result == nil || string(result) == "null" || string(result) == "[]" { + fmt.Println("(no diagnostics)") + return + } + + var diags []diagnostic + if err := json.Unmarshal(result, &diags); err != nil { + fmt.Println("(no diagnostics)") + return + } + + for _, d := range diags { + sev := severity(d.Severity) + line := d.Range.Start.Line + 1 + col := d.Range.Start.Character + 1 + fmt.Printf("%s:%d:%d: %s: %s\n", file, line, col, sev, d.Message) + } }) - if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } - - if result == nil || string(result) == "null" || string(result) == "[]" { - fmt.Println("(no diagnostics)") - return - } - - var diags []diagnostic - if err := json.Unmarshal(result, &diags); err != nil { - fmt.Println("(no diagnostics)") - return - } - - for _, d := range diags { - sev := severity(d.Severity) - line := d.Range.Start.Line + 1 - col := d.Range.Start.Character + 1 - fmt.Printf("%s:%d:%d: %s: %s\n", file, line, col, sev, d.Message) - } } type diagnostic struct { diff --git a/tools/lsp/cmd/lsp_hover/main.go b/tools/lsp/cmd/lsp_hover/main.go index ba97e84..60ba190 100644 --- a/tools/lsp/cmd/lsp_hover/main.go +++ b/tools/lsp/cmd/lsp_hover/main.go @@ -4,66 +4,29 @@ package main import ( "encoding/json" "fmt" - "os" - "path/filepath" - "strconv" - "ollie/tools/lsp" + "ollie/tools/lsp/cmdutil" ) func main() { - if len(os.Args) > 1 && os.Args[1] == "--bridge" { - lsp.RunBridge() - return - } - - var args struct { - Path string `json:"path"` - Line string `json:"line"` - Col string `json:"col"` - } - if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } - - file, _ := filepath.Abs(args.Path) - line, _ := strconv.Atoi(args.Line) - col, _ := strconv.Atoi(args.Col) - - result, err := lsp.ClientRequest(&lsp.BridgeRequest{ - Method: "hover", - File: file, - Line: line - 1, - Col: col - 1, - }) - if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } - - if result == nil || string(result) == "null" { - fmt.Println("(no hover information)") - return - } - - var hover struct { - Contents struct { - Kind string `json:"kind"` - Value string `json:"value"` - } `json:"contents"` - } - if err := json.Unmarshal(result, &hover); err != nil { - // Try as plain string - var plain struct { - Contents string `json:"contents"` + cmdutil.Run("hover", func(result json.RawMessage) { + var hover struct { + Contents struct { + Kind string `json:"kind"` + Value string `json:"value"` + } `json:"contents"` } - if json.Unmarshal(result, &plain) == nil { - fmt.Println(plain.Contents) + if err := json.Unmarshal(result, &hover); err != nil { + var plain struct { + Contents string `json:"contents"` + } + if json.Unmarshal(result, &plain) == nil { + fmt.Println(plain.Contents) + return + } + fmt.Println("(no hover information)") return } - fmt.Println("(no hover information)") - return - } - fmt.Println(hover.Contents.Value) + fmt.Println(hover.Contents.Value) + }) } diff --git a/tools/lsp/cmd/lsp_references/main.go b/tools/lsp/cmd/lsp_references/main.go index bc6b876..704a569 100644 --- a/tools/lsp/cmd/lsp_references/main.go +++ b/tools/lsp/cmd/lsp_references/main.go @@ -4,66 +4,31 @@ package main import ( "encoding/json" "fmt" - "os" - "path/filepath" - "strconv" "ollie/tools/lsp" + "ollie/tools/lsp/cmdutil" ) func main() { - if len(os.Args) > 1 && os.Args[1] == "--bridge" { - lsp.RunBridge() - return - } + cmdutil.Run("references", func(result json.RawMessage) { + var locations []struct { + URI string `json:"uri"` + Range struct { + Start struct { + Line int `json:"line"` + Character int `json:"character"` + } `json:"start"` + } `json:"range"` + } + if err := json.Unmarshal(result, &locations); err != nil { + fmt.Println("(no references found)") + return + } - var args struct { - Path string `json:"path"` - Line string `json:"line"` - Col string `json:"col"` - } - if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } - - file, _ := filepath.Abs(args.Path) - line, _ := strconv.Atoi(args.Line) - col, _ := strconv.Atoi(args.Col) - - result, err := lsp.ClientRequest(&lsp.BridgeRequest{ - Method: "references", - File: file, - Line: line - 1, - Col: col - 1, + fmt.Printf("STATUS=ok MATCH_COUNT=%d\n", len(locations)) + for _, loc := range locations { + path := lsp.URIToPath(loc.URI) + fmt.Printf("%s:%d:%d\n", path, loc.Range.Start.Line+1, loc.Range.Start.Character+1) + } }) - if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } - - if result == nil || string(result) == "null" { - fmt.Println("(no references found)") - return - } - - var locations []struct { - URI string `json:"uri"` - Range struct { - Start struct { - Line int `json:"line"` - Character int `json:"character"` - } `json:"start"` - } `json:"range"` - } - if err := json.Unmarshal(result, &locations); err != nil { - fmt.Println("(no references found)") - return - } - - fmt.Printf("STATUS=ok MATCH_COUNT=%d\n", len(locations)) - for _, loc := range locations { - path := lsp.URIToPath(loc.URI) - fmt.Printf("%s:%d:%d\n", path, loc.Range.Start.Line+1, loc.Range.Start.Character+1) - } } diff --git a/tools/lsp/cmd/lsp_rename/main.go b/tools/lsp/cmd/lsp_rename/main.go index aba371d..cabf5f7 100644 --- a/tools/lsp/cmd/lsp_rename/main.go +++ b/tools/lsp/cmd/lsp_rename/main.go @@ -11,53 +11,50 @@ import ( "strings" "ollie/tools/lsp" + "ollie/tools/lsp/cmdutil" ) func main() { - if len(os.Args) > 1 && os.Args[1] == "--bridge" { - lsp.RunBridge() - return - } + cmdutil.RunCustom(func() { + var args struct { + Path string `json:"path"` + Line string `json:"line"` + Col string `json:"col"` + NewName string `json:"new_name"` + } + if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } - var args struct { - Path string `json:"path"` - Line string `json:"line"` - Col string `json:"col"` - NewName string `json:"new_name"` - } - if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } + file, _ := filepath.Abs(args.Path) + line, _ := strconv.Atoi(args.Line) + col, _ := strconv.Atoi(args.Col) - file, _ := filepath.Abs(args.Path) - line, _ := strconv.Atoi(args.Line) - col, _ := strconv.Atoi(args.Col) + result, err := lsp.ClientRequest(&lsp.BridgeRequest{ + Method: "rename", + File: file, + Line: line - 1, + Col: col - 1, + NewName: args.NewName, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } - result, err := lsp.ClientRequest(&lsp.BridgeRequest{ - Method: "rename", - File: file, - Line: line - 1, - Col: col - 1, - NewName: args.NewName, + if result == nil || string(result) == "null" { + fmt.Println("(rename produced no changes)") + return + } + + summary := applyWorkspaceEdit(result) + if summary == "" { + fmt.Println("(rename produced no changes)") + } else { + fmt.Print(summary) + } }) - if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } - - if result == nil || string(result) == "null" { - fmt.Println("(rename produced no changes)") - return - } - - // Parse WorkspaceEdit and apply - summary := applyWorkspaceEdit(result) - if summary == "" { - fmt.Println("(rename produced no changes)") - } else { - fmt.Print(summary) - } } func applyWorkspaceEdit(raw json.RawMessage) string { @@ -74,7 +71,6 @@ func applyWorkspaceEdit(raw json.RawMessage) string { return "" } - // Normalize to path → edits map changes := make(map[string][]textEdit) if len(edit.DocumentChanges) > 0 { for _, dc := range edit.DocumentChanges { @@ -97,7 +93,6 @@ func applyWorkspaceEdit(raw json.RawMessage) string { } lines := strings.SplitAfter(string(data), "\n") - // Sort edits reverse (bottom-up) to preserve positions sort.Slice(edits, func(i, j int) bool { if edits[i].Range.Start.Line != edits[j].Range.Start.Line { return edits[i].Range.Start.Line > edits[j].Range.Start.Line diff --git a/tools/lsp/cmd/lsp_symbols/main.go b/tools/lsp/cmd/lsp_symbols/main.go index a835987..481182f 100644 --- a/tools/lsp/cmd/lsp_symbols/main.go +++ b/tools/lsp/cmd/lsp_symbols/main.go @@ -8,61 +8,57 @@ import ( "path/filepath" "ollie/tools/lsp" + "ollie/tools/lsp/cmdutil" ) func main() { - if len(os.Args) > 1 && os.Args[1] == "--bridge" { - lsp.RunBridge() - return - } - - var args struct { - Path string `json:"path"` - Query string `json:"query"` - } - if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } - - file, _ := filepath.Abs(args.Path) - workspace := args.Query != "" - - result, err := lsp.ClientRequest(&lsp.BridgeRequest{ - Method: "symbols", - File: file, - Query: args.Query, - Workspace: workspace, - }) - if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } - - if result == nil || string(result) == "null" { - fmt.Println("(no symbols found)") - return - } - - // Try document symbols (hierarchical) - var docSymbols []documentSymbol - if json.Unmarshal(result, &docSymbols) == nil && len(docSymbols) > 0 { - printDocSymbols(docSymbols, 0) - return - } - - // Try workspace symbols (flat) - var wsSymbols []workspaceSymbol - if json.Unmarshal(result, &wsSymbols) == nil && len(wsSymbols) > 0 { - for _, s := range wsSymbols { - path := lsp.URIToPath(s.Location.URI) - line := s.Location.Range.Start.Line + 1 - fmt.Printf("%s:%d %s [%s]\n", path, line, s.Name, symbolKind(s.Kind)) + cmdutil.RunCustom(func() { + var args struct { + Path string `json:"path"` + Query string `json:"query"` + } + if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) } - return - } - fmt.Println("(no symbols found)") + file, _ := filepath.Abs(args.Path) + workspace := args.Query != "" + + result, err := lsp.ClientRequest(&lsp.BridgeRequest{ + Method: "symbols", + File: file, + Query: args.Query, + Workspace: workspace, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + if result == nil || string(result) == "null" { + fmt.Println("(no symbols found)") + return + } + + var docSymbols []documentSymbol + if json.Unmarshal(result, &docSymbols) == nil && len(docSymbols) > 0 { + printDocSymbols(docSymbols, 0) + return + } + + var wsSymbols []workspaceSymbol + if json.Unmarshal(result, &wsSymbols) == nil && len(wsSymbols) > 0 { + for _, s := range wsSymbols { + path := lsp.URIToPath(s.Location.URI) + line := s.Location.Range.Start.Line + 1 + fmt.Printf("%s:%d %s [%s]\n", path, line, s.Name, symbolKind(s.Kind)) + } + return + } + + fmt.Println("(no symbols found)") + }) } func printDocSymbols(symbols []documentSymbol, indent int) { diff --git a/tools/lsp/cmdutil/cmdutil.go b/tools/lsp/cmdutil/cmdutil.go new file mode 100644 index 0000000..0ce2c37 --- /dev/null +++ b/tools/lsp/cmdutil/cmdutil.go @@ -0,0 +1,85 @@ +// Package cmdutil provides shared boilerplate for LSP tool command binaries. +package cmdutil + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + + "ollie/tools/lsp" +) + +// PositionArgs are the common args for position-based LSP commands. +type PositionArgs struct { + Path string `json:"path"` + Line string `json:"line"` + Col string `json:"col"` +} + +// ResolvePosition converts the string fields to absolute path and 0-indexed ints. +func (a PositionArgs) ResolvePosition() (file string, line, col int) { + file, _ = filepath.Abs(a.Path) + l, _ := strconv.Atoi(a.Line) + c, _ := strconv.Atoi(a.Col) + return file, l - 1, c - 1 +} + +// Run is the common entry point for position-based LSP commands. +// It handles --bridge, reads PositionArgs from stdin, dispatches the request, +// and passes the result to format. If format is nil, raw JSON is printed. +func Run(method string, format func(json.RawMessage)) { + if len(os.Args) > 1 && os.Args[1] == "--bridge" { + if err := lsp.RunBridge(); err != nil { + fmt.Fprintf(os.Stderr, "bridge: %v\n", err) + os.Exit(1) + } + return + } + + var args PositionArgs + if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + file, line, col := args.ResolvePosition() + + result, err := lsp.ClientRequest(&lsp.BridgeRequest{ + Method: method, + File: file, + Line: line, + Col: col, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + if result == nil || string(result) == "null" { + fmt.Printf("(no %s found)\n", method) + return + } + + if format != nil { + format(result) + } else { + os.Stdout.Write(result) + fmt.Println() + } +} + +// RunCustom handles --bridge and then calls fn for custom argument handling. +// Use for commands that don't follow the simple position-args pattern +// (e.g. diagnostics, rename, symbols). +func RunCustom(fn func()) { + if len(os.Args) > 1 && os.Args[1] == "--bridge" { + if err := lsp.RunBridge(); err != nil { + fmt.Fprintf(os.Stderr, "bridge: %v\n", err) + os.Exit(1) + } + return + } + fn() +} diff --git a/toolsrv/rpcwire.go b/toolsrv/rpcwire.go new file mode 100644 index 0000000..03e4d3d --- /dev/null +++ b/toolsrv/rpcwire.go @@ -0,0 +1,22 @@ +package toolsrv + +import "encoding/json" + +// RPCRequest is a JSON-RPC 2.0 request. +type RPCRequest struct { + JSONRPC string `json:"jsonrpc"` + ID json.RawMessage `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` +} + +// RPCError is a JSON-RPC 2.0 error object. +type RPCError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// OutputNotification carries streamed tool output data. +type OutputNotification struct { + Data string `json:"data"` +} diff --git a/toolsrv/transport.go b/toolsrv/transport.go index 1a445d6..2dcfd58 100644 --- a/toolsrv/transport.go +++ b/toolsrv/transport.go @@ -46,7 +46,7 @@ func localDial(ctx context.Context, cwd string, opts ...Option) (*transport, err } // Generate a unique socket path. - sockDir := filepath.Join(runtimeDir(), "ollie") + sockDir := filepath.Join(paths.RuntimeDir(), "ollie") os.MkdirAll(sockDir, 0700) sockPath := filepath.Join(sockDir, fmt.Sprintf("toolsrv-%d-%d.sock", os.Getpid(), socketSeq.Add(1))) @@ -137,7 +137,7 @@ func sshDial(ctx context.Context, cfg RemoteConfig) (*transport, error) { } // Generate socket paths. - sockDir := filepath.Join(runtimeDir(), "ollie") + sockDir := filepath.Join(paths.RuntimeDir(), "ollie") os.MkdirAll(sockDir, 0700) localSock := filepath.Join(sockDir, fmt.Sprintf("remote-%s-%d-%d.sock", cfg.SSHTarget, os.Getpid(), socketSeq.Add(1))) remoteSock := fmt.Sprintf("/tmp/ollie-toolsrv-%d.sock", os.Getpid()) @@ -285,11 +285,3 @@ func shellEscape(s string) string { } return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" } - -// runtimeDir returns XDG_RUNTIME_DIR or a fallback. -func runtimeDir() string { - if d := os.Getenv("XDG_RUNTIME_DIR"); d != "" { - return d - } - return filepath.Join(os.TempDir(), fmt.Sprintf("runtime-%d", os.Getuid())) -} \ No newline at end of file