summaryrefslogtreecommitdiff
path: root/internal/hexailsp
diff options
context:
space:
mode:
Diffstat (limited to 'internal/hexailsp')
-rw-r--r--internal/hexailsp/dependencies.go77
-rw-r--r--internal/hexailsp/run.go49
-rw-r--r--internal/hexailsp/run_more_test.go36
3 files changed, 133 insertions, 29 deletions
diff --git a/internal/hexailsp/dependencies.go b/internal/hexailsp/dependencies.go
new file mode 100644
index 0000000..7e025d4
--- /dev/null
+++ b/internal/hexailsp/dependencies.go
@@ -0,0 +1,77 @@
+package hexailsp
+
+import (
+ "log"
+
+ "codeberg.org/snonux/hexai/internal/appconfig"
+ "codeberg.org/snonux/hexai/internal/ignore"
+ "codeberg.org/snonux/hexai/internal/llm"
+ "codeberg.org/snonux/hexai/internal/llmutils"
+ "codeberg.org/snonux/hexai/internal/logging"
+ "codeberg.org/snonux/hexai/internal/lsp"
+ "codeberg.org/snonux/hexai/internal/runtimeconfig"
+)
+
+type configLoader func(*log.Logger, appconfig.LoadOptions) appconfig.App
+
+type clientBuilder func(appconfig.App, llm.Client) llm.Client
+
+type configStoreFactory func(appconfig.App) *runtimeconfig.Store
+
+type ignoreCheckerFactory func(appconfig.App) *ignore.Checker
+
+type runDependencies struct {
+ loadConfig configLoader
+ buildClient clientBuilder
+ newConfigStore configStoreFactory
+ newIgnoreChecker ignoreCheckerFactory
+ statusSink lsp.StatusSink
+}
+
+func defaultRunDependencies() runDependencies {
+ return runDependencies{
+ loadConfig: appconfig.LoadWithOptions,
+ buildClient: defaultClientBuilder,
+ newConfigStore: runtimeconfig.New,
+ newIgnoreChecker: defaultIgnoreCheckerFactory,
+ statusSink: tmuxStatusSink{},
+ }
+}
+
+func normalizeRunDependencies(deps runDependencies) runDependencies {
+ if deps.loadConfig == nil {
+ deps.loadConfig = appconfig.LoadWithOptions
+ }
+ if deps.buildClient == nil {
+ deps.buildClient = defaultClientBuilder
+ }
+ if deps.newConfigStore == nil {
+ deps.newConfigStore = runtimeconfig.New
+ }
+ if deps.newIgnoreChecker == nil {
+ deps.newIgnoreChecker = defaultIgnoreCheckerFactory
+ }
+ if deps.statusSink == nil {
+ deps.statusSink = tmuxStatusSink{}
+ }
+ return deps
+}
+
+func defaultClientBuilder(cfg appconfig.App, client llm.Client) llm.Client {
+ if client != nil {
+ return client
+ }
+ c, err := llmutils.NewClientFromApp(cfg)
+ if err != nil {
+ logging.Logf("lsp ", "llm disabled: %v", err)
+ return nil
+ }
+ logging.Logf("lsp ", "llm enabled provider=%s model=%s", c.Name(), c.DefaultModel())
+ return c
+}
+
+func defaultIgnoreCheckerFactory(cfg appconfig.App) *ignore.Checker {
+ gitRoot := appconfig.FindGitRoot()
+ useGI := cfg.IgnoreGitignore == nil || *cfg.IgnoreGitignore
+ return ignore.New(gitRoot, useGI, cfg.IgnoreExtraPatterns)
+}
diff --git a/internal/hexailsp/run.go b/internal/hexailsp/run.go
index ec5dbba..57e7476 100644
--- a/internal/hexailsp/run.go
+++ b/internal/hexailsp/run.go
@@ -13,10 +13,8 @@ import (
"codeberg.org/snonux/hexai/internal/appconfig"
"codeberg.org/snonux/hexai/internal/ignore"
"codeberg.org/snonux/hexai/internal/llm"
- "codeberg.org/snonux/hexai/internal/llmutils"
"codeberg.org/snonux/hexai/internal/logging"
"codeberg.org/snonux/hexai/internal/lsp"
- "codeberg.org/snonux/hexai/internal/runtimeconfig"
"codeberg.org/snonux/hexai/internal/stats"
tmx "codeberg.org/snonux/hexai/internal/tmux"
)
@@ -56,6 +54,11 @@ func Run(logPath string, stdin io.Reader, stdout io.Writer, stderr io.Writer) er
// RunWithConfig is like Run but accepts an explicit config file path.
func RunWithConfig(logPath string, configPath string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error {
+ return runWithConfigDependencies(logPath, configPath, stdin, stdout, stderr, defaultRunDependencies())
+}
+
+func runWithConfigDependencies(logPath string, configPath string, stdin io.Reader, stdout io.Writer, stderr io.Writer, deps runDependencies) error {
+ deps = normalizeRunDependencies(deps)
logger := log.New(stderr, "hexai-lsp-server ", log.LstdFlags|log.Lmsgprefix)
if strings.TrimSpace(logPath) != "" {
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
@@ -71,35 +74,36 @@ func RunWithConfig(logPath string, configPath string, stdin io.Reader, stdout io
}
logging.Bind(logger)
loadOpts := appconfig.LoadOptions{ConfigPath: configPath}
- cfg := appconfig.LoadWithOptions(logger, loadOpts)
+ cfg := deps.loadConfig(logger, loadOpts)
if err := cfg.Validate(); err != nil {
return fmt.Errorf("invalid config: %w", err)
}
if cfg.StatsWindowMinutes > 0 {
stats.SetWindow(time.Duration(cfg.StatsWindowMinutes) * time.Minute)
}
- return RunWithFactory(logPath, configPath, stdin, stdout, logger, cfg, nil, nil)
+ return runWithDependencies(logPath, configPath, stdin, stdout, logger, cfg, nil, nil, deps)
}
// 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, configPath string, stdin io.Reader, stdout io.Writer, logger *log.Logger, cfg appconfig.App, client llm.Client, factory ServerFactory) error {
+ return runWithDependencies(logPath, configPath, stdin, stdout, logger, cfg, client, factory, defaultRunDependencies())
+}
+
+func runWithDependencies(logPath string, configPath string, stdin io.Reader, stdout io.Writer, logger *log.Logger, cfg appconfig.App, client llm.Client, factory ServerFactory, deps runDependencies) error {
+ deps = normalizeRunDependencies(deps)
normalizeLoggingConfig(&cfg)
if err := cfg.Validate(); err != nil {
return fmt.Errorf("invalid config: %w", err)
}
- client = buildClientIfNil(cfg, client)
+ client = deps.buildClient(cfg, client)
factory = ensureFactory(factory)
- // Create gitignore-aware file checker for LSP filtering
- gitRoot := appconfig.FindGitRoot()
- useGI := cfg.IgnoreGitignore == nil || *cfg.IgnoreGitignore
- ignoreChecker := ignore.New(gitRoot, useGI, cfg.IgnoreExtraPatterns)
-
- store := runtimeconfig.New(cfg)
+ ignoreChecker := deps.newIgnoreChecker(cfg)
+ store := deps.newConfigStore(cfg)
logContext := strings.TrimSpace(logPath) != ""
loadOpts := appconfig.LoadOptions{ConfigPath: strings.TrimSpace(configPath)}
- opts := makeServerOptions(cfg, logContext, client, loadOpts, ignoreChecker)
+ opts := makeServerOptions(cfg, logContext, client, loadOpts, ignoreChecker, deps.statusSink)
opts.ConfigLoadOptions = loadOpts
opts.ConfigStore = store
server := factory(stdin, stdout, logger, opts)
@@ -110,13 +114,13 @@ func RunWithFactory(logPath string, configPath string, stdin io.Reader, stdout i
if updated.StatsWindowMinutes > 0 {
stats.SetWindow(time.Duration(updated.StatsWindowMinutes) * time.Minute)
}
- if newClient := buildClientIfNil(updated, nil); newClient != nil {
+ if newClient := deps.buildClient(updated, nil); newClient != nil {
client = newClient
}
// Update ignore checker patterns on config hot-reload
useGI := updated.IgnoreGitignore == nil || *updated.IgnoreGitignore
ignoreChecker.Update(useGI, updated.IgnoreExtraPatterns)
- opts := makeServerOptions(updated, logContext, client, loadOpts, ignoreChecker)
+ opts := makeServerOptions(updated, logContext, client, loadOpts, ignoreChecker, deps.statusSink)
opts.ConfigStore = store
configurable.ApplyOptions(opts)
})
@@ -136,19 +140,6 @@ func normalizeLoggingConfig(cfg *appconfig.App) {
}
}
-func buildClientIfNil(cfg appconfig.App, client llm.Client) llm.Client {
- if client != nil {
- return client
- }
- c, err := llmutils.NewClientFromApp(cfg)
- if err != nil {
- logging.Logf("lsp ", "llm disabled: %v", err)
- return nil
- }
- 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
@@ -158,7 +149,7 @@ func ensureFactory(factory ServerFactory) ServerFactory {
}
}
-func makeServerOptions(cfg appconfig.App, logContext bool, client llm.Client, loadOpts appconfig.LoadOptions, ignoreChecker *ignore.Checker) lsp.ServerOptions {
+func makeServerOptions(cfg appconfig.App, logContext bool, client llm.Client, loadOpts appconfig.LoadOptions, ignoreChecker *ignore.Checker, statusSink lsp.StatusSink) lsp.ServerOptions {
return lsp.ServerOptions{
ConfigLoadOptions: loadOpts,
LogContext: logContext,
@@ -166,6 +157,6 @@ func makeServerOptions(cfg appconfig.App, logContext bool, client llm.Client, lo
Config: &cfg,
Client: client,
IgnoreChecker: ignoreChecker,
- StatusSink: tmuxStatusSink{},
+ StatusSink: statusSink,
}
}
diff --git a/internal/hexailsp/run_more_test.go b/internal/hexailsp/run_more_test.go
index 7017811..d0f17b5 100644
--- a/internal/hexailsp/run_more_test.go
+++ b/internal/hexailsp/run_more_test.go
@@ -30,6 +30,11 @@ func (stubClient) Chat(context.Context, []llm.Message, ...llm.RequestOption) (st
func (stubClient) Name() string { return "stub" }
func (stubClient) DefaultModel() string { return "stub-model" }
+type recordingStatusSink struct{}
+
+func (recordingStatusSink) SetLLMStart(string, string) error { return nil }
+func (recordingStatusSink) SetGlobal(lsp.GlobalStatus) error { return nil }
+
func TestRunWithFactory_BuildsOptionsAndClient(t *testing.T) {
var captured lsp.ServerOptions
factory := func(r io.Reader, w io.Writer, logger *log.Logger, opts lsp.ServerOptions) ServerRunner {
@@ -101,3 +106,34 @@ func TestRunWithFactory_SubscriptionAppliesUpdates(t *testing.T) {
t.Fatalf("expected normalized context mode, got %+v", latest)
}
}
+
+func TestRunWithDependencies_UsesInjectedClientBuilderAndStatusSink(t *testing.T) {
+ var captured lsp.ServerOptions
+ sink := &recordingStatusSink{}
+ buildCalls := 0
+ factory := func(r io.Reader, w io.Writer, logger *log.Logger, opts lsp.ServerOptions) ServerRunner {
+ captured = opts
+ return &recRunner{}
+ }
+ cfg := appconfig.Load(nil)
+ if err := runWithDependencies("", "", bytes.NewBuffer(nil), bytes.NewBuffer(nil), log.New(io.Discard, "", 0), cfg, nil, factory, runDependencies{
+ buildClient: func(appconfig.App, llm.Client) llm.Client {
+ buildCalls++
+ return stubClient{}
+ },
+ newConfigStore: runtimeconfig.New,
+ newIgnoreChecker: defaultIgnoreCheckerFactory,
+ statusSink: sink,
+ }); err != nil {
+ t.Fatalf("runWithDependencies error: %v", err)
+ }
+ if buildCalls != 1 {
+ t.Fatalf("expected one client build, got %d", buildCalls)
+ }
+ if captured.Client == nil {
+ t.Fatal("expected injected client to be passed through")
+ }
+ if captured.StatusSink != sink {
+ t.Fatal("expected injected status sink to be passed through")
+ }
+}