diff options
Diffstat (limited to 'internal/hexailsp')
| -rw-r--r-- | internal/hexailsp/dependencies.go | 77 | ||||
| -rw-r--r-- | internal/hexailsp/run.go | 49 | ||||
| -rw-r--r-- | internal/hexailsp/run_more_test.go | 36 |
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") + } +} |
