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():
}
}()
}
|