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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
|
// Summary: Hexai LSP runner; configures logging, loads config, builds the LLM client,
// and constructs/runs the LSP server (with injectable factory for tests).
package hexailsp
import (
"io"
"log"
"os"
"strings"
"codeberg.org/snonux/hexai/internal/appconfig"
"codeberg.org/snonux/hexai/internal/llm"
"codeberg.org/snonux/hexai/internal/logging"
"codeberg.org/snonux/hexai/internal/lsp"
)
// ServerRunner is the minimal interface satisfied by lsp.Server.
type ServerRunner interface{ Run() error }
// ServerFactory creates a ServerRunner. Default uses lsp.NewServer.
type ServerFactory func(r io.Reader, w io.Writer, logger *log.Logger, opts lsp.ServerOptions) ServerRunner
// Run configures logging, loads config, builds the LLM client and runs the LSP server.
// It is thin and delegates to RunWithFactory for testability.
func Run(logPath string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error {
logger := log.New(stderr, "hexai-lsp ", log.LstdFlags|log.Lmsgprefix)
if strings.TrimSpace(logPath) != "" {
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
logger.Fatalf("failed to open log file: %v", err)
}
defer f.Close()
logger.SetOutput(f)
}
logging.Bind(logger)
cfg := appconfig.Load(logger)
if err := cfg.Validate(); err != nil {
logger.Fatalf("invalid config: %v", err)
}
return RunWithFactory(logPath, stdin, stdout, logger, cfg, nil, nil)
}
// RunWithFactory is the testable entrypoint. When client is nil, it is built from cfg+env.
// When factory is nil, lsp.NewServer is used.
func RunWithFactory(logPath string, stdin io.Reader, stdout io.Writer, logger *log.Logger, cfg appconfig.App, client llm.Client, factory ServerFactory) error {
normalizeLoggingConfig(&cfg)
if err := cfg.Validate(); err != nil {
logger.Fatalf("invalid config: %v", err)
}
client = buildClientIfNil(cfg, client)
factory = ensureFactory(factory)
opts := makeServerOptions(cfg, strings.TrimSpace(logPath) != "", client)
server := factory(stdin, stdout, logger, opts)
if err := server.Run(); err != nil {
logger.Fatalf("server error: %v", err)
}
return nil
}
// --- helpers to keep RunWithFactory small ---
func normalizeLoggingConfig(cfg *appconfig.App) {
cfg.ContextMode = strings.ToLower(strings.TrimSpace(cfg.ContextMode))
if cfg.LogPreviewLimit >= 0 {
logging.SetLogPreviewLimit(cfg.LogPreviewLimit)
}
}
func buildClientIfNil(cfg appconfig.App, client llm.Client) llm.Client {
if client != nil {
return client
}
llmCfg := llm.Config{
Provider: cfg.Provider,
OpenAIBaseURL: cfg.OpenAIBaseURL,
OpenAIModel: cfg.OpenAIModel,
OpenAITemperature: cfg.OpenAITemperature,
OllamaBaseURL: cfg.OllamaBaseURL,
OllamaModel: cfg.OllamaModel,
OllamaTemperature: cfg.OllamaTemperature,
CopilotBaseURL: cfg.CopilotBaseURL,
CopilotModel: cfg.CopilotModel,
CopilotTemperature: cfg.CopilotTemperature,
}
// Prefer HEXAI_OPENAI_API_KEY; fall back to OPENAI_API_KEY
oaKey := os.Getenv("HEXAI_OPENAI_API_KEY")
if strings.TrimSpace(oaKey) == "" {
oaKey = os.Getenv("OPENAI_API_KEY")
}
// Prefer HEXAI_COPILOT_API_KEY; fall back to COPILOT_API_KEY
cpKey := os.Getenv("HEXAI_COPILOT_API_KEY")
if strings.TrimSpace(cpKey) == "" {
cpKey = os.Getenv("COPILOT_API_KEY")
}
if c, err := llm.NewFromConfig(llmCfg, oaKey, cpKey); err != nil {
logging.Logf("lsp ", "llm disabled: %v", err)
return nil
} else {
logging.Logf("lsp ", "llm enabled provider=%s model=%s", c.Name(), c.DefaultModel())
return c
}
}
func ensureFactory(factory ServerFactory) ServerFactory {
if factory != nil {
return factory
}
return func(r io.Reader, w io.Writer, logger *log.Logger, opts lsp.ServerOptions) ServerRunner {
return lsp.NewServer(r, w, logger, opts)
}
}
func makeServerOptions(cfg appconfig.App, logContext bool, client llm.Client) lsp.ServerOptions {
// Map custom actions from appconfig to lsp type
var customs []lsp.CustomAction
if len(cfg.CustomActions) > 0 {
customs = make([]lsp.CustomAction, 0, len(cfg.CustomActions))
for _, ca := range cfg.CustomActions {
customs = append(customs, lsp.CustomAction{
ID: ca.ID,
Title: ca.Title,
Kind: ca.Kind,
Scope: ca.Scope,
Instruction: ca.Instruction,
System: ca.System,
User: ca.User,
})
}
}
return lsp.ServerOptions{
LogContext: logContext,
MaxTokens: cfg.MaxTokens,
ContextMode: cfg.ContextMode,
WindowLines: cfg.ContextWindowLines,
MaxContextTokens: cfg.MaxContextTokens,
CodingTemperature: cfg.CodingTemperature,
Client: client,
TriggerCharacters: cfg.TriggerCharacters,
ManualInvokeMinPrefix: cfg.ManualInvokeMinPrefix,
CompletionDebounceMs: cfg.CompletionDebounceMs,
CompletionThrottleMs: cfg.CompletionThrottleMs,
InlineOpen: cfg.InlineOpen,
InlineClose: cfg.InlineClose,
ChatSuffix: cfg.ChatSuffix,
ChatPrefixes: cfg.ChatPrefixes,
// Prompts
PromptCompSysGeneral: cfg.PromptCompletionSystemGeneral,
PromptCompSysParams: cfg.PromptCompletionSystemParams,
PromptCompSysInline: cfg.PromptCompletionSystemInline,
PromptCompUserGeneral: cfg.PromptCompletionUserGeneral,
PromptCompUserParams: cfg.PromptCompletionUserParams,
PromptCompExtraHeader: cfg.PromptCompletionExtraHeader,
PromptNativeCompletion: cfg.PromptNativeCompletion,
PromptChatSystem: cfg.PromptChatSystem,
PromptRewriteSystem: cfg.PromptCodeActionRewriteSystem,
PromptDiagnosticsSystem: cfg.PromptCodeActionDiagnosticsSystem,
PromptDocumentSystem: cfg.PromptCodeActionDocumentSystem,
PromptRewriteUser: cfg.PromptCodeActionRewriteUser,
PromptDiagnosticsUser: cfg.PromptCodeActionDiagnosticsUser,
PromptDocumentUser: cfg.PromptCodeActionDocumentUser,
PromptGoTestSystem: cfg.PromptCodeActionGoTestSystem,
PromptGoTestUser: cfg.PromptCodeActionGoTestUser,
PromptSimplifySystem: cfg.PromptCodeActionSimplifySystem,
PromptSimplifyUser: cfg.PromptCodeActionSimplifyUser,
CustomActions: customs,
}
}
|