diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-07 11:49:38 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-07 11:49:38 +0300 |
| commit | 5ed470a093ffb7d28c88f9687429f238959935da (patch) | |
| tree | f4fd4e0bae2e5312de675d9de708c6543f2299e0 /docs/coverage.html | |
| parent | 9a901054e8828054f7b26514b72b6e938f97e4a7 (diff) | |
test: add seams for RunTUI and client; expand hexaiaction tests; cover lsp initialized and testutil fixtures
Diffstat (limited to 'docs/coverage.html')
| -rw-r--r-- | docs/coverage.html | 410 |
1 files changed, 207 insertions, 203 deletions
diff --git a/docs/coverage.html b/docs/coverage.html index 8f6eb62..90eae60 100644 --- a/docs/coverage.html +++ b/docs/coverage.html @@ -63,13 +63,13 @@ <option value="file3">codeberg.org/snonux/hexai/internal/appconfig/config.go (91.6%)</option> - <option value="file4">codeberg.org/snonux/hexai/internal/hexaiaction/cmdentry.go (76.5%)</option> + <option value="file4">codeberg.org/snonux/hexai/internal/hexaiaction/cmdentry.go (81.5%)</option> <option value="file5">codeberg.org/snonux/hexai/internal/hexaiaction/parse.go (92.6%)</option> <option value="file6">codeberg.org/snonux/hexai/internal/hexaiaction/prompts.go (91.9%)</option> - <option value="file7">codeberg.org/snonux/hexai/internal/hexaiaction/run.go (48.7%)</option> + <option value="file7">codeberg.org/snonux/hexai/internal/hexaiaction/run.go (71.8%)</option> <option value="file8">codeberg.org/snonux/hexai/internal/hexaiaction/tui.go (65.5%)</option> @@ -109,7 +109,7 @@ <option value="file26">codeberg.org/snonux/hexai/internal/lsp/handlers_execute.go (75.0%)</option> - <option value="file27">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (55.6%)</option> + <option value="file27">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (66.7%)</option> <option value="file28">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (89.0%)</option> @@ -117,7 +117,7 @@ <option value="file30">codeberg.org/snonux/hexai/internal/lsp/transport.go (71.4%)</option> - <option value="file31">codeberg.org/snonux/hexai/internal/testutil/fixtures.go (60.0%)</option> + <option value="file31">codeberg.org/snonux/hexai/internal/testutil/fixtures.go (100.0%)</option> <option value="file32">codeberg.org/snonux/hexai/internal/textutil/textutil.go (89.0%)</option> @@ -321,7 +321,7 @@ type App struct { } // Constructor: defaults for App (kept first among functions) -func newDefaultConfig() App <span class="cov5" title="14">{ +func newDefaultConfig() App <span class="cov5" title="16">{ // Coding-friendly default temperature across providers // Users can override per provider in config.toml (including 0.0). t := 0.2 @@ -372,17 +372,17 @@ func newDefaultConfig() App <span class="cov5" title="14">{ // Load reads configuration from a file and merges with defaults. // It respects the XDG Base Directory Specification. -func Load(logger *log.Logger) App <span class="cov5" title="13">{ +func Load(logger *log.Logger) App <span class="cov5" title="15">{ cfg := newDefaultConfig() if logger == nil </span><span class="cov3" title="4">{ return cfg // Return defaults if no logger is provided (e.g. in tests) }</span> - <span class="cov4" title="9">configPath, err := getConfigPath() + <span class="cov4" title="11">configPath, err := getConfigPath() if err != nil </span><span class="cov0" title="0">{ logger.Printf("%v", err) // Even if config path cannot be resolved, still allow env overrides below. - }</span> else<span class="cov4" title="9"> { + }</span> else<span class="cov4" title="11"> { if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil </span><span class="cov3" title="4">{ cfg.mergeWith(fileCfg) }</span> @@ -391,10 +391,10 @@ func Load(logger *log.Logger) App <span class="cov5" title="13">{ } // Environment overrides (take precedence over file) - <span class="cov4" title="9">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{ cfg.mergeWith(envCfg) }</span> - <span class="cov4" title="9">return cfg</span> + <span class="cov4" title="11">return cfg</span> } // Private helpers @@ -665,16 +665,16 @@ func (fc *fileConfig) toApp() App <span class="cov3" title="4">{ <span class="cov3" title="4">return out</span> } -func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov4" title="10">{ +func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov5" title="12">{ b, err := os.ReadFile(path) - if err != nil </span><span class="cov3" title="4">{ + if err != nil </span><span class="cov3" title="6">{ if !os.IsNotExist(err) && logger != nil </span><span class="cov0" title="0">{ logger.Printf("cannot open TOML config file %s: %v", path, err) }</span> - <span class="cov3" title="4">return nil, err</span> + <span class="cov3" title="6">return nil, err</span> } - <span class="cov4" title="6">var tables fileConfig + <span class="cov3" title="6">var tables fileConfig errTables := toml.NewDecoder(strings.NewReader(string(b))).Decode(&tables) // Raw map for validation/presence checks var raw map[string]any @@ -747,50 +747,50 @@ func (a *App) mergeWith(other *App) <span class="cov3" title="5">{ }</span> // mergeBasics merges general (non-provider) fields. -func (a *App) mergeBasics(other *App) <span class="cov6" title="20">{ +func (a *App) mergeBasics(other *App) <span class="cov5" title="20">{ if other.MaxTokens > 0 </span><span class="cov4" title="7">{ a.MaxTokens = other.MaxTokens }</span> - <span class="cov6" title="20">if s := strings.TrimSpace(other.ContextMode); s != "" </span><span class="cov4" title="7">{ + <span class="cov5" title="20">if s := strings.TrimSpace(other.ContextMode); s != "" </span><span class="cov4" title="7">{ a.ContextMode = s }</span> - <span class="cov6" title="20">if other.ContextWindowLines > 0 </span><span class="cov4" title="7">{ + <span class="cov5" title="20">if other.ContextWindowLines > 0 </span><span class="cov4" title="7">{ a.ContextWindowLines = other.ContextWindowLines }</span> - <span class="cov6" title="20">if other.MaxContextTokens > 0 </span><span class="cov4" title="7">{ + <span class="cov5" title="20">if other.MaxContextTokens > 0 </span><span class="cov4" title="7">{ a.MaxContextTokens = other.MaxContextTokens }</span> - <span class="cov6" title="20">if other.LogPreviewLimit >= 0 </span><span class="cov6" title="20">{ + <span class="cov5" title="20">if other.LogPreviewLimit >= 0 </span><span class="cov5" title="20">{ a.LogPreviewLimit = other.LogPreviewLimit }</span> - <span class="cov6" title="20">if other.CodingTemperature != nil </span><span class="cov4" title="7">{ // allow explicit 0.0 + <span class="cov5" title="20">if other.CodingTemperature != nil </span><span class="cov4" title="7">{ // allow explicit 0.0 a.CodingTemperature = other.CodingTemperature }</span> - <span class="cov6" title="20">if other.ManualInvokeMinPrefix >= 0 </span><span class="cov6" title="20">{ + <span class="cov5" title="20">if other.ManualInvokeMinPrefix >= 0 </span><span class="cov5" title="20">{ a.ManualInvokeMinPrefix = other.ManualInvokeMinPrefix }</span> - <span class="cov6" title="20">if other.CompletionDebounceMs > 0 </span><span class="cov4" title="7">{ + <span class="cov5" title="20">if other.CompletionDebounceMs > 0 </span><span class="cov4" title="7">{ a.CompletionDebounceMs = other.CompletionDebounceMs }</span> - <span class="cov6" title="20">if other.CompletionThrottleMs > 0 </span><span class="cov4" title="7">{ + <span class="cov5" title="20">if other.CompletionThrottleMs > 0 </span><span class="cov4" title="7">{ a.CompletionThrottleMs = other.CompletionThrottleMs }</span> - <span class="cov6" title="20">if len(other.TriggerCharacters) > 0 </span><span class="cov4" title="7">{ + <span class="cov5" title="20">if len(other.TriggerCharacters) > 0 </span><span class="cov4" title="7">{ a.TriggerCharacters = slices.Clone(other.TriggerCharacters) }</span> - <span class="cov6" title="20">if s := strings.TrimSpace(other.InlineOpen); s != "" </span><span class="cov2" title="2">{ + <span class="cov5" title="20">if s := strings.TrimSpace(other.InlineOpen); s != "" </span><span class="cov2" title="2">{ a.InlineOpen = s }</span> - <span class="cov6" title="20">if s := strings.TrimSpace(other.InlineClose); s != "" </span><span class="cov2" title="2">{ + <span class="cov5" title="20">if s := strings.TrimSpace(other.InlineClose); s != "" </span><span class="cov2" title="2">{ a.InlineClose = s }</span> - <span class="cov6" title="20">if s := strings.TrimSpace(other.ChatSuffix); s != "" </span><span class="cov2" title="2">{ + <span class="cov5" title="20">if s := strings.TrimSpace(other.ChatSuffix); s != "" </span><span class="cov2" title="2">{ a.ChatSuffix = s }</span> - <span class="cov6" title="20">if len(other.ChatPrefixes) > 0 </span><span class="cov2" title="2">{ + <span class="cov5" title="20">if len(other.ChatPrefixes) > 0 </span><span class="cov2" title="2">{ a.ChatPrefixes = slices.Clone(other.ChatPrefixes) }</span> - <span class="cov6" title="20">if s := strings.TrimSpace(other.Provider); s != "" </span><span class="cov4" title="7">{ + <span class="cov5" title="20">if s := strings.TrimSpace(other.Provider); s != "" </span><span class="cov4" title="7">{ a.Provider = s }</span> } @@ -889,33 +889,33 @@ func (a *App) mergeProviderFields(other *App) <span class="cov5" title="14">{ }</span> } -func getConfigPath() (string, error) <span class="cov4" title="10">{ +func getConfigPath() (string, error) <span class="cov5" title="12">{ var configPath string if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" </span><span class="cov4" title="7">{ configPath = filepath.Join(xdgConfigHome, "hexai", "config.toml") - }</span> else<span class="cov2" title="3"> { + }</span> else<span class="cov3" title="5"> { home, err := os.UserHomeDir() if err != nil </span><span class="cov0" title="0">{ return "", fmt.Errorf("cannot find user home directory: %v", err) }</span> - <span class="cov2" title="3">configPath = filepath.Join(home, ".config", "hexai", "config.toml")</span> + <span class="cov3" title="5">configPath = filepath.Join(home, ".config", "hexai", "config.toml")</span> } - <span class="cov4" title="10">return configPath, nil</span> + <span class="cov5" title="12">return configPath, nil</span> } // --- Environment overrides --- // loadFromEnv constructs an App containing only fields set via HEXAI_* env vars. // These values should take precedence over file config when merged. -func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="9">{ +func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="11">{ var out App var any bool // helpers - getenv := func(k string) string </span><span class="cov10" title="216">{ return strings.TrimSpace(os.Getenv(k)) }</span> - <span class="cov4" title="9">parseInt := func(k string) (int, bool) </span><span class="cov7" title="63">{ + getenv := func(k string) string </span><span class="cov10" title="264">{ return strings.TrimSpace(os.Getenv(k)) }</span> + <span class="cov4" title="11">parseInt := func(k string) (int, bool) </span><span class="cov8" title="77">{ v := getenv(k) - if v == "" </span><span class="cov7" title="56">{ + if v == "" </span><span class="cov7" title="70">{ return 0, false }</span> <span class="cov4" title="7">n, err := strconv.Atoi(v) @@ -927,9 +927,9 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="9">{ } <span class="cov4" title="7">return n, true</span> } - <span class="cov4" title="9">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov7" title="36">{ + <span class="cov4" title="11">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov7" title="44">{ v := getenv(k) - if v == "" </span><span class="cov6" title="32">{ + if v == "" </span><span class="cov6" title="40">{ return nil, false }</span> <span class="cov3" title="4">f, err := strconv.ParseFloat(v, 64) @@ -942,43 +942,43 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="9">{ <span class="cov3" title="4">return &f, true</span> } - <span class="cov4" title="9">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov1" title="1">{ out.MaxTokens = n any = true }</span> - <span class="cov4" title="9">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{ out.ContextMode = s any = true }</span> - <span class="cov4" title="9">if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok </span><span class="cov1" title="1">{ out.ContextWindowLines = n any = true }</span> - <span class="cov4" title="9">if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok </span><span class="cov1" title="1">{ out.MaxContextTokens = n any = true }</span> - <span class="cov4" title="9">if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok </span><span class="cov1" title="1">{ out.LogPreviewLimit = n any = true }</span> - <span class="cov4" title="9">if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok </span><span class="cov1" title="1">{ out.ManualInvokeMinPrefix = n any = true }</span> - <span class="cov4" title="9">if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok </span><span class="cov1" title="1">{ out.CompletionDebounceMs = n any = true }</span> - <span class="cov4" title="9">if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok </span><span class="cov1" title="1">{ out.CompletionThrottleMs = n any = true }</span> - <span class="cov4" title="9">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.CodingTemperature = f any = true }</span> - <span class="cov4" title="9">if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" </span><span class="cov1" title="1">{ parts := strings.Split(s, ",") out.TriggerCharacters = nil for _, p := range parts </span><span class="cov2" title="3">{ @@ -988,19 +988,19 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="9">{ } <span class="cov1" title="1">any = true</span> } - <span class="cov4" title="9">if s := getenv("HEXAI_INLINE_OPEN"); s != "" </span><span class="cov0" title="0">{ + <span class="cov4" title="11">if s := getenv("HEXAI_INLINE_OPEN"); s != "" </span><span class="cov0" title="0">{ out.InlineOpen = s any = true }</span> - <span class="cov4" title="9">if s := getenv("HEXAI_INLINE_CLOSE"); s != "" </span><span class="cov0" title="0">{ + <span class="cov4" title="11">if s := getenv("HEXAI_INLINE_CLOSE"); s != "" </span><span class="cov0" title="0">{ out.InlineClose = s any = true }</span> - <span class="cov4" title="9">if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" </span><span class="cov0" title="0">{ + <span class="cov4" title="11">if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" </span><span class="cov0" title="0">{ out.ChatSuffix = s any = true }</span> - <span class="cov4" title="9">if s := getenv("HEXAI_CHAT_PREFIXES"); s != "" </span><span class="cov0" title="0">{ + <span class="cov4" title="11">if s := getenv("HEXAI_CHAT_PREFIXES"); s != "" </span><span class="cov0" title="0">{ parts := strings.Split(s, ",") out.ChatPrefixes = nil for _, p := range parts </span><span class="cov0" title="0">{ @@ -1010,52 +1010,52 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="9">{ } <span class="cov0" title="0">any = true</span> } - <span class="cov4" title="9">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov1" title="1">{ out.Provider = s any = true }</span> // Provider-specific - <span class="cov4" title="9">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OpenAIBaseURL = s any = true }</span> - <span class="cov4" title="9">if s := getenv("HEXAI_OPENAI_MODEL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if s := getenv("HEXAI_OPENAI_MODEL"); s != "" </span><span class="cov1" title="1">{ out.OpenAIModel = s any = true }</span> - <span class="cov4" title="9">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.OpenAITemperature = f any = true }</span> - <span class="cov4" title="9">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OllamaBaseURL = s any = true }</span> - <span class="cov4" title="9">if s := getenv("HEXAI_OLLAMA_MODEL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if s := getenv("HEXAI_OLLAMA_MODEL"); s != "" </span><span class="cov1" title="1">{ out.OllamaModel = s any = true }</span> - <span class="cov4" title="9">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.OllamaTemperature = f any = true }</span> - <span class="cov4" title="9">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.CopilotBaseURL = s any = true }</span> - <span class="cov4" title="9">if s := getenv("HEXAI_COPILOT_MODEL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if s := getenv("HEXAI_COPILOT_MODEL"); s != "" </span><span class="cov1" title="1">{ out.CopilotModel = s any = true }</span> - <span class="cov4" title="9">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="11">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.CopilotTemperature = f any = true }</span> - <span class="cov4" title="9">if !any </span><span class="cov4" title="8">{ + <span class="cov4" title="11">if !any </span><span class="cov4" title="10">{ return nil }</span> <span class="cov1" title="1">return &out</span> @@ -1090,22 +1090,22 @@ type Options struct { // RunCommand is the CLI orchestrator used by cmd/hexai-action. It decides whether // to run inline, in a tmux split pane, or in child mode; then delegates to Run. -func RunCommand(ctx context.Context, opts Options, stdin io.Reader, stdout, stderr io.Writer) error <span class="cov0" title="0">{ - if opts.UIChild </span><span class="cov0" title="0">{ +func RunCommand(ctx context.Context, opts Options, stdin io.Reader, stdout, stderr io.Writer) error <span class="cov6" title="3">{ + if opts.UIChild </span><span class="cov1" title="1">{ return runChild(ctx, opts.Infile, opts.Outfile, stdout, stderr) }</span> - <span class="cov0" title="0">if shouldRunInTmux(opts.ForceTmux, opts.NoTmux) </span><span class="cov0" title="0">{ + <span class="cov4" title="2">if shouldRunInTmux(opts.ForceTmux, opts.NoTmux) </span><span class="cov1" title="1">{ return runInTmuxParent(stdin, stdout, opts.TmuxTarget, opts.TmuxSplit, opts.TmuxPercent) }</span> // Inline path: only if we have a TTY for UI; otherwise echo input - <span class="cov0" title="0">if isTTYFn(os.Stdout.Fd()) && isTTYFn(os.Stdin.Fd()) </span><span class="cov0" title="0">{ + <span class="cov1" title="1">if isTTYFn(os.Stdout.Fd()) && isTTYFn(os.Stdin.Fd()) </span><span class="cov0" title="0">{ in, out, closeIn, closeOut, err := openIO(opts.Infile, opts.Outfile) if err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov0" title="0">defer closeIn(); defer closeOut() return Run(ctx, in, out, stderr)</span> } // Fallback: echo - <span class="cov0" title="0">return echoThrough(opts.Infile, opts.Outfile, stdin, stdout)</span> + <span class="cov1" title="1">return echoThrough(opts.Infile, opts.Outfile, stdin, stdout)</span> } // seams for unit tests @@ -1115,36 +1115,36 @@ var splitRunFn = tmux.SplitRun var osExecutableFn = os.Executable var runFn = Run -func shouldRunInTmux(forceTmux, noTmux bool) bool <span class="cov10" title="5">{ - if noTmux </span><span class="cov1" title="1">{ return false }</span> - <span class="cov8" title="4">if forceTmux </span><span class="cov1" title="1">{ return true }</span> - <span class="cov7" title="3">if !(isTTYFn(os.Stdin.Fd()) && isTTYFn(os.Stdout.Fd())) && tmuxAvailableFn() </span><span class="cov1" title="1">{ return true }</span> +func shouldRunInTmux(forceTmux, noTmux bool) bool <span class="cov10" title="7">{ + if noTmux </span><span class="cov4" title="2">{ return false }</span> + <span class="cov8" title="5">if forceTmux </span><span class="cov4" title="2">{ return true }</span> + <span class="cov6" title="3">if !(isTTYFn(os.Stdin.Fd()) && isTTYFn(os.Stdout.Fd())) && tmuxAvailableFn() </span><span class="cov1" title="1">{ return true }</span> <span class="cov4" title="2">return false</span> } // openIO returns readers/writers for infile/outfile flags with deferred closers. -func openIO(infile, outfile string) (io.Reader, io.Writer, func(), func(), error) <span class="cov4" title="2">{ +func openIO(infile, outfile string) (io.Reader, io.Writer, func(), func(), error) <span class="cov6" title="3">{ in := io.Reader(os.Stdin) out := io.Writer(os.Stdout) closeIn := func() </span>{<span class="cov0" title="0">}</span> - <span class="cov4" title="2">closeOut := func() </span>{<span class="cov0" title="0">}</span> - <span class="cov4" title="2">if path := infile; path != "" </span><span class="cov4" title="2">{ + <span class="cov6" title="3">closeOut := func() </span>{<span class="cov0" title="0">}</span> + <span class="cov6" title="3">if path := infile; path != "" </span><span class="cov6" title="3">{ f, err := os.Open(path) if err != nil </span><span class="cov0" title="0">{ return nil, nil, func()</span>{<span class="cov0" title="0">}</span>, func(){<span class="cov0" title="0">}</span>, fmt.Errorf("hexai-action: cannot open infile: %w", err) } - <span class="cov4" title="2">in = f - closeIn = func() </span><span class="cov4" title="2">{ _ = f.Close() }</span> + <span class="cov6" title="3">in = f + closeIn = func() </span><span class="cov6" title="3">{ _ = f.Close() }</span> } - <span class="cov4" title="2">if path := outfile; path != "" </span><span class="cov4" title="2">{ + <span class="cov6" title="3">if path := outfile; path != "" </span><span class="cov6" title="3">{ f, err := os.Create(path) if err != nil </span><span class="cov0" title="0">{ return nil, nil, func()</span>{<span class="cov0" title="0">}</span>, func(){<span class="cov0" title="0">}</span>, fmt.Errorf("hexai-action: cannot open outfile: %w", err) } - <span class="cov4" title="2">out = f - closeOut = func() </span><span class="cov4" title="2">{ _ = f.Close() }</span> + <span class="cov6" title="3">out = f + closeOut = func() </span><span class="cov6" title="3">{ _ = f.Close() }</span> } - <span class="cov4" title="2">return in, out, closeIn, closeOut, nil</span> + <span class="cov6" title="3">return in, out, closeIn, closeOut, nil</span> } // runChild runs the interactive flow and writes the final output atomically when outfile is set. -func runChild(ctx context.Context, infile, outfile string, stdout, stderr io.Writer) error <span class="cov4" title="2">{ +func runChild(ctx context.Context, infile, outfile string, stdout, stderr io.Writer) error <span class="cov6" title="3">{ if outfile == "" </span><span class="cov1" title="1">{ // No atomic handoff needed; just run normally to provided stdout var in io.Reader = os.Stdin @@ -1156,63 +1156,63 @@ func runChild(ctx context.Context, infile, outfile string, stdout, stderr io.Wri } <span class="cov1" title="1">return runFn(ctx, in, stdout, stderr)</span> } - <span class="cov1" title="1">tmp := outfile + ".tmp" + <span class="cov4" title="2">tmp := outfile + ".tmp" in, out, closeIn, closeOut, err := openIO(infile, tmp) if err != nil </span><span class="cov0" title="0">{ return err }</span> - <span class="cov1" title="1">defer closeIn() + <span class="cov4" title="2">defer closeIn() if err := runFn(ctx, in, out, stderr); err != nil </span><span class="cov0" title="0">{ closeOut() if copyErr := echoThrough(infile, tmp, os.Stdin, stdout); copyErr != nil </span><span class="cov0" title="0">{ return fmt.Errorf("hexai-action child: %v; echo failed: %v", err, copyErr) }</span> - } else<span class="cov1" title="1"> { + } else<span class="cov4" title="2"> { closeOut() }</span> - <span class="cov1" title="1">return os.Rename(tmp, outfile)</span> + <span class="cov4" title="2">return os.Rename(tmp, outfile)</span> } -func runInTmuxParent(stdin io.Reader, stdout io.Writer, target, split string, percent int) error <span class="cov7" title="3">{ +func runInTmuxParent(stdin io.Reader, stdout io.Writer, target, split string, percent int) error <span class="cov7" title="4">{ dir, err := os.MkdirTemp("", "hexai-action-") if err != nil </span><span class="cov0" title="0">{ return err }</span> - <span class="cov7" title="3">defer func() </span><span class="cov7" title="3">{ _ = os.RemoveAll(dir) }</span>() - <span class="cov7" title="3">inPath := filepath.Join(dir, "input.txt") + <span class="cov7" title="4">defer func() </span><span class="cov7" title="4">{ _ = os.RemoveAll(dir) }</span>() + <span class="cov7" title="4">inPath := filepath.Join(dir, "input.txt") outPath := filepath.Join(dir, "reply.txt") if err := persistStdin(inPath, stdin); err != nil </span><span class="cov0" title="0">{ return err }</span> - <span class="cov7" title="3">exe, err := osExecutableFn() + <span class="cov7" title="4">exe, err := osExecutableFn() if err != nil </span><span class="cov1" title="1">{ return err }</span> - <span class="cov4" title="2">argv := []string{exe, "-ui-child", "-infile", inPath, "-outfile", outPath} + <span class="cov6" title="3">argv := []string{exe, "-ui-child", "-infile", inPath, "-outfile", outPath} opts := tmux.SplitOpts{Target: target, Vertical: split != "h", Percent: percent} if err := splitRunFn(opts, argv); err != nil </span><span class="cov1" title="1">{ return err }</span> - <span class="cov1" title="1">if err := waitForFile(outPath, 60*time.Second); err != nil </span><span class="cov0" title="0">{ return err }</span> - <span class="cov1" title="1">return catFileTo(stdout, outPath)</span> + <span class="cov4" title="2">if err := waitForFile(outPath, 60*time.Second); err != nil </span><span class="cov0" title="0">{ return err }</span> + <span class="cov4" title="2">return catFileTo(stdout, outPath)</span> } -func persistStdin(path string, stdin io.Reader) error <span class="cov8" title="4">{ +func persistStdin(path string, stdin io.Reader) error <span class="cov8" title="5">{ f, err := os.Create(path) if err != nil </span><span class="cov0" title="0">{ return err }</span> - <span class="cov8" title="4">defer func() </span><span class="cov8" title="4">{ _ = f.Close() }</span>() - <span class="cov8" title="4">if _, err := io.Copy(f, stdin); err != nil </span><span class="cov0" title="0">{ return err }</span> - <span class="cov8" title="4">return f.Sync()</span> + <span class="cov8" title="5">defer func() </span><span class="cov8" title="5">{ _ = f.Close() }</span>() + <span class="cov8" title="5">if _, err := io.Copy(f, stdin); err != nil </span><span class="cov0" title="0">{ return err }</span> + <span class="cov8" title="5">return f.Sync()</span> } -func waitForFile(path string, timeout time.Duration) error <span class="cov4" title="2">{ +func waitForFile(path string, timeout time.Duration) error <span class="cov6" title="3">{ deadline := time.Now().Add(timeout) - for </span><span class="cov7" title="3">{ - if _, err := os.Stat(path); err == nil </span><span class="cov1" title="1">{ return nil }</span> + for </span><span class="cov7" title="4">{ + if _, err := os.Stat(path); err == nil </span><span class="cov4" title="2">{ return nil }</span> <span class="cov4" title="2">if time.Now().After(deadline) </span><span class="cov1" title="1">{ return fmt.Errorf("hexai-action: timeout waiting for reply file") }</span> <span class="cov1" title="1">time.Sleep(200 * time.Millisecond)</span> } } -func catFileTo(w io.Writer, path string) error <span class="cov1" title="1">{ +func catFileTo(w io.Writer, path string) error <span class="cov4" title="2">{ f, err := os.Open(path) if err != nil </span><span class="cov0" title="0">{ return err }</span> - <span class="cov1" title="1">defer func() </span><span class="cov1" title="1">{ _ = f.Close() }</span>() - <span class="cov1" title="1">_, err = io.Copy(w, f) + <span class="cov4" title="2">defer func() </span><span class="cov4" title="2">{ _ = f.Close() }</span>() + <span class="cov4" title="2">_, err = io.Copy(w, f) return err</span> } -func echoThrough(infile, outfile string, stdin io.Reader, stdout io.Writer) error <span class="cov4" title="2">{ +func echoThrough(infile, outfile string, stdin io.Reader, stdout io.Writer) error <span class="cov6" title="3">{ var in io.Reader = stdin var out io.Writer = stdout if infile != "" </span><span class="cov1" title="1">{ @@ -1221,13 +1221,13 @@ func echoThrough(infile, outfile string, stdin io.Reader, stdout io.Writer) erro <span class="cov1" title="1">defer func() </span><span class="cov1" title="1">{ _ = f.Close() }</span>() <span class="cov1" title="1">in = f</span> } - <span class="cov4" title="2">if outfile != "" </span><span class="cov1" title="1">{ + <span class="cov6" title="3">if outfile != "" </span><span class="cov1" title="1">{ f, err := os.Create(outfile) if err != nil </span><span class="cov0" title="0">{ return err }</span> <span class="cov1" title="1">defer func() </span><span class="cov1" title="1">{ _ = f.Close() }</span>() <span class="cov1" title="1">out = f</span> } - <span class="cov4" title="2">_, err := io.Copy(out, in) + <span class="cov6" title="3">_, err := io.Copy(out, in) return err</span> } </pre> @@ -1251,32 +1251,32 @@ import ( // <rest is selection/code> // // If the header is absent, the entire input is treated as selection. -func ParseInput(r io.Reader) (InputParts, error) <span class="cov4" title="2">{ +func ParseInput(r io.Reader) (InputParts, error) <span class="cov7" title="4">{ b, err := io.ReadAll(bufio.NewReader(r)) if err != nil </span><span class="cov0" title="0">{ return InputParts{}, err }</span> - <span class="cov4" title="2">raw := strings.TrimSpace(string(b)) + <span class="cov7" title="4">raw := strings.TrimSpace(string(b)) if raw == "" </span><span class="cov0" title="0">{ return InputParts{Selection: ""}, nil }</span> - <span class="cov4" title="2">lines := strings.Split(raw, "\n") + <span class="cov7" title="4">lines := strings.Split(raw, "\n") // find a case-insensitive line equal to "diagnostics:" diagsIdx := -1 - for i, ln := range lines </span><span class="cov4" title="2">{ + for i, ln := range lines </span><span class="cov7" title="5">{ t := strings.TrimSpace(strings.ToLower(ln)) if t == "diagnostics:" </span><span class="cov1" title="1">{ diagsIdx = i break</span> } } - <span class="cov4" title="2">if diagsIdx < 0 </span><span class="cov1" title="1">{ + <span class="cov7" title="4">if diagsIdx < 0 </span><span class="cov5" title="3">{ return InputParts{Selection: raw}, nil }</span> // collect diagnostics until a blank line or EOF <span class="cov1" title="1">diags := []string{} i := diagsIdx + 1 - for ; i < len(lines); i++ </span><span class="cov6" title="3">{ + for ; i < len(lines); i++ </span><span class="cov5" title="3">{ t := strings.TrimSpace(lines[i]) if t == "" </span><span class="cov1" title="1">{ i++ @@ -1291,7 +1291,7 @@ func ParseInput(r io.Reader) (InputParts, error) <span class="cov4" title="2">{ // ExtractInstruction mirrors the LSP instructionFromSelection behavior (subset), // scanning the first line for an instruction marker and removing it from the selection. -func ExtractInstruction(sel string) (string, string) <span class="cov10" title="7">{ return textutil.InstructionFromSelection(sel) }</span> +func ExtractInstruction(sel string) (string, string) <span class="cov10" title="8">{ return textutil.InstructionFromSelection(sel) }</span> // findFirstInstructionInLine follows the same precedence as LSP: // - ;text; (strict) @@ -1316,16 +1316,16 @@ import ( ) // Render performs simple {{var}} replacement like LSP. -func Render(t string, vars map[string]string) string <span class="cov9" title="8">{ return textutil.RenderTemplate(t, vars) }</span> +func Render(t string, vars map[string]string) string <span class="cov9" title="9">{ return textutil.RenderTemplate(t, vars) }</span> // StripFences removes surrounding markdown code fences. -func StripFences(s string) string <span class="cov10" title="9">{ return textutil.StripCodeFences(s) }</span> +func StripFences(s string) string <span class="cov10" title="10">{ return textutil.StripCodeFences(s) }</span> type chatDoer interface { Chat(ctx context.Context, msgs []llm.Message, opts ...llm.RequestOption) (string, error) } -func runRewrite(ctx context.Context, cfg appconfig.App, client chatDoer, instruction, selection string) (string, error) <span class="cov3" title="2">{ +func runRewrite(ctx context.Context, cfg appconfig.App, client chatDoer, instruction, selection string) (string, error) <span class="cov5" title="3">{ sys := cfg.PromptCodeActionRewriteSystem user := Render(cfg.PromptCodeActionRewriteUser, map[string]string{"instruction": instruction, "selection": selection}) return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) @@ -1368,26 +1368,26 @@ func runOnce(ctx context.Context, client chatDoer, sys, user string) (string, er <span class="cov1" title="1">return strings.TrimSpace(StripFences(txt)), nil</span> } -func runOnceWithOpts(ctx context.Context, client chatDoer, sys, user string, opts []llm.RequestOption) (string, error) <span class="cov8" title="7">{ +func runOnceWithOpts(ctx context.Context, client chatDoer, sys, user string, opts []llm.RequestOption) (string, error) <span class="cov9" title="8">{ msgs := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} txt, err := client.Chat(ctx, msgs, opts...) if err != nil </span><span class="cov0" title="0">{ return "", err }</span> - <span class="cov8" title="7">return strings.TrimSpace(StripFences(txt)), nil</span> + <span class="cov9" title="8">return strings.TrimSpace(StripFences(txt)), nil</span> } // reqOptsFrom builds LLM request options similar to LSP behavior. -func reqOptsFrom(cfg appconfig.App) []llm.RequestOption <span class="cov8" title="7">{ +func reqOptsFrom(cfg appconfig.App) []llm.RequestOption <span class="cov9" title="8">{ opts := []llm.RequestOption{llm.WithMaxTokens(cfg.MaxTokens)} - if cfg.CodingTemperature != nil </span><span class="cov5" title="3">{ + if cfg.CodingTemperature != nil </span><span class="cov6" title="4">{ opts = append(opts, llm.WithTemperature(*cfg.CodingTemperature)) }</span> - <span class="cov8" title="7">return opts</span> + <span class="cov9" title="8">return opts</span> } // Timeout helpers to mirror LSP behavior. -func timeout10s(parent context.Context) (context.Context, context.CancelFunc) <span class="cov3" title="2">{ +func timeout10s(parent context.Context) (context.Context, context.CancelFunc) <span class="cov5" title="3">{ return context.WithTimeout(parent, 10*time.Second) }</span> @@ -1411,45 +1411,49 @@ import ( ) // Run executes the hexai-action command flow. -func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error <span class="cov1" title="1">{ - logger := log.New(stderr, "hexai-action ", log.LstdFlags|log.Lmsgprefix) - cfg := appconfig.Load(logger) - client, err := llmutils.NewClientFromApp(cfg) - if err != nil </span><span class="cov1" title="1">{ - fmt.Fprintf(stderr, logging.AnsiBase+"hexai-action: LLM disabled: %v"+logging.AnsiReset+"\n", err) - return err - }</span> - <span class="cov0" title="0">parts, err := ParseInput(stdin) +// seams for testability +var chooseActionFn = RunTUI +var newClientFromApp = llmutils.NewClientFromApp + +func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error <span class="cov6" title="3">{ + logger := log.New(stderr, "hexai-action ", log.LstdFlags|log.Lmsgprefix) + cfg := appconfig.Load(logger) + client, err := newClientFromApp(cfg) + if err != nil </span><span class="cov1" title="1">{ + fmt.Fprintf(stderr, logging.AnsiBase+"hexai-action: LLM disabled: %v"+logging.AnsiReset+"\n", err) + return err + }</span> + <span class="cov4" title="2">parts, err := ParseInput(stdin) if err != nil </span><span class="cov0" title="0">{ fmt.Fprintln(stderr, logging.AnsiBase+"hexai-action: failed to read input"+logging.AnsiReset) return err }</span> - <span class="cov0" title="0">if strings.TrimSpace(parts.Selection) == "" </span><span class="cov0" title="0">{ + <span class="cov4" title="2">if strings.TrimSpace(parts.Selection) == "" </span><span class="cov0" title="0">{ return fmt.Errorf("hexai-action: no input provided on stdin") }</span> - <span class="cov0" title="0">kind, err := RunTUI() - if err != nil </span><span class="cov0" title="0">{ - return err - }</span> - <span class="cov0" title="0">out, err := executeAction(ctx, kind, parts, cfg, client, stderr) + <span class="cov4" title="2">kind, err := chooseActionFn() + if err != nil </span><span class="cov0" title="0">{ + return err + }</span> + <span class="cov4" title="2">out, err := executeAction(ctx, kind, parts, cfg, client, stderr) if err != nil </span><span class="cov0" title="0">{ return err }</span> - <span class="cov0" title="0">io.WriteString(stdout, out) + <span class="cov4" title="2">io.WriteString(stdout, out) return nil</span> } -func executeAction(ctx context.Context, kind ActionKind, parts InputParts, cfg appconfig.App, client chatDoer, stderr io.Writer) (string, error) <span class="cov10" title="4">{ +func executeAction(ctx context.Context, kind ActionKind, parts InputParts, cfg appconfig.App, client chatDoer, stderr io.Writer) (string, error) <span class="cov10" title="6">{ switch kind </span>{ - case ActionSkip:<span class="cov1" title="1"> + case ActionSkip:<span class="cov4" title="2"> return parts.Selection, nil</span> - case ActionRewrite:<span class="cov1" title="1"> + case ActionRewrite:<span class="cov4" title="2"> instr, cleaned := ExtractInstruction(parts.Selection) if strings.TrimSpace(instr) == "" </span><span class="cov0" title="0">{ fmt.Fprintln(stderr, logging.AnsiBase+"hexai-action: no inline instruction found; echoing input"+logging.AnsiReset) return parts.Selection, nil }</span> - <span class="cov1" title="1">cctx, cancel := timeout10s(ctx) + <span class="cov4" title="2">cctx, cancel := timeout10s(ctx) defer cancel() return runRewrite(cctx, cfg, client, instr, cleaned)</span> case ActionDiagnostics:<span class="cov0" title="0"> @@ -2901,8 +2905,8 @@ type Options struct { type RequestOption func(*Options) func WithModel(model string) RequestOption <span class="cov1" title="1">{ return func(o *Options) </span><span class="cov1" title="1">{ o.Model = model }</span> } -func WithTemperature(t float64) RequestOption <span class="cov4" title="4">{ return func(o *Options) </span><span class="cov1" title="1">{ o.Temperature = t }</span> } -func WithMaxTokens(n int) RequestOption <span class="cov10" title="32">{ return func(o *Options) </span><span class="cov1" title="1">{ o.MaxTokens = n }</span> } +func WithTemperature(t float64) RequestOption <span class="cov5" title="5">{ return func(o *Options) </span><span class="cov1" title="1">{ o.Temperature = t }</span> } +func WithMaxTokens(n int) RequestOption <span class="cov10" title="33">{ return func(o *Options) </span><span class="cov1" title="1">{ o.MaxTokens = n }</span> } func WithStop(stop ...string) RequestOption <span class="cov1" title="1">{ return func(o *Options) </span><span class="cov1" title="1">{ o.Stop = append([]string{}, stop...) }</span> } @@ -3069,11 +3073,11 @@ var std *log.Logger func Bind(l *log.Logger) <span class="cov2" title="3">{ std = l }</span> // Logf prints a formatted message with a module prefix and base ANSI style. -func Logf(prefix, format string, args ...any) <span class="cov10" title="143">{ +func Logf(prefix, format string, args ...any) <span class="cov10" title="144">{ if std == nil </span><span class="cov9" title="103">{ return }</span> - <span class="cov7" title="40">msg := fmt.Sprintf(format, args...) + <span class="cov7" title="41">msg := fmt.Sprintf(format, args...) std.Print(AnsiBase + prefix + msg + AnsiReset)</span> } @@ -5092,7 +5096,7 @@ func (s *Server) handleInitialize(req Request) <span class="cov10" title="2">{ s.reply(req.ID, res, nil)</span> } -func (s *Server) handleInitialized() <span class="cov0" title="0">{ +func (s *Server) handleInitialized() <span class="cov1" title="1">{ logging.Logf("lsp ", "client initialized") }</span> @@ -5694,70 +5698,70 @@ type ServerOptions struct { PromptGoTestUser string } -func NewServer(r io.Reader, w io.Writer, logger *log.Logger, opts ServerOptions) *Server <span class="cov10" title="6">{ +func NewServer(r io.Reader, w io.Writer, logger *log.Logger, opts ServerOptions) *Server <span class="cov10" title="7">{ s := &Server{in: bufio.NewReader(r), out: w, logger: logger, docs: make(map[string]*document), logContext: opts.LogContext} maxTokens := opts.MaxTokens - if maxTokens <= 0 </span><span class="cov9" title="5">{ + if maxTokens <= 0 </span><span class="cov9" title="6">{ maxTokens = 500 }</span> - <span class="cov10" title="6">s.maxTokens = maxTokens + <span class="cov10" title="7">s.maxTokens = maxTokens contextMode := opts.ContextMode - if contextMode == "" </span><span class="cov9" title="5">{ + if contextMode == "" </span><span class="cov9" title="6">{ contextMode = "file-on-new-func" }</span> - <span class="cov10" title="6">windowLines := opts.WindowLines - if windowLines <= 0 </span><span class="cov9" title="5">{ + <span class="cov10" title="7">windowLines := opts.WindowLines + if windowLines <= 0 </span><span class="cov9" title="6">{ windowLines = 120 }</span> - <span class="cov10" title="6">maxContextTokens := opts.MaxContextTokens - if maxContextTokens <= 0 </span><span class="cov9" title="5">{ + <span class="cov10" title="7">maxContextTokens := opts.MaxContextTokens + if maxContextTokens <= 0 </span><span class="cov9" title="6">{ maxContextTokens = 2000 }</span> - <span class="cov10" title="6">s.contextMode = contextMode + <span class="cov10" title="7">s.contextMode = contextMode s.windowLines = windowLines s.maxContextTokens = maxContextTokens s.startTime = time.Now() s.llmClient = opts.Client - if len(opts.TriggerCharacters) == 0 </span><span class="cov10" title="6">{ + if len(opts.TriggerCharacters) == 0 </span><span class="cov10" title="7">{ // Defaults (no space to avoid auto-trigger after whitespace) s.triggerChars = []string{".", ":", "/", "_", ")", "{"} }</span> else<span class="cov0" title="0"> { s.triggerChars = append([]string{}, opts.TriggerCharacters...) }</span> - <span class="cov10" title="6">s.codingTemperature = opts.CodingTemperature + <span class="cov10" title="7">s.codingTemperature = opts.CodingTemperature s.compCache = make(map[string]string) s.manualInvokeMinPrefix = opts.ManualInvokeMinPrefix if opts.CompletionDebounceMs > 0 </span><span class="cov1" title="1">{ s.completionDebounce = time.Duration(opts.CompletionDebounceMs) * time.Millisecond }</span> - <span class="cov10" title="6">if opts.CompletionThrottleMs > 0 </span><span class="cov0" title="0">{ + <span class="cov10" title="7">if opts.CompletionThrottleMs > 0 </span><span class="cov0" title="0">{ s.throttleInterval = time.Duration(opts.CompletionThrottleMs) * time.Millisecond }</span> // Trigger character config (with sane defaults if missing) - <span class="cov10" title="6">if strings.TrimSpace(opts.InlineOpen) == "" </span><span class="cov7" title="4">{ + <span class="cov10" title="7">if strings.TrimSpace(opts.InlineOpen) == "" </span><span class="cov8" title="5">{ s.inlineOpen = ">" }</span> else<span class="cov4" title="2"> { s.inlineOpen = opts.InlineOpen }</span> - <span class="cov10" title="6">if strings.TrimSpace(opts.InlineClose) == "" </span><span class="cov7" title="4">{ + <span class="cov10" title="7">if strings.TrimSpace(opts.InlineClose) == "" </span><span class="cov8" title="5">{ s.inlineClose = ">" }</span> else<span class="cov4" title="2"> { s.inlineClose = opts.InlineClose }</span> - <span class="cov10" title="6">if strings.TrimSpace(opts.ChatSuffix) == "" </span><span class="cov6" title="3">{ + <span class="cov10" title="7">if strings.TrimSpace(opts.ChatSuffix) == "" </span><span class="cov7" title="4">{ s.chatSuffix = ">" }</span> else<span class="cov6" title="3"> { s.chatSuffix = opts.ChatSuffix }</span> - <span class="cov10" title="6">if len(opts.ChatPrefixes) == 0 </span><span class="cov6" title="3">{ + <span class="cov10" title="7">if len(opts.ChatPrefixes) == 0 </span><span class="cov7" title="4">{ s.chatPrefixes = []string{"?", "!", ":", ";"} }</span> else<span class="cov6" title="3"> { s.chatPrefixes = append([]string{}, opts.ChatPrefixes...) }</span> // Prompts - <span class="cov10" title="6">s.promptCompSysGeneral = opts.PromptCompSysGeneral + <span class="cov10" title="7">s.promptCompSysGeneral = opts.PromptCompSysGeneral s.promptCompSysParams = opts.PromptCompSysParams s.promptCompSysInline = opts.PromptCompSysInline s.promptCompUserGeneral = opts.PromptCompUserGeneral @@ -5775,20 +5779,20 @@ func NewServer(r io.Reader, w io.Writer, logger *log.Logger, opts ServerOptions) s.promptGoTestUser = opts.PromptGoTestUser // Assign package-level inline trigger chars for free helper functions - if s.inlineOpen != "" </span><span class="cov10" title="6">{ + if s.inlineOpen != "" </span><span class="cov10" title="7">{ inlineOpenChar = s.inlineOpen[0] }</span> - <span class="cov10" title="6">if s.inlineClose != "" </span><span class="cov10" title="6">{ + <span class="cov10" title="7">if s.inlineClose != "" </span><span class="cov10" title="7">{ inlineCloseChar = s.inlineClose[0] }</span> - <span class="cov10" title="6">if s.chatSuffix != "" </span><span class="cov10" title="6">{ + <span class="cov10" title="7">if s.chatSuffix != "" </span><span class="cov10" title="7">{ chatSuffixChar = s.chatSuffix[0] }</span> - <span class="cov10" title="6">if len(s.chatPrefixes) > 0 </span><span class="cov10" title="6">{ + <span class="cov10" title="7">if len(s.chatPrefixes) > 0 </span><span class="cov10" title="7">{ chatPrefixSingles = append([]string{}, s.chatPrefixes...) }</span> // Initialize dispatch table - <span class="cov10" title="6">s.handlers = map[string]func(Request){ + <span class="cov10" title="7">s.handlers = map[string]func(Request){ "initialize": s.handleInitialize, "initialized": func(_ Request) </span><span class="cov0" title="0">{ s.handleInitialized() }</span>, "shutdown": s.handleShutdown, @@ -5801,7 +5805,7 @@ func NewServer(r io.Reader, w io.Writer, logger *log.Logger, opts ServerOptions) "codeAction/resolve": s.handleCodeActionResolve, "workspace/executeCommand": s.handleExecuteCommand, } - <span class="cov10" title="6">return s</span> + <span class="cov10" title="7">return s</span> } func (s *Server) Run() error <span class="cov1" title="1">{ @@ -5916,12 +5920,12 @@ func MultilineFunctionSuggestion() string <span class="cov8" title="1">{ }</span> // MarkdownCodeFence returns a fenced markdown snippet used in post-processing tests. -func MarkdownCodeFence() string <span class="cov0" title="0">{ +func MarkdownCodeFence() string <span class="cov8" title="1">{ return "```go\nname := value\n```" }</span> // MalformedJSON returns a deliberately malformed JSON string. -func MalformedJSON() string <span class="cov0" title="0">{ +func MalformedJSON() string <span class="cov8" title="1">{ return "{\"choices\":[{\"delta\":{\"content\":\"oops\"}}]" }</span> </pre> @@ -5931,51 +5935,51 @@ func MalformedJSON() string <span class="cov0" title="0">{ import "strings" // RenderTemplate performs simple {{var}} replacement in a template string. -func RenderTemplate(t string, vars map[string]string) string <span class="cov8" title="44">{ +func RenderTemplate(t string, vars map[string]string) string <span class="cov8" title="45">{ if t == "" || len(vars) == 0 </span><span class="cov5" title="11">{ return t }</span> - <span class="cov7" title="33">out := t - for k, v := range vars </span><span class="cov9" title="91">{ + <span class="cov7" title="34">out := t + for k, v := range vars </span><span class="cov9" title="93">{ out = strings.ReplaceAll(out, "{{"+k+"}}", v) }</span> - <span class="cov7" title="33">return out</span> + <span class="cov7" title="34">return out</span> } // StripCodeFences removes surrounding Markdown triple-backtick fences. -func StripCodeFences(s string) string <span class="cov8" title="50">{ +func StripCodeFences(s string) string <span class="cov8" title="51">{ t := strings.TrimSpace(s) if t == "" </span><span class="cov0" title="0">{ return t }</span> - <span class="cov8" title="50">lines := strings.Split(t, "\n") + <span class="cov8" title="51">lines := strings.Split(t, "\n") start := 0 for start < len(lines) && strings.TrimSpace(lines[start]) == "" </span><span class="cov0" title="0">{ start++ }</span> - <span class="cov8" title="50">end := len(lines) - 1 + <span class="cov8" title="51">end := len(lines) - 1 for end >= 0 && strings.TrimSpace(lines[end]) == "" </span><span class="cov0" title="0">{ end-- }</span> - <span class="cov8" title="50">if start >= len(lines) || end < 0 || start > end </span><span class="cov0" title="0">{ + <span class="cov8" title="51">if start >= len(lines) || end < 0 || start > end </span><span class="cov0" title="0">{ return t }</span> - <span class="cov8" title="50">first := strings.TrimSpace(lines[start]) + <span class="cov8" title="51">first := strings.TrimSpace(lines[start]) last := strings.TrimSpace(lines[end]) if strings.HasPrefix(first, "```") && last == "```" && end > start </span><span class="cov6" title="20">{ inner := strings.Join(lines[start+1:end], "\n") return inner }</span> - <span class="cov7" title="30">return t</span> + <span class="cov7" title="31">return t</span> } // InstructionFromSelection extracts the first inline instruction and returns // (instruction, cleanedSelection). It detects markers on the earliest position // per line in precedence: strict ;text;, /* */, <!-- -->, //, #, --. -func InstructionFromSelection(sel string) (string, string) <span class="cov5" title="13">{ +func InstructionFromSelection(sel string) (string, string) <span class="cov6" title="14">{ lines := strings.Split(sel, "\n") - for idx, line := range lines </span><span class="cov5" title="13">{ - if instr, cleaned, ok := FindFirstInstructionInLine(line); ok && strings.TrimSpace(instr) != "" </span><span class="cov5" title="13">{ + for idx, line := range lines </span><span class="cov6" title="14">{ + if instr, cleaned, ok := FindFirstInstructionInLine(line); ok && strings.TrimSpace(instr) != "" </span><span class="cov6" title="14">{ lines[idx] = cleaned return instr, strings.Join(lines, "\n") }</span> @@ -5984,13 +5988,13 @@ func InstructionFromSelection(sel string) (string, string) <span class="cov5" ti } // FindFirstInstructionInLine returns (instruction, cleaned, ok) for a single line. -func FindFirstInstructionInLine(line string) (instr, cleaned string, ok bool) <span class="cov6" title="14">{ +func FindFirstInstructionInLine(line string) (instr, cleaned string, ok bool) <span class="cov6" title="15">{ type cand struct{ start, end int; text string } cands := []cand{} - if t, l, r, ok := FindStrictInlineTag(line); ok </span><span class="cov3" title="4">{ + if t, l, r, ok := FindStrictInlineTag(line); ok </span><span class="cov4" title="5">{ cands = append(cands, cand{start: l, end: r, text: t}) }</span> - <span class="cov6" title="14">if i := strings.Index(line, "/*"); i >= 0 </span><span class="cov2" title="2">{ + <span class="cov6" title="15">if i := strings.Index(line, "/*"); i >= 0 </span><span class="cov2" title="2">{ if j := strings.Index(line[i+2:], "*/"); j >= 0 </span><span class="cov2" title="2">{ start := i end := i + 2 + j + 2 @@ -5998,7 +6002,7 @@ func FindFirstInstructionInLine(line string) (instr, cleaned string, ok bool) <s cands = append(cands, cand{start: start, end: end, text: text}) }</span> } - <span class="cov6" title="14">if i := strings.Index(line, "<!--"); i >= 0 </span><span class="cov2" title="2">{ + <span class="cov6" title="15">if i := strings.Index(line, "<!--"); i >= 0 </span><span class="cov2" title="2">{ if j := strings.Index(line[i+4:], "-->"); j >= 0 </span><span class="cov2" title="2">{ start := i end := i + 4 + j + 3 @@ -6006,34 +6010,34 @@ func FindFirstInstructionInLine(line string) (instr, cleaned string, ok bool) <s cands = append(cands, cand{start: start, end: end, text: text}) }</span> } - <span class="cov6" title="14">if i := strings.Index(line, "//"); i >= 0 </span><span class="cov3" title="3">{ + <span class="cov6" title="15">if i := strings.Index(line, "//"); i >= 0 </span><span class="cov3" title="3">{ cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])}) }</span> - <span class="cov6" title="14">if i := strings.Index(line, "#"); i >= 0 </span><span class="cov2" title="2">{ + <span class="cov6" title="15">if i := strings.Index(line, "#"); i >= 0 </span><span class="cov2" title="2">{ cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+1:])}) }</span> - <span class="cov6" title="14">if i := strings.Index(line, "--"); i >= 0 </span><span class="cov3" title="4">{ + <span class="cov6" title="15">if i := strings.Index(line, "--"); i >= 0 </span><span class="cov3" title="4">{ cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])}) }</span> - <span class="cov6" title="14">if len(cands) == 0 </span><span class="cov0" title="0">{ return "", line, false }</span> - <span class="cov6" title="14">best := cands[0] + <span class="cov6" title="15">if len(cands) == 0 </span><span class="cov0" title="0">{ return "", line, false }</span> + <span class="cov6" title="15">best := cands[0] for _, c := range cands[1:] </span><span class="cov3" title="3">{ if c.start >= 0 && (best.start < 0 || c.start < best.start) </span><span class="cov0" title="0">{ best = c }</span> } - <span class="cov6" title="14">cleaned = strings.TrimRight(line[:best.start]+line[best.end:], " \t") + <span class="cov6" title="15">cleaned = strings.TrimRight(line[:best.start]+line[best.end:], " \t") return best.text, cleaned, true</span> } // FindStrictInlineTag finds ;text; with no spaces after/before semicolons. -func FindStrictInlineTag(line string) (text string, left, right int, ok bool) <span class="cov6" title="16">{ - for i := 0; i < len(line); i++ </span><span class="cov10" title="112">{ +func FindStrictInlineTag(line string) (text string, left, right int, ok bool) <span class="cov6" title="17">{ + for i := 0; i < len(line); i++ </span><span class="cov10" title="113">{ if line[i] != ';' </span><span class="cov9" title="105">{ continue</span> } - <span class="cov4" title="7">if i+1 < len(line) && line[i+1] == ' ' </span><span class="cov1" title="1">{ continue</span> } - <span class="cov4" title="6">for j := i + 1; j < len(line); j++ </span><span class="cov7" title="35">{ - if line[j] == ';' </span><span class="cov4" title="5">{ + <span class="cov4" title="8">if i+1 < len(line) && line[i+1] == ' ' </span><span class="cov1" title="1">{ continue</span> } + <span class="cov4" title="7">for j := i + 1; j < len(line); j++ </span><span class="cov8" title="41">{ + if line[j] == ';' </span><span class="cov4" title="6">{ if j-1 >= 0 && line[j-1] == ' ' </span><span class="cov0" title="0">{ continue</span> } - <span class="cov4" title="5">inner := strings.TrimSpace(line[i+1 : j]) - if inner != "" </span><span class="cov4" title="5">{ return inner, i, j + 1, true }</span> + <span class="cov4" title="6">inner := strings.TrimSpace(line[i+1 : j]) + if inner != "" </span><span class="cov4" title="6">{ return inner, i, j + 1, true }</span> } } } |
