summaryrefslogtreecommitdiff
path: root/internal/hexailsp/run.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-08-17 18:52:51 +0300
committerPaul Buetow <paul@buetow.org>2025-08-17 18:52:51 +0300
commit454451105ad3522d2ac3d22136eedee4a4d034af (patch)
treeaa4b5723809c4d45bfc9094a38c01c6415582f9c /internal/hexailsp/run.go
parent498923e77c201ca90dc35c7934f4f7f1c9c3ccd2 (diff)
cli+lsp: refactor main packages into internal runners; add tests
- Move CLI logic to internal/hexaicli with Run/RunWithClient - Move LSP logic to internal/hexailsp with Run/RunWithFactory - Extract helpers; keep behavior identical for both binaries - Add unit tests for hexaicli (input parsing, messages, streaming) and hexailsp (factory wiring, client creation, logging settings) - Add top-of-file summaries and 'Not yet reviewed by a human' comments to all Go files - Update README with internal package docs
Diffstat (limited to 'internal/hexailsp/run.go')
-rw-r--r--internal/hexailsp/run.go91
1 files changed, 91 insertions, 0 deletions
diff --git a/internal/hexailsp/run.go b/internal/hexailsp/run.go
new file mode 100644
index 0000000..2231fc2
--- /dev/null
+++ b/internal/hexailsp/run.go
@@ -0,0 +1,91 @@
+// Summary: Hexai LSP runner; configures logging, loads config, builds the LLM client,
+// and constructs/runs the LSP server (with injectable factory for tests).
+// Not yet reviewed by a human
+package hexailsp
+
+import (
+ "log"
+ "os"
+ "strings"
+ "io"
+
+ "hexai/internal/appconfig"
+ "hexai/internal/llm"
+ "hexai/internal/logging"
+ "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, 0644)
+ 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)
+ 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 {
+ // Normalize and apply logging config
+ cfg.ContextMode = strings.ToLower(strings.TrimSpace(cfg.ContextMode))
+ if cfg.LogPreviewLimit >= 0 {
+ logging.SetLogPreviewLimit(cfg.LogPreviewLimit)
+ }
+
+ // Build LLM client if not provided
+ if client == nil {
+ llmCfg := llm.Config{
+ Provider: cfg.Provider,
+ OpenAIBaseURL: cfg.OpenAIBaseURL,
+ OpenAIModel: cfg.OpenAIModel,
+ OllamaBaseURL: cfg.OllamaBaseURL,
+ OllamaModel: cfg.OllamaModel,
+ CopilotBaseURL: cfg.CopilotBaseURL,
+ CopilotModel: cfg.CopilotModel,
+ }
+ oaKey := os.Getenv("OPENAI_API_KEY")
+ cpKey := os.Getenv("COPILOT_API_KEY")
+ if c, err := llm.NewFromConfig(llmCfg, oaKey, cpKey); err != nil {
+ logging.Logf("lsp ", "llm disabled: %v", err)
+ } else {
+ client = c
+ logging.Logf("lsp ", "llm enabled provider=%s model=%s", c.Name(), c.DefaultModel())
+ }
+ }
+
+ if factory == nil {
+ factory = func(r io.Reader, w io.Writer, logger *log.Logger, opts lsp.ServerOptions) ServerRunner {
+ return lsp.NewServer(r, w, logger, opts)
+ }
+ }
+
+ server := factory(stdin, stdout, logger, lsp.ServerOptions{
+ LogContext: strings.TrimSpace(logPath) != "",
+ MaxTokens: cfg.MaxTokens,
+ ContextMode: cfg.ContextMode,
+ WindowLines: cfg.ContextWindowLines,
+ MaxContextTokens: cfg.MaxContextTokens,
+ NoDiskIO: cfg.NoDiskIO,
+ Client: client,
+ TriggerCharacters: cfg.TriggerCharacters,
+ })
+ if err := server.Run(); err != nil {
+ logger.Fatalf("server error: %v", err)
+ }
+ return nil
+}