summaryrefslogtreecommitdiff
path: root/internal/lsp/context.go
blob: 02aa40a61978156cc5b41f93c604ff818bc1b31d (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
// Summary: Builds additional context snippets based on configured mode and truncates text by token heuristic.
// Not yet reviewed by a human
package lsp

import (
	"hexai/internal/logging"
	"strings"
)

// buildAdditionalContext builds extra context messages based on the configured mode.
// Modes:
// - minimal: no extra context
// - window: include a window of lines around the cursor
// - file-on-new-func: include full file only when defining a new function
// - always-full: always include the full file
func (s *Server) buildAdditionalContext(newFunc bool, uri string, pos Position) (string, bool) {
	mode := s.contextMode
	switch mode {
	case "minimal":
		return "", false
	case "window":
		return s.windowContext(uri, pos), true
	case "file-on-new-func":
		if newFunc {
			return s.fullFileContext(uri), true
		}
		return "", false
	case "always-full":
		return s.fullFileContext(uri), true
	default:
		// fallback to minimal if unknown
		return "", false
	}
}

func (s *Server) windowContext(uri string, pos Position) string {
	d := s.getDocument(uri)
	if d == nil || len(d.lines) == 0 {
		logging.Logf("lsp ", "context: window requested but document not open; skipping uri=%s", uri)
		return ""
	}
	n := len(d.lines)
	half := s.windowLines / 2
	start := pos.Line - half
	if start < 0 {
		start = 0
	}
	end := pos.Line + half + 1
	if end > n {
		end = n
	}
	text := strings.Join(d.lines[start:end], "\n")
	return truncateToApproxTokens(text, s.maxContextTokens)
}

func (s *Server) fullFileContext(uri string) string {
	d := s.getDocument(uri)
	if d == nil {
		logging.Logf("lsp ", "context: full-file requested but document not open; skipping uri=%s", uri)
		return ""
	}
	return truncateToApproxTokens(d.text, s.maxContextTokens)
}

// truncateToApproxTokens naively truncates the input to fit approx N tokens.
// Uses 4 chars/token heuristic for speed and determinism.
func truncateToApproxTokens(text string, maxTokens int) string {
	if maxTokens <= 0 {
		return ""
	}
	maxChars := maxTokens * 4
	if len(text) <= maxChars {
		return text
	}
	// try to cut on a line boundary near maxChars
	cut := maxChars
	if cut > len(text) {
		cut = len(text)
	}
	if i := strings.LastIndex(text[:cut], "\n"); i > 0 {
		cut = i
	}
	return text[:cut]
}