// lsp_rename — Rename symbol across workspace. Applies edits directly. package main import ( "encoding/json" "fmt" "os" "path/filepath" "sort" "strconv" "strings" "ollie/tools/lsp/internal/lsp" "ollie/tools/lsp/internal/cmdutil" ) func main() { 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) } 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) } 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) } }) } func applyWorkspaceEdit(raw json.RawMessage) string { var edit struct { Changes map[string][]textEdit `json:"changes"` DocumentChanges []struct { TextDocument struct { URI string `json:"uri"` } `json:"textDocument"` Edits []textEdit `json:"edits"` } `json:"documentChanges"` } if err := json.Unmarshal(raw, &edit); err != nil { return "" } changes := make(map[string][]textEdit) if len(edit.DocumentChanges) > 0 { for _, dc := range edit.DocumentChanges { path := lsp.URIToPath(dc.TextDocument.URI) changes[path] = append(changes[path], dc.Edits...) } } else { for uri, edits := range edit.Changes { path := lsp.URIToPath(uri) changes[path] = edits } } var sb strings.Builder totalEdits := 0 for path, edits := range changes { data, err := os.ReadFile(path) if err != nil { continue } lines := strings.SplitAfter(string(data), "\n") 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 } return edits[i].Range.Start.Character > edits[j].Range.Start.Character }) for _, e := range edits { sl, sc := e.Range.Start.Line, e.Range.Start.Character el, ec := e.Range.End.Line, e.Range.End.Character if sl >= len(lines) { continue } before := strings.Join(lines[:sl], "") + lines[sl][:min(sc, len(lines[sl]))] var after string if el < len(lines) { after = lines[el][min(ec, len(lines[el])):] + strings.Join(lines[el+1:], "") } content := before + e.NewText + after lines = strings.SplitAfter(content, "\n") } os.WriteFile(path, []byte(strings.Join(lines, "")), 0644) totalEdits += len(edits) fmt.Fprintf(&sb, "%s: %d edit(s)\n", path, len(edits)) } fmt.Fprintf(&sb, "total: %d file(s), %d edit(s)\n", len(changes), totalEdits) return sb.String() } type textEdit struct { Range struct { Start struct { Line int `json:"line"` Character int `json:"character"` } `json:"start"` End struct { Line int `json:"line"` Character int `json:"character"` } `json:"end"` } `json:"range"` NewText string `json:"newText"` } func min(a, b int) int { if a < b { return a } return b }