summaryrefslogtreecommitdiff
path: root/internal/lsp/handlers_document.go
blob: 5b285ab71fd123d04d7bc73a78bada2177de6a6a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// Document open/change/close handlers plus the shared client-edit transport
// helpers (workspace/applyEdit, window/showDocument). The in-editor chat logic
// that used to live here was extracted into chatService (chat_handlers.go);
// these handlers now delegate chat detection to s.chatSvc().
package lsp

import (
	"context"
	"encoding/json"
	"strings"
	"time"

	"codeberg.org/snonux/hexai/internal/logging"
)

func (s *Server) handleDidOpen(req Request) {
	var p DidOpenTextDocumentParams
	if err := json.Unmarshal(req.Params, &p); err == nil {
		s.setDocument(p.TextDocument.URI, p.TextDocument.Text)
		s.markActivity()
		// Log when an ignored file is opened (document still stored for editor sync)
		if ignored, reason := s.isFileIgnored(p.TextDocument.URI); ignored {
			logging.Logf("lsp ", "file opened (ignored): %s (%s)", p.TextDocument.URI, reason)
		}
	}
}

func (s *Server) handleDidChange(req Request) {
	var p DidChangeTextDocumentParams
	if err := json.Unmarshal(req.Params, &p); err == nil {
		if len(p.ContentChanges) > 0 {
			s.setDocument(p.TextDocument.URI, p.ContentChanges[len(p.ContentChanges)-1].Text)
		}
		s.markActivity()
		// Detect in-editor chat trigger lines and respond inline.
		s.chatSvc().detectAndHandleChat(p.TextDocument.URI)
	}
}

func (s *Server) handleDidClose(req Request) {
	var p DidCloseTextDocumentParams
	if err := json.Unmarshal(req.Params, &p); err == nil {
		s.deleteDocument(p.TextDocument.URI)
		s.markActivity()
	}
}

// docBeforeAfter returns the full document text split at the given position.
// The returned strings are the text before the cursor (inclusive of anything
// left of the position) and the text after the cursor.
func (s *Server) docBeforeAfter(uri string, pos Position) (string, string) {
	d := s.getDocument(uri)
	if d == nil {
		return "", ""
	}
	// Clamp indices
	line := pos.Line
	if line < 0 {
		line = 0
	}
	if line >= len(d.lines) {
		line = len(d.lines) - 1
	}
	col := pos.Character
	if col < 0 {
		col = 0
	}
	if col > len(d.lines[line]) {
		col = len(d.lines[line])
	}
	// Build before
	var b strings.Builder
	for i := 0; i < line; i++ {
		b.WriteString(d.lines[i])
		b.WriteByte('\n')
	}
	b.WriteString(d.lines[line][:col])
	before := b.String()
	// Build after
	var a strings.Builder
	a.WriteString(d.lines[line][col:])
	for i := line + 1; i < len(d.lines); i++ {
		a.WriteByte('\n')
		a.WriteString(d.lines[i])
	}
	return before, a.String()
}

// clientApplyEdit sends a workspace/applyEdit request to the client.
func (s *Server) clientApplyEdit(label string, edit WorkspaceEdit) {
	params := ApplyWorkspaceEditParams{Label: label, Edit: edit}
	b, err := json.Marshal(params)
	if err != nil {
		logging.Logf("lsp ", "clientApplyEdit: marshal error: %v", err)
		return
	}
	id := s.nextReqID()
	req := Request{JSONRPC: "2.0", ID: id, Method: "workspace/applyEdit"}
	req.Params = b
	s.writeMessage(req)
}

// nextReqID returns a unique json.RawMessage id for server-initiated requests.
func (s *Server) nextReqID() json.RawMessage {
	s.mu.Lock()
	s.nextID++
	idNum := s.nextID
	s.mu.Unlock()
	b, _ := json.Marshal(idNum)
	return b
}

// clientShowDocument asks the client to open/focus a document and select a range.
func (s *Server) clientShowDocument(uri string, sel *Range) {
	var params struct {
		URI       string `json:"uri"`
		External  bool   `json:"external,omitempty"`
		TakeFocus bool   `json:"takeFocus,omitempty"`
		Selection *Range `json:"selection,omitempty"`
	}
	params.URI = uri
	params.TakeFocus = true
	params.Selection = sel
	b, err := json.Marshal(params)
	if err != nil {
		logging.Logf("lsp ", "clientShowDocument: marshal error: %v", err)
		return
	}
	id := s.nextReqID()
	req := Request{JSONRPC: "2.0", ID: id, Method: "window/showDocument"}
	req.Params = b
	s.writeMessage(req)
}

// deferShowDocument schedules a showDocument after a short delay to allow the client
// time to apply any pending edits (e.g., create the file before focusing it).
// The goroutine respects s.serverCtx so it won't write after shutdown.
func (s *Server) deferShowDocument(uri string, sel Range) {
	ctx := s.serverCtx
	if ctx == nil {
		// Fallback for tests that don't set a server context.
		ctx = context.Background()
	}
	s.inflight.Add(1)
	go func() {
		defer s.inflight.Done()
		timer := time.NewTimer(120 * time.Millisecond)
		defer timer.Stop()
		select {
		case <-timer.C:
			s.clientShowDocument(uri, &sel)
		case <-ctx.Done():
		}
	}()
}