| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- package hostbridge
- import (
- "context"
- "fmt"
- "io/ioutil"
- "log"
- "os"
- "path/filepath"
- "strings"
- "sync"
- "sync/atomic"
- proto "github.com/cline/grpc-go/host"
- )
- // diffSession represents an in-memory diff editing session
- type diffSession struct {
- originalPath string // File path from OpenDiff request
- originalContent []byte // Original file content (for comparison)
- currentContent []byte // Current modified content
- lines []string // Current content split into lines
- encoding string // File encoding (default: utf8)
- }
- // DiffService implements the proto.DiffServiceServer interface
- type DiffService struct {
- proto.UnimplementedDiffServiceServer
- verbose bool
- sessions *sync.Map // thread-safe: diffId -> *diffSession
- counter *int64 // atomic counter for unique IDs
- }
- // NewDiffService creates a new DiffService
- func NewDiffService(verbose bool) *DiffService {
- counter := int64(0)
- return &DiffService{
- verbose: verbose,
- sessions: &sync.Map{},
- counter: &counter,
- }
- }
- // generateDiffID creates a unique diff ID
- func (s *DiffService) generateDiffID() string {
- id := atomic.AddInt64(s.counter, 1)
- return fmt.Sprintf("diff_%d_%d", os.Getpid(), id)
- }
- // splitLines splits content into lines, preserving line ending information
- func splitLines(content string) []string {
- if content == "" {
- return []string{}
- }
- lines := []string{}
- current := ""
- for _, char := range content {
- if char == '\n' {
- lines = append(lines, current)
- current = ""
- } else if char != '\r' { // Skip \r characters, handle \r\n as \n
- current += string(char)
- }
- }
- // Add the last line if it doesn't end with newline
- if current != "" {
- lines = append(lines, current)
- }
- return lines
- }
- // joinLines joins lines back into content with newlines
- func joinLines(lines []string) string {
- if len(lines) == 0 {
- return ""
- }
- return strings.Join(lines, "\n")
- }
- // OpenDiff opens a diff view for the specified file
- func (s *DiffService) OpenDiff(ctx context.Context, req *proto.OpenDiffRequest) (*proto.OpenDiffResponse, error) {
- if s.verbose {
- log.Printf("OpenDiff called for path: %s", req.GetPath())
- }
- diffID := s.generateDiffID()
- var originalContent []byte
- // Check if file exists and read original content
- if req.GetPath() != "" {
- if _, err := os.Stat(req.GetPath()); err == nil {
- // File exists, read its content
- var readErr error
- originalContent, readErr = ioutil.ReadFile(req.GetPath())
- if readErr != nil {
- return nil, fmt.Errorf("failed to read original file: %w", readErr)
- }
- } else {
- // File doesn't exist, use empty content
- originalContent = []byte{}
- }
- }
- // Use provided content as the initial current content
- currentContent := []byte(req.GetContent())
- // Create the diff session
- session := &diffSession{
- originalPath: req.GetPath(),
- originalContent: originalContent,
- currentContent: currentContent,
- lines: splitLines(req.GetContent()),
- encoding: "utf8", // Default encoding
- }
- // Store the session
- s.sessions.Store(diffID, session)
- if s.verbose {
- log.Printf("Created diff session: %s (original: %d bytes, current: %d bytes)",
- diffID, len(originalContent), len(currentContent))
- }
- return &proto.OpenDiffResponse{
- DiffId: &diffID,
- }, nil
- }
- // GetDocumentText returns the current content of the diff document
- func (s *DiffService) GetDocumentText(ctx context.Context, req *proto.GetDocumentTextRequest) (*proto.GetDocumentTextResponse, error) {
- if s.verbose {
- log.Printf("GetDocumentText called for diff ID: %s", req.GetDiffId())
- }
- sessionInterface, exists := s.sessions.Load(req.GetDiffId())
- if !exists {
- return nil, fmt.Errorf("diff session not found: %s", req.GetDiffId())
- }
- session := sessionInterface.(*diffSession)
- content := string(session.currentContent)
- return &proto.GetDocumentTextResponse{
- Content: &content,
- }, nil
- }
- // ReplaceText replaces text in the diff document using line-based operations
- func (s *DiffService) ReplaceText(ctx context.Context, req *proto.ReplaceTextRequest) (*proto.ReplaceTextResponse, error) {
- if s.verbose {
- log.Printf("ReplaceText called for diff ID: %s, lines %d-%d",
- req.GetDiffId(), req.GetStartLine(), req.GetEndLine())
- }
- sessionInterface, exists := s.sessions.Load(req.GetDiffId())
- if !exists {
- return nil, fmt.Errorf("diff session not found: %s", req.GetDiffId())
- }
- session := sessionInterface.(*diffSession)
- startLine := int(req.GetStartLine())
- endLine := int(req.GetEndLine())
- newContent := req.GetContent()
- // Validate line ranges
- if startLine < 0 {
- startLine = 0
- }
- if endLine < startLine {
- endLine = startLine
- }
- // Split new content into lines
- newLines := splitLines(newContent)
- // Ensure we have enough lines in the current content
- for len(session.lines) < endLine {
- session.lines = append(session.lines, "")
- }
- // Replace the specified line range
- if endLine > len(session.lines) {
- // Extending beyond current content - append new lines
- session.lines = append(session.lines[:startLine], newLines...)
- } else {
- // Replace within existing content
- result := make([]string, 0, len(session.lines)-endLine+startLine+len(newLines))
- result = append(result, session.lines[:startLine]...)
- result = append(result, newLines...)
- result = append(result, session.lines[endLine:]...)
- session.lines = result
- }
- // Update current content
- session.currentContent = []byte(joinLines(session.lines))
- // Store the updated session
- s.sessions.Store(req.GetDiffId(), session)
- if s.verbose {
- log.Printf("Updated diff session %s: %d lines, %d bytes",
- req.GetDiffId(), len(session.lines), len(session.currentContent))
- }
- return &proto.ReplaceTextResponse{}, nil
- }
- // ScrollDiff scrolls the diff view to a specific line (no-op for CLI)
- func (s *DiffService) ScrollDiff(ctx context.Context, req *proto.ScrollDiffRequest) (*proto.ScrollDiffResponse, error) {
- if s.verbose {
- log.Printf("ScrollDiff called for diff ID: %s, line: %d", req.GetDiffId(), req.GetLine())
- }
- // Verify session exists
- if _, exists := s.sessions.Load(req.GetDiffId()); !exists {
- return nil, fmt.Errorf("diff session not found: %s", req.GetDiffId())
- }
- // In a CLI implementation, scrolling is a no-op
- // In a GUI implementation, this would scroll the view to the specified line
- return &proto.ScrollDiffResponse{}, nil
- }
- // TruncateDocument truncates the diff document at the specified line
- func (s *DiffService) TruncateDocument(ctx context.Context, req *proto.TruncateDocumentRequest) (*proto.TruncateDocumentResponse, error) {
- if s.verbose {
- log.Printf("TruncateDocument called for diff ID: %s, end line: %d", req.GetDiffId(), req.GetEndLine())
- }
- sessionInterface, exists := s.sessions.Load(req.GetDiffId())
- if !exists {
- return nil, fmt.Errorf("diff session not found: %s", req.GetDiffId())
- }
- session := sessionInterface.(*diffSession)
- endLine := int(req.GetEndLine())
- // Truncate lines at the specified position
- if endLine >= 0 && endLine < len(session.lines) {
- session.lines = session.lines[:endLine]
- session.currentContent = []byte(joinLines(session.lines))
- // Store the updated session
- s.sessions.Store(req.GetDiffId(), session)
- if s.verbose {
- log.Printf("Truncated diff session %s to %d lines", req.GetDiffId(), len(session.lines))
- }
- }
- return &proto.TruncateDocumentResponse{}, nil
- }
- // SaveDocument saves the diff document to the original file
- func (s *DiffService) SaveDocument(ctx context.Context, req *proto.SaveDocumentRequest) (*proto.SaveDocumentResponse, error) {
- if s.verbose {
- log.Printf("SaveDocument called for diff ID: %s", req.GetDiffId())
- }
- sessionInterface, exists := s.sessions.Load(req.GetDiffId())
- if !exists {
- return nil, fmt.Errorf("diff session not found: %s", req.GetDiffId())
- }
- session := sessionInterface.(*diffSession)
- if session.originalPath == "" {
- return nil, fmt.Errorf("no file path specified for diff session: %s", req.GetDiffId())
- }
- // Create parent directories if they don't exist
- dir := filepath.Dir(session.originalPath)
- if err := os.MkdirAll(dir, 0755); err != nil {
- return nil, fmt.Errorf("failed to create directories: %w", err)
- }
- // Write the current content to the original file
- if err := ioutil.WriteFile(session.originalPath, session.currentContent, 0644); err != nil {
- return nil, fmt.Errorf("failed to save file: %w", err)
- }
- if s.verbose {
- log.Printf("Saved diff session %s to file: %s (%d bytes)",
- req.GetDiffId(), session.originalPath, len(session.currentContent))
- }
- return &proto.SaveDocumentResponse{}, nil
- }
- // CloseAllDiffs closes all diff views and cleans up all sessions
- func (s *DiffService) CloseAllDiffs(ctx context.Context, req *proto.CloseAllDiffsRequest) (*proto.CloseAllDiffsResponse, error) {
- if s.verbose {
- log.Printf("CloseAllDiffs called")
- }
- var count int64
- s.sessions.Range(func(key, value any) bool {
- // Optional: attempt to close if the value supports it
- if c, ok := value.(interface{ Close() error }); ok {
- _ = c.Close() // best-effort; ignore error
- }
- s.sessions.Delete(key)
- atomic.AddInt64(&count, 1)
- return true
- })
- if s.verbose {
- log.Printf("Closed %d diff sessions", count)
- }
- return &proto.CloseAllDiffsResponse{}, nil
- }
- // OpenMultiFileDiff displays a diff view comparing before/after states for multiple files
- func (s *DiffService) OpenMultiFileDiff(ctx context.Context, req *proto.OpenMultiFileDiffRequest) (*proto.OpenMultiFileDiffResponse, error) {
- if s.verbose {
- log.Printf("OpenMultiFileDiff called with title: %s, %d files", req.GetTitle(), len(req.GetDiffs()))
- }
- // In a CLI implementation, we could display the diffs to console
- // For now, we'll just log the information
- title := req.GetTitle()
- if title == "" {
- title = "Multi-file diff"
- }
- if s.verbose {
- log.Printf("=== %s ===", title)
- for i, diff := range req.GetDiffs() {
- log.Printf("File %d: %s", i+1, diff.GetFilePath())
- log.Printf(" Left content: %d bytes", len(diff.GetLeftContent()))
- log.Printf(" Right content: %d bytes", len(diff.GetRightContent()))
- }
- }
- // In a more sophisticated CLI implementation, we could:
- // 1. Use a diff library to generate unified diffs
- // 2. Display them with colors
- // 3. Allow navigation between files
- // For now, this is a no-op that just acknowledges the request
- return &proto.OpenMultiFileDiffResponse{}, nil
- }
|