diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-06 13:18:21 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-06 13:18:21 +0300 |
| commit | 5e966f50111adf6e2cb2683fe588f6fe033fa931 (patch) | |
| tree | 19ac2033483c2ac6147e8f44ac37f14e6a5c0cf7 /docs/coverage.html | |
| parent | 80e61812986573464cd24c4b3ffa605c4003146a (diff) | |
fix unit test coverage
Diffstat (limited to 'docs/coverage.html')
| -rw-r--r-- | docs/coverage.html | 2585 |
1 files changed, 1559 insertions, 1026 deletions
diff --git a/docs/coverage.html b/docs/coverage.html index d22ef74..6b80630 100644 --- a/docs/coverage.html +++ b/docs/coverage.html @@ -55,53 +55,69 @@ <div id="nav"> <select id="files"> - <option value="file0">codeberg.org/snonux/hexai/cmd/hexai-lsp/main.go (0.0%)</option> + <option value="file0">codeberg.org/snonux/hexai/cmd/hexai-lsp/main.go (75.0%)</option> - <option value="file1">codeberg.org/snonux/hexai/cmd/hexai/main.go (0.0%)</option> + <option value="file1">codeberg.org/snonux/hexai/cmd/hexai/main.go (71.4%)</option> - <option value="file2">codeberg.org/snonux/hexai/internal/appconfig/config.go (91.6%)</option> + <option value="file2">codeberg.org/snonux/hexai/cmd/internal/hexai-action/main.go (0.0%)</option> - <option value="file3">codeberg.org/snonux/hexai/internal/hexaicli/run.go (72.6%)</option> + <option value="file3">codeberg.org/snonux/hexai/internal/appconfig/config.go (91.6%)</option> - <option value="file4">codeberg.org/snonux/hexai/internal/hexailsp/run.go (92.5%)</option> + <option value="file4">codeberg.org/snonux/hexai/internal/hexaiaction/parse.go (92.6%)</option> - <option value="file5">codeberg.org/snonux/hexai/internal/llm/copilot.go (81.8%)</option> + <option value="file5">codeberg.org/snonux/hexai/internal/hexaiaction/prompts.go (81.1%)</option> - <option value="file6">codeberg.org/snonux/hexai/internal/llm/ollama.go (88.0%)</option> + <option value="file6">codeberg.org/snonux/hexai/internal/hexaiaction/run.go (33.3%)</option> - <option value="file7">codeberg.org/snonux/hexai/internal/llm/openai.go (85.5%)</option> + <option value="file7">codeberg.org/snonux/hexai/internal/hexaiaction/tui.go (47.3%)</option> - <option value="file8">codeberg.org/snonux/hexai/internal/llm/provider.go (100.0%)</option> + <option value="file8">codeberg.org/snonux/hexai/internal/hexaiaction/tui_delegate.go (91.7%)</option> - <option value="file9">codeberg.org/snonux/hexai/internal/llm/util.go (100.0%)</option> + <option value="file9">codeberg.org/snonux/hexai/internal/hexaicli/run.go (78.8%)</option> - <option value="file10">codeberg.org/snonux/hexai/internal/logging/chatlogger.go (100.0%)</option> + <option value="file10">codeberg.org/snonux/hexai/internal/hexailsp/run.go (92.5%)</option> - <option value="file11">codeberg.org/snonux/hexai/internal/logging/logging.go (100.0%)</option> + <option value="file11">codeberg.org/snonux/hexai/internal/llm/copilot.go (82.4%)</option> - <option value="file12">codeberg.org/snonux/hexai/internal/lsp/context.go (74.4%)</option> + <option value="file12">codeberg.org/snonux/hexai/internal/llm/ollama.go (89.8%)</option> - <option value="file13">codeberg.org/snonux/hexai/internal/lsp/document.go (90.1%)</option> + <option value="file13">codeberg.org/snonux/hexai/internal/llm/openai.go (85.5%)</option> - <option value="file14">codeberg.org/snonux/hexai/internal/lsp/handlers.go (92.9%)</option> + <option value="file14">codeberg.org/snonux/hexai/internal/llm/provider.go (100.0%)</option> - <option value="file15">codeberg.org/snonux/hexai/internal/lsp/handlers_codeaction.go (81.2%)</option> + <option value="file15">codeberg.org/snonux/hexai/internal/llm/util.go (100.0%)</option> - <option value="file16">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (87.6%)</option> + <option value="file16">codeberg.org/snonux/hexai/internal/llmutils/client.go (100.0%)</option> - <option value="file17">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (88.9%)</option> + <option value="file17">codeberg.org/snonux/hexai/internal/logging/chatlogger.go (100.0%)</option> - <option value="file18">codeberg.org/snonux/hexai/internal/lsp/handlers_execute.go (75.0%)</option> + <option value="file18">codeberg.org/snonux/hexai/internal/logging/logging.go (90.9%)</option> - <option value="file19">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (55.6%)</option> + <option value="file19">codeberg.org/snonux/hexai/internal/lsp/context.go (74.4%)</option> - <option value="file20">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (88.5%)</option> + <option value="file20">codeberg.org/snonux/hexai/internal/lsp/document.go (90.1%)</option> - <option value="file21">codeberg.org/snonux/hexai/internal/lsp/server.go (82.1%)</option> + <option value="file21">codeberg.org/snonux/hexai/internal/lsp/handlers.go (92.9%)</option> - <option value="file22">codeberg.org/snonux/hexai/internal/lsp/transport.go (71.4%)</option> + <option value="file22">codeberg.org/snonux/hexai/internal/lsp/handlers_codeaction.go (81.9%)</option> - <option value="file23">codeberg.org/snonux/hexai/internal/testutil/fixtures.go (60.0%)</option> + <option value="file23">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (87.6%)</option> + + <option value="file24">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (88.9%)</option> + + <option value="file25">codeberg.org/snonux/hexai/internal/lsp/handlers_execute.go (75.0%)</option> + + <option value="file26">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (55.6%)</option> + + <option value="file27">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (89.0%)</option> + + <option value="file28">codeberg.org/snonux/hexai/internal/lsp/server.go (82.1%)</option> + + <option value="file29">codeberg.org/snonux/hexai/internal/lsp/transport.go (71.4%)</option> + + <option value="file30">codeberg.org/snonux/hexai/internal/testutil/fixtures.go (60.0%)</option> + + <option value="file31">codeberg.org/snonux/hexai/internal/textutil/textutil.go (89.0%)</option> </select> </div> @@ -136,11 +152,11 @@ import ( "codeberg.org/snonux/hexai/internal/hexailsp" ) -func main() <span class="cov0" title="0">{ +func main() <span class="cov8" title="1">{ logPath := flag.String("log", "/tmp/hexai-lsp.log", "path to log file (optional)") showVersion := flag.Bool("version", false, "print version and exit") flag.Parse() - if *showVersion </span><span class="cov0" title="0">{ + if *showVersion </span><span class="cov8" title="1">{ log.Println(internal.Version) return }</span> @@ -164,10 +180,10 @@ import ( "codeberg.org/snonux/hexai/internal/hexaicli" ) -func main() <span class="cov0" title="0">{ +func main() <span class="cov8" title="1">{ showVersion := flag.Bool("version", false, "print version and exit") flag.Parse() - if *showVersion </span><span class="cov0" title="0">{ + if *showVersion </span><span class="cov8" title="1">{ fmt.Fprintln(os.Stdout, internal.Version) return }</span> @@ -178,7 +194,25 @@ func main() <span class="cov0" title="0">{ } </pre> - <pre class="file" id="file2" style="display: none">// Summary: Application configuration model and loader; reads ~/.config/hexai/config.toml and merges defaults. + <pre class="file" id="file2" style="display: none">package main + +import ( + "context" + "fmt" + "os" + + "codeberg.org/snonux/hexai/internal/hexaiaction" +) + +func main() <span class="cov0" title="0">{ + if err := hexaiaction.Run(context.Background(), os.Stdin, os.Stdout, os.Stderr); err != nil </span><span class="cov0" title="0">{ + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + }</span> +} +</pre> + + <pre class="file" id="file3" style="display: none">// Summary: Application configuration model and loader; reads ~/.config/hexai/config.toml and merges defaults. package appconfig import ( @@ -234,43 +268,43 @@ type App struct { // Default temperature for Ollama requests (nil means use provider default) OllamaTemperature *float64 `json:"ollama_temperature" toml:"ollama_temperature"` CopilotBaseURL string `json:"copilot_base_url" toml:"copilot_base_url"` - CopilotModel string `json:"copilot_model" toml:"copilot_model"` - // Default temperature for Copilot requests (nil means use provider default) - CopilotTemperature *float64 `json:"copilot_temperature" toml:"copilot_temperature"` - - // Prompt templates (configured only via file; no env overrides) - // Completion/chat/code action/CLI prompt strings. See config.toml.example for placeholders. - // Completion - PromptCompletionSystemGeneral string `json:"-" toml:"-"` - PromptCompletionSystemParams string `json:"-" toml:"-"` - PromptCompletionSystemInline string `json:"-" toml:"-"` - PromptCompletionUserGeneral string `json:"-" toml:"-"` - PromptCompletionUserParams string `json:"-" toml:"-"` - PromptCompletionExtraHeader string `json:"-" toml:"-"` - // Provider-native code-completer - PromptNativeCompletion string `json:"-" toml:"-"` - // In-editor chat - PromptChatSystem string `json:"-" toml:"-"` - // Code actions - PromptCodeActionRewriteSystem string `json:"-" toml:"-"` - PromptCodeActionDiagnosticsSystem string `json:"-" toml:"-"` - PromptCodeActionDocumentSystem string `json:"-" toml:"-"` - PromptCodeActionRewriteUser string `json:"-" toml:"-"` - PromptCodeActionDiagnosticsUser string `json:"-" toml:"-"` - PromptCodeActionDocumentUser string `json:"-" toml:"-"` - PromptCodeActionGoTestSystem string `json:"-" toml:"-"` - PromptCodeActionGoTestUser string `json:"-" toml:"-"` - // CLI - PromptCLIDefaultSystem string `json:"-" toml:"-"` - PromptCLIExplainSystem string `json:"-" toml:"-"` + CopilotModel string `json:"copilot_model" toml:"copilot_model"` + // Default temperature for Copilot requests (nil means use provider default) + CopilotTemperature *float64 `json:"copilot_temperature" toml:"copilot_temperature"` + + // Prompt templates (configured only via file; no env overrides) + // Completion/chat/code action/CLI prompt strings. See config.toml.example for placeholders. + // Completion + PromptCompletionSystemGeneral string `json:"-" toml:"-"` + PromptCompletionSystemParams string `json:"-" toml:"-"` + PromptCompletionSystemInline string `json:"-" toml:"-"` + PromptCompletionUserGeneral string `json:"-" toml:"-"` + PromptCompletionUserParams string `json:"-" toml:"-"` + PromptCompletionExtraHeader string `json:"-" toml:"-"` + // Provider-native code-completer + PromptNativeCompletion string `json:"-" toml:"-"` + // In-editor chat + PromptChatSystem string `json:"-" toml:"-"` + // Code actions + PromptCodeActionRewriteSystem string `json:"-" toml:"-"` + PromptCodeActionDiagnosticsSystem string `json:"-" toml:"-"` + PromptCodeActionDocumentSystem string `json:"-" toml:"-"` + PromptCodeActionRewriteUser string `json:"-" toml:"-"` + PromptCodeActionDiagnosticsUser string `json:"-" toml:"-"` + PromptCodeActionDocumentUser string `json:"-" toml:"-"` + PromptCodeActionGoTestSystem string `json:"-" toml:"-"` + PromptCodeActionGoTestUser string `json:"-" toml:"-"` + // CLI + PromptCLIDefaultSystem string `json:"-" toml:"-"` + PromptCLIExplainSystem string `json:"-" toml:"-"` } // Constructor: defaults for App (kept first among functions) -func newDefaultConfig() App <span class="cov5" title="11">{ +func newDefaultConfig() App <span class="cov5" title="13">{ // Coding-friendly default temperature across providers // Users can override per provider in config.toml (including 0.0). t := 0.2 - return App{ + return App{ MaxTokens: 4000, ContextMode: "always-full", ContextWindowLines: 120, @@ -287,47 +321,47 @@ func newDefaultConfig() App <span class="cov5" title="11">{ InlineOpen: ">", InlineClose: ">", ChatSuffix: ">", - ChatPrefixes: []string{"?", "!", ":", ";"}, - - // Default prompt templates (match current hard-coded strings) - PromptCompletionSystemParams: "You are a code completion engine for function signatures. Return only the parameter list contents (without parentheses), no braces, no prose. Prefer idiomatic names and types.", - PromptCompletionUserParams: "Cursor is inside the function parameter list. Suggest only the parameter list (no parentheses).\nFunction line: {{function}}\nCurrent line (cursor at {{char}}): {{current}}", - PromptCompletionSystemGeneral: "You are a terse code completion engine. Return only the code to insert, no surrounding prose or backticks. Only continue from the cursor; never repeat characters already present to the left of the cursor on the current line (e.g., if 'name :=' is already typed, only return the right-hand side expression).", - PromptCompletionUserGeneral: "Provide the next likely code to insert at the cursor.\nFile: {{file}}\nFunction/context: {{function}}\nAbove line: {{above}}\nCurrent line (cursor at character {{char}}): {{current}}\nBelow line: {{below}}\nOnly return the completion snippet.", - PromptCompletionSystemInline: "You are a precise code completion/refactoring engine. Output only the code to insert with no prose, no comments, and no backticks. Return raw code only.", - PromptCompletionExtraHeader: "Additional context:\n{{context}}", - - PromptNativeCompletion: "// Path: {{path}}\n{{before}}", - - PromptChatSystem: "You are a helpful coding assistant. Answer concisely and clearly.", - - PromptCodeActionRewriteSystem: "You are a precise code refactoring engine. Rewrite the given code strictly according to the instruction. Return only the updated code with no prose or backticks. Preserve formatting where reasonable.", - PromptCodeActionDiagnosticsSystem: "You are a precise code fixer. Resolve the given diagnostics by editing only the selected code. Return only the corrected code with no prose or backticks. Keep behavior and style, and avoid unrelated changes.", - PromptCodeActionDocumentSystem: "You are a precise code documentation engine. Add idiomatic documentation comments to the given code. Preserve exact behavior and formatting as much as possible. Return only the updated code with comments, no prose or backticks.", - PromptCodeActionRewriteUser: "Instruction: {{instruction}}\n\nSelected code to transform:\n{{selection}}", - PromptCodeActionDiagnosticsUser: "Diagnostics to resolve (selection only):\n{{diagnostics}}\n\nSelected code:\n{{selection}}", - PromptCodeActionDocumentUser: "Add documentation comments to this code:\n{{selection}}", - PromptCodeActionGoTestSystem: "You are a precise Go unit test generator. Given a Go function, write one or more Test* functions using the testing package. Do NOT include package or imports, only the test function(s). Prefer table-driven tests. Keep it minimal and idiomatic.", - PromptCodeActionGoTestUser: "Function under test:\n{{function}}", - - PromptCLIDefaultSystem: "You are Hexai CLI. Default to very short, concise answers. If the user asks for commands, output only the commands (one per line) with no commentary or explanation. Only when the word 'explain' appears in the prompt, produce a verbose explanation.", - PromptCLIExplainSystem: "You are Hexai CLI. The user requested an explanation. Provide a clear, verbose explanation with reasoning and details. If commands are needed, include them with brief context.", - } + ChatPrefixes: []string{"?", "!", ":", ";"}, + + // Default prompt templates (match current hard-coded strings) + PromptCompletionSystemParams: "You are a code completion engine for function signatures. Return only the parameter list contents (without parentheses), no braces, no prose. Prefer idiomatic names and types.", + PromptCompletionUserParams: "Cursor is inside the function parameter list. Suggest only the parameter list (no parentheses).\nFunction line: {{function}}\nCurrent line (cursor at {{char}}): {{current}}", + PromptCompletionSystemGeneral: "You are a terse code completion engine. Return only the code to insert, no surrounding prose or backticks. Only continue from the cursor; never repeat characters already present to the left of the cursor on the current line (e.g., if 'name :=' is already typed, only return the right-hand side expression).", + PromptCompletionUserGeneral: "Provide the next likely code to insert at the cursor.\nFile: {{file}}\nFunction/context: {{function}}\nAbove line: {{above}}\nCurrent line (cursor at character {{char}}): {{current}}\nBelow line: {{below}}\nOnly return the completion snippet.", + PromptCompletionSystemInline: "You are a precise code completion/refactoring engine. Output only the code to insert with no prose, no comments, and no backticks. Return raw code only.", + PromptCompletionExtraHeader: "Additional context:\n{{context}}", + + PromptNativeCompletion: "// Path: {{path}}\n{{before}}", + + PromptChatSystem: "You are a helpful coding assistant. Answer concisely and clearly.", + + PromptCodeActionRewriteSystem: "You are a precise code refactoring engine. Rewrite the given code strictly according to the instruction. Return only the updated code with no prose or backticks. Preserve formatting where reasonable.", + PromptCodeActionDiagnosticsSystem: "You are a precise code fixer. Resolve the given diagnostics by editing only the selected code. Return only the corrected code with no prose or backticks. Keep behavior and style, and avoid unrelated changes.", + PromptCodeActionDocumentSystem: "You are a precise code documentation engine. Add idiomatic documentation comments to the given code. Preserve exact behavior and formatting as much as possible. Return only the updated code with comments, no prose or backticks.", + PromptCodeActionRewriteUser: "Instruction: {{instruction}}\n\nSelected code to transform:\n{{selection}}", + PromptCodeActionDiagnosticsUser: "Diagnostics to resolve (selection only):\n{{diagnostics}}\n\nSelected code:\n{{selection}}", + PromptCodeActionDocumentUser: "Add documentation comments to this code:\n{{selection}}", + PromptCodeActionGoTestSystem: "You are a precise Go unit test generator. Given a Go function, write one or more Test* functions using the testing package. Do NOT include package or imports, only the test function(s). Prefer table-driven tests. Keep it minimal and idiomatic.", + PromptCodeActionGoTestUser: "Function under test:\n{{function}}", + + PromptCLIDefaultSystem: "You are Hexai CLI. Default to very short, concise answers. If the user asks for commands, output only the commands (one per line) with no commentary or explanation. Only when the word 'explain' appears in the prompt, produce a verbose explanation.", + PromptCLIExplainSystem: "You are Hexai CLI. The user requested an explanation. Provide a clear, verbose explanation with reasoning and details. If commands are needed, include them with brief context.", + } }</span> // 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="10">{ +func Load(logger *log.Logger) App <span class="cov5" title="12">{ cfg := newDefaultConfig() - if logger == nil </span><span class="cov2" title="3">{ + 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="7">configPath, err := getConfigPath() + <span class="cov4" title="8">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="7"> { + }</span> else<span class="cov4" title="8"> { if fileCfg, err := loadFromFile(configPath, logger); err == nil && fileCfg != nil </span><span class="cov3" title="4">{ cfg.mergeWith(fileCfg) }</span> @@ -336,359 +370,359 @@ func Load(logger *log.Logger) App <span class="cov5" title="10">{ } // Environment overrides (take precedence over file) - <span class="cov4" title="7">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{ + <span class="cov4" title="8">if envCfg := loadFromEnv(logger); envCfg != nil </span><span class="cov1" title="1">{ cfg.mergeWith(envCfg) }</span> - <span class="cov4" title="7">return cfg</span> + <span class="cov4" title="8">return cfg</span> } // Private helpers // Sectioned (table-based) file format only. type fileConfig struct { - // Section tables only (flat keys are not allowed) - General sectionGeneral `toml:"general"` - Logging sectionLogging `toml:"logging"` - Completion sectionCompletion `toml:"completion"` - Triggers sectionTriggers `toml:"triggers"` - Inline sectionInline `toml:"inline"` - Chat sectionChat `toml:"chat"` - Provider sectionProvider `toml:"provider"` - OpenAI sectionOpenAI `toml:"openai"` - Copilot sectionCopilot `toml:"copilot"` - Ollama sectionOllama `toml:"ollama"` - Prompts sectionPrompts `toml:"prompts"` + // Section tables only (flat keys are not allowed) + General sectionGeneral `toml:"general"` + Logging sectionLogging `toml:"logging"` + Completion sectionCompletion `toml:"completion"` + Triggers sectionTriggers `toml:"triggers"` + Inline sectionInline `toml:"inline"` + Chat sectionChat `toml:"chat"` + Provider sectionProvider `toml:"provider"` + OpenAI sectionOpenAI `toml:"openai"` + Copilot sectionCopilot `toml:"copilot"` + Ollama sectionOllama `toml:"ollama"` + Prompts sectionPrompts `toml:"prompts"` } type sectionGeneral struct { - MaxTokens int `toml:"max_tokens"` - ContextMode string `toml:"context_mode"` - ContextWindowLines int `toml:"context_window_lines"` - MaxContextTokens int `toml:"max_context_tokens"` - CodingTemperature *float64 `toml:"coding_temperature"` + MaxTokens int `toml:"max_tokens"` + ContextMode string `toml:"context_mode"` + ContextWindowLines int `toml:"context_window_lines"` + MaxContextTokens int `toml:"max_context_tokens"` + CodingTemperature *float64 `toml:"coding_temperature"` } type sectionLogging struct { - LogPreviewLimit int `toml:"log_preview_limit"` + LogPreviewLimit int `toml:"log_preview_limit"` } type sectionCompletion struct { - CompletionDebounceMs int `toml:"completion_debounce_ms"` - CompletionThrottleMs int `toml:"completion_throttle_ms"` - ManualInvokeMinPrefix int `toml:"manual_invoke_min_prefix"` + CompletionDebounceMs int `toml:"completion_debounce_ms"` + CompletionThrottleMs int `toml:"completion_throttle_ms"` + ManualInvokeMinPrefix int `toml:"manual_invoke_min_prefix"` } type sectionTriggers struct { - TriggerCharacters []string `toml:"trigger_characters"` + TriggerCharacters []string `toml:"trigger_characters"` } type sectionInline struct { - InlineOpen string `toml:"inline_open"` - InlineClose string `toml:"inline_close"` + InlineOpen string `toml:"inline_open"` + InlineClose string `toml:"inline_close"` } type sectionChat struct { - ChatSuffix string `toml:"chat_suffix"` - ChatPrefixes []string `toml:"chat_prefixes"` + ChatSuffix string `toml:"chat_suffix"` + ChatPrefixes []string `toml:"chat_prefixes"` } type sectionProvider struct { - Name string `toml:"name"` + Name string `toml:"name"` } type sectionOpenAI struct { - Model string `toml:"model"` - BaseURL string `toml:"base_url"` - Temperature *float64 `toml:"temperature"` + Model string `toml:"model"` + BaseURL string `toml:"base_url"` + Temperature *float64 `toml:"temperature"` } type sectionCopilot struct { - Model string `toml:"model"` - BaseURL string `toml:"base_url"` - Temperature *float64 `toml:"temperature"` + Model string `toml:"model"` + BaseURL string `toml:"base_url"` + Temperature *float64 `toml:"temperature"` } type sectionOllama struct { - Model string `toml:"model"` - BaseURL string `toml:"base_url"` - Temperature *float64 `toml:"temperature"` + Model string `toml:"model"` + BaseURL string `toml:"base_url"` + Temperature *float64 `toml:"temperature"` } // Prompts sections type sectionPrompts struct { - Completion sectionPromptsCompletion `toml:"completion"` - Chat sectionPromptsChat `toml:"chat"` - CodeAction sectionPromptsCodeAction `toml:"code_action"` - CLI sectionPromptsCLI `toml:"cli"` - ProviderNative sectionPromptsProviderNative `toml:"provider_native"` + Completion sectionPromptsCompletion `toml:"completion"` + Chat sectionPromptsChat `toml:"chat"` + CodeAction sectionPromptsCodeAction `toml:"code_action"` + CLI sectionPromptsCLI `toml:"cli"` + ProviderNative sectionPromptsProviderNative `toml:"provider_native"` } type sectionPromptsCompletion struct { - SystemGeneral string `toml:"system_general"` - SystemParams string `toml:"system_params"` - SystemInline string `toml:"system_inline"` - UserGeneral string `toml:"user_general"` - UserParams string `toml:"user_params"` - ExtraHeader string `toml:"additional_context"` + SystemGeneral string `toml:"system_general"` + SystemParams string `toml:"system_params"` + SystemInline string `toml:"system_inline"` + UserGeneral string `toml:"user_general"` + UserParams string `toml:"user_params"` + ExtraHeader string `toml:"additional_context"` } type sectionPromptsChat struct { - System string `toml:"system"` + System string `toml:"system"` } type sectionPromptsCodeAction struct { - RewriteSystem string `toml:"rewrite_system"` - DiagnosticsSystem string `toml:"diagnostics_system"` - DocumentSystem string `toml:"document_system"` - RewriteUser string `toml:"rewrite_user"` - DiagnosticsUser string `toml:"diagnostics_user"` - DocumentUser string `toml:"document_user"` - GoTestSystem string `toml:"go_test_system"` - GoTestUser string `toml:"go_test_user"` + RewriteSystem string `toml:"rewrite_system"` + DiagnosticsSystem string `toml:"diagnostics_system"` + DocumentSystem string `toml:"document_system"` + RewriteUser string `toml:"rewrite_user"` + DiagnosticsUser string `toml:"diagnostics_user"` + DocumentUser string `toml:"document_user"` + GoTestSystem string `toml:"go_test_system"` + GoTestUser string `toml:"go_test_user"` } type sectionPromptsCLI struct { - DefaultSystem string `toml:"default_system"` - ExplainSystem string `toml:"explain_system"` + DefaultSystem string `toml:"default_system"` + ExplainSystem string `toml:"explain_system"` } type sectionPromptsProviderNative struct { - Completion string `toml:"completion"` + Completion string `toml:"completion"` } func (fc *fileConfig) toApp() App <span class="cov3" title="4">{ - out := App{} - - // Merge section: general - if (fc.General != sectionGeneral{}) || fc.General.CodingTemperature != nil </span><span class="cov2" title="3">{ - tmp := App{ - MaxTokens: fc.General.MaxTokens, - ContextMode: fc.General.ContextMode, - ContextWindowLines: fc.General.ContextWindowLines, - MaxContextTokens: fc.General.MaxContextTokens, - CodingTemperature: fc.General.CodingTemperature, - } - out.mergeBasics(&tmp) - }</span> - - // logging - <span class="cov3" title="4">if (fc.Logging != sectionLogging{}) </span><span class="cov1" title="1">{ - tmp := App{LogPreviewLimit: fc.Logging.LogPreviewLimit} - out.mergeBasics(&tmp) - }</span> - - // completion - <span class="cov3" title="4">if (fc.Completion != sectionCompletion{}) </span><span class="cov2" title="3">{ - tmp := App{ - CompletionDebounceMs: fc.Completion.CompletionDebounceMs, - CompletionThrottleMs: fc.Completion.CompletionThrottleMs, - ManualInvokeMinPrefix: fc.Completion.ManualInvokeMinPrefix, - } - out.mergeBasics(&tmp) - }</span> - - // triggers - <span class="cov3" title="4">if len(fc.Triggers.TriggerCharacters) > 0 </span><span class="cov2" title="3">{ - tmp := App{TriggerCharacters: fc.Triggers.TriggerCharacters} - out.mergeBasics(&tmp) - }</span> - - // inline - <span class="cov3" title="4">if (fc.Inline != sectionInline{}) </span><span class="cov1" title="1">{ - tmp := App{InlineOpen: fc.Inline.InlineOpen, InlineClose: fc.Inline.InlineClose} - out.mergeBasics(&tmp) - }</span> - - // chat - <span class="cov3" title="4">if strings.TrimSpace(fc.Chat.ChatSuffix) != "" || len(fc.Chat.ChatPrefixes) > 0 </span><span class="cov1" title="1">{ - tmp := App{ChatSuffix: fc.Chat.ChatSuffix, ChatPrefixes: fc.Chat.ChatPrefixes} - out.mergeBasics(&tmp) - }</span> - - // provider - <span class="cov3" title="4">if strings.TrimSpace(fc.Provider.Name) != "" </span><span class="cov2" title="3">{ - tmp := App{Provider: fc.Provider.Name} - out.mergeBasics(&tmp) - }</span> - - // openai - <span class="cov3" title="4">if (fc.OpenAI != sectionOpenAI{}) || fc.OpenAI.Temperature != nil </span><span class="cov2" title="3">{ - tmp := App{ - OpenAIBaseURL: fc.OpenAI.BaseURL, - OpenAIModel: fc.OpenAI.Model, - OpenAITemperature: fc.OpenAI.Temperature, - } - out.mergeProviderFields(&tmp) - }</span> - - // copilot - <span class="cov3" title="4">if (fc.Copilot != sectionCopilot{}) || fc.Copilot.Temperature != nil </span><span class="cov2" title="3">{ - tmp := App{ - CopilotBaseURL: fc.Copilot.BaseURL, - CopilotModel: fc.Copilot.Model, - CopilotTemperature: fc.Copilot.Temperature, - } - out.mergeProviderFields(&tmp) - }</span> - - // ollama - <span class="cov3" title="4">if (fc.Ollama != sectionOllama{}) || fc.Ollama.Temperature != nil </span><span class="cov2" title="3">{ - tmp := App{ - OllamaBaseURL: fc.Ollama.BaseURL, - OllamaModel: fc.Ollama.Model, - OllamaTemperature: fc.Ollama.Temperature, - } - out.mergeProviderFields(&tmp) - }</span> - - // prompts - // completion - <span class="cov3" title="4">if (fc.Prompts.Completion != sectionPromptsCompletion{}) </span><span class="cov1" title="1">{ - if strings.TrimSpace(fc.Prompts.Completion.SystemGeneral) != "" </span><span class="cov1" title="1">{ - out.PromptCompletionSystemGeneral = fc.Prompts.Completion.SystemGeneral - }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.Completion.SystemParams) != "" </span><span class="cov1" title="1">{ - out.PromptCompletionSystemParams = fc.Prompts.Completion.SystemParams - }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.Completion.SystemInline) != "" </span><span class="cov1" title="1">{ - out.PromptCompletionSystemInline = fc.Prompts.Completion.SystemInline - }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.Completion.UserGeneral) != "" </span><span class="cov1" title="1">{ - out.PromptCompletionUserGeneral = fc.Prompts.Completion.UserGeneral - }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.Completion.UserParams) != "" </span><span class="cov1" title="1">{ - out.PromptCompletionUserParams = fc.Prompts.Completion.UserParams - }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.Completion.ExtraHeader) != "" </span><span class="cov1" title="1">{ - out.PromptCompletionExtraHeader = fc.Prompts.Completion.ExtraHeader + out := App{} + + // Merge section: general + if (fc.General != sectionGeneral{}) || fc.General.CodingTemperature != nil </span><span class="cov2" title="3">{ + tmp := App{ + MaxTokens: fc.General.MaxTokens, + ContextMode: fc.General.ContextMode, + ContextWindowLines: fc.General.ContextWindowLines, + MaxContextTokens: fc.General.MaxContextTokens, + CodingTemperature: fc.General.CodingTemperature, + } + out.mergeBasics(&tmp) }</span> - } - // chat - <span class="cov3" title="4">if strings.TrimSpace(fc.Prompts.Chat.System) != "" </span><span class="cov1" title="1">{ - out.PromptChatSystem = fc.Prompts.Chat.System - }</span> - // code action - <span class="cov3" title="4">if (fc.Prompts.CodeAction != sectionPromptsCodeAction{}) </span><span class="cov1" title="1">{ - if strings.TrimSpace(fc.Prompts.CodeAction.RewriteSystem) != "" </span><span class="cov1" title="1">{ - out.PromptCodeActionRewriteSystem = fc.Prompts.CodeAction.RewriteSystem + + // logging + <span class="cov3" title="4">if (fc.Logging != sectionLogging{}) </span><span class="cov1" title="1">{ + tmp := App{LogPreviewLimit: fc.Logging.LogPreviewLimit} + out.mergeBasics(&tmp) }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsSystem) != "" </span><span class="cov1" title="1">{ - out.PromptCodeActionDiagnosticsSystem = fc.Prompts.CodeAction.DiagnosticsSystem + + // completion + <span class="cov3" title="4">if (fc.Completion != sectionCompletion{}) </span><span class="cov2" title="3">{ + tmp := App{ + CompletionDebounceMs: fc.Completion.CompletionDebounceMs, + CompletionThrottleMs: fc.Completion.CompletionThrottleMs, + ManualInvokeMinPrefix: fc.Completion.ManualInvokeMinPrefix, + } + out.mergeBasics(&tmp) }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.DocumentSystem) != "" </span><span class="cov1" title="1">{ - out.PromptCodeActionDocumentSystem = fc.Prompts.CodeAction.DocumentSystem + + // triggers + <span class="cov3" title="4">if len(fc.Triggers.TriggerCharacters) > 0 </span><span class="cov2" title="3">{ + tmp := App{TriggerCharacters: fc.Triggers.TriggerCharacters} + out.mergeBasics(&tmp) }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.RewriteUser) != "" </span><span class="cov1" title="1">{ - out.PromptCodeActionRewriteUser = fc.Prompts.CodeAction.RewriteUser + + // inline + <span class="cov3" title="4">if (fc.Inline != sectionInline{}) </span><span class="cov1" title="1">{ + tmp := App{InlineOpen: fc.Inline.InlineOpen, InlineClose: fc.Inline.InlineClose} + out.mergeBasics(&tmp) }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsUser) != "" </span><span class="cov1" title="1">{ - out.PromptCodeActionDiagnosticsUser = fc.Prompts.CodeAction.DiagnosticsUser + + // chat + <span class="cov3" title="4">if strings.TrimSpace(fc.Chat.ChatSuffix) != "" || len(fc.Chat.ChatPrefixes) > 0 </span><span class="cov1" title="1">{ + tmp := App{ChatSuffix: fc.Chat.ChatSuffix, ChatPrefixes: fc.Chat.ChatPrefixes} + out.mergeBasics(&tmp) }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.DocumentUser) != "" </span><span class="cov1" title="1">{ - out.PromptCodeActionDocumentUser = fc.Prompts.CodeAction.DocumentUser + + // provider + <span class="cov3" title="4">if strings.TrimSpace(fc.Provider.Name) != "" </span><span class="cov2" title="3">{ + tmp := App{Provider: fc.Provider.Name} + out.mergeBasics(&tmp) }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.GoTestSystem) != "" </span><span class="cov1" title="1">{ - out.PromptCodeActionGoTestSystem = fc.Prompts.CodeAction.GoTestSystem + + // openai + <span class="cov3" title="4">if (fc.OpenAI != sectionOpenAI{}) || fc.OpenAI.Temperature != nil </span><span class="cov2" title="3">{ + tmp := App{ + OpenAIBaseURL: fc.OpenAI.BaseURL, + OpenAIModel: fc.OpenAI.Model, + OpenAITemperature: fc.OpenAI.Temperature, + } + out.mergeProviderFields(&tmp) }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.GoTestUser) != "" </span><span class="cov1" title="1">{ - out.PromptCodeActionGoTestUser = fc.Prompts.CodeAction.GoTestUser + + // copilot + <span class="cov3" title="4">if (fc.Copilot != sectionCopilot{}) || fc.Copilot.Temperature != nil </span><span class="cov2" title="3">{ + tmp := App{ + CopilotBaseURL: fc.Copilot.BaseURL, + CopilotModel: fc.Copilot.Model, + CopilotTemperature: fc.Copilot.Temperature, + } + out.mergeProviderFields(&tmp) }</span> - } - // cli - <span class="cov3" title="4">if (fc.Prompts.CLI != sectionPromptsCLI{}) </span><span class="cov1" title="1">{ - if strings.TrimSpace(fc.Prompts.CLI.DefaultSystem) != "" </span><span class="cov1" title="1">{ - out.PromptCLIDefaultSystem = fc.Prompts.CLI.DefaultSystem + + // ollama + <span class="cov3" title="4">if (fc.Ollama != sectionOllama{}) || fc.Ollama.Temperature != nil </span><span class="cov2" title="3">{ + tmp := App{ + OllamaBaseURL: fc.Ollama.BaseURL, + OllamaModel: fc.Ollama.Model, + OllamaTemperature: fc.Ollama.Temperature, + } + out.mergeProviderFields(&tmp) }</span> - <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CLI.ExplainSystem) != "" </span><span class="cov1" title="1">{ - out.PromptCLIExplainSystem = fc.Prompts.CLI.ExplainSystem + + // prompts + // completion + <span class="cov3" title="4">if (fc.Prompts.Completion != sectionPromptsCompletion{}) </span><span class="cov1" title="1">{ + if strings.TrimSpace(fc.Prompts.Completion.SystemGeneral) != "" </span><span class="cov1" title="1">{ + out.PromptCompletionSystemGeneral = fc.Prompts.Completion.SystemGeneral + }</span> + <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.Completion.SystemParams) != "" </span><span class="cov1" title="1">{ + out.PromptCompletionSystemParams = fc.Prompts.Completion.SystemParams + }</span> + <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.Completion.SystemInline) != "" </span><span class="cov1" title="1">{ + out.PromptCompletionSystemInline = fc.Prompts.Completion.SystemInline + }</span> + <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.Completion.UserGeneral) != "" </span><span class="cov1" title="1">{ + out.PromptCompletionUserGeneral = fc.Prompts.Completion.UserGeneral + }</span> + <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.Completion.UserParams) != "" </span><span class="cov1" title="1">{ + out.PromptCompletionUserParams = fc.Prompts.Completion.UserParams + }</span> + <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.Completion.ExtraHeader) != "" </span><span class="cov1" title="1">{ + out.PromptCompletionExtraHeader = fc.Prompts.Completion.ExtraHeader + }</span> + } + // chat + <span class="cov3" title="4">if strings.TrimSpace(fc.Prompts.Chat.System) != "" </span><span class="cov1" title="1">{ + out.PromptChatSystem = fc.Prompts.Chat.System + }</span> + // code action + <span class="cov3" title="4">if (fc.Prompts.CodeAction != sectionPromptsCodeAction{}) </span><span class="cov1" title="1">{ + if strings.TrimSpace(fc.Prompts.CodeAction.RewriteSystem) != "" </span><span class="cov1" title="1">{ + out.PromptCodeActionRewriteSystem = fc.Prompts.CodeAction.RewriteSystem + }</span> + <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsSystem) != "" </span><span class="cov1" title="1">{ + out.PromptCodeActionDiagnosticsSystem = fc.Prompts.CodeAction.DiagnosticsSystem + }</span> + <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.DocumentSystem) != "" </span><span class="cov1" title="1">{ + out.PromptCodeActionDocumentSystem = fc.Prompts.CodeAction.DocumentSystem + }</span> + <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.RewriteUser) != "" </span><span class="cov1" title="1">{ + out.PromptCodeActionRewriteUser = fc.Prompts.CodeAction.RewriteUser + }</span> + <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsUser) != "" </span><span class="cov1" title="1">{ + out.PromptCodeActionDiagnosticsUser = fc.Prompts.CodeAction.DiagnosticsUser + }</span> + <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.DocumentUser) != "" </span><span class="cov1" title="1">{ + out.PromptCodeActionDocumentUser = fc.Prompts.CodeAction.DocumentUser + }</span> + <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.GoTestSystem) != "" </span><span class="cov1" title="1">{ + out.PromptCodeActionGoTestSystem = fc.Prompts.CodeAction.GoTestSystem + }</span> + <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CodeAction.GoTestUser) != "" </span><span class="cov1" title="1">{ + out.PromptCodeActionGoTestUser = fc.Prompts.CodeAction.GoTestUser + }</span> + } + // cli + <span class="cov3" title="4">if (fc.Prompts.CLI != sectionPromptsCLI{}) </span><span class="cov1" title="1">{ + if strings.TrimSpace(fc.Prompts.CLI.DefaultSystem) != "" </span><span class="cov1" title="1">{ + out.PromptCLIDefaultSystem = fc.Prompts.CLI.DefaultSystem + }</span> + <span class="cov1" title="1">if strings.TrimSpace(fc.Prompts.CLI.ExplainSystem) != "" </span><span class="cov1" title="1">{ + out.PromptCLIExplainSystem = fc.Prompts.CLI.ExplainSystem + }</span> + } + // provider-native + <span class="cov3" title="4">if strings.TrimSpace(fc.Prompts.ProviderNative.Completion) != "" </span><span class="cov1" title="1">{ + out.PromptNativeCompletion = fc.Prompts.ProviderNative.Completion }</span> - } - // provider-native - <span class="cov3" title="4">if strings.TrimSpace(fc.Prompts.ProviderNative.Completion) != "" </span><span class="cov1" title="1">{ - out.PromptNativeCompletion = fc.Prompts.ProviderNative.Completion - }</span> - <span class="cov3" title="4">return out</span> + <span class="cov3" title="4">return out</span> } -func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov4" title="8">{ - b, err := os.ReadFile(path) - if err != nil </span><span class="cov2" title="2">{ - 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="cov2" title="2">return nil, err</span> - } - - <span class="cov4" 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 - _ = toml.Unmarshal(b, &raw) - if errTables != nil </span><span class="cov2" title="2">{ - if logger != nil </span><span class="cov2" title="2">{ - logger.Printf("invalid TOML config file %s: %v", path, errTables) - }</span> - <span class="cov2" title="2">return nil, errTables</span> - } +func loadFromFile(path string, logger *log.Logger) (*App, error) <span class="cov4" title="9">{ + b, err := os.ReadFile(path) + if err != nil </span><span class="cov2" title="3">{ + 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="cov2" title="3">return nil, err</span> + } - // Reject legacy flat keys at top-level (sectioned-only config is allowed) - <span class="cov3" title="4">legacy := map[string]struct{}{ - "max_tokens": {}, "context_mode": {}, "context_window_lines": {}, "max_context_tokens": {}, - "log_preview_limit": {}, "completion_debounce_ms": {}, "completion_throttle_ms": {}, - "manual_invoke_min_prefix": {}, "trigger_characters": {}, "inline_open": {}, "inline_close": {}, - "chat_suffix": {}, "chat_prefixes": {}, "coding_temperature": {}, "provider": {}, - "openai_model": {}, "openai_base_url": {}, "openai_temperature": {}, - "ollama_model": {}, "ollama_base_url": {}, "ollama_temperature": {}, - "copilot_model": {}, "copilot_base_url": {}, "copilot_temperature": {}, - } - for k := range raw </span><span class="cov6" title="27">{ - if _, isTable := map[string]struct{}{"general": {}, "logging": {}, "completion": {}, "triggers": {}, "inline": {}, "chat": {}, "provider": {}, "openai": {}, "copilot": {}, "ollama": {}, "prompts": {}}[k]; isTable </span><span class="cov6" title="27">{ - continue</span> + <span class="cov4" 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 + _ = toml.Unmarshal(b, &raw) + if errTables != nil </span><span class="cov2" title="2">{ + if logger != nil </span><span class="cov2" title="2">{ + logger.Printf("invalid TOML config file %s: %v", path, errTables) + }</span> + <span class="cov2" title="2">return nil, errTables</span> } - <span class="cov0" title="0">if _, isLegacy := legacy[k]; isLegacy </span><span class="cov0" title="0">{ - return nil, fmt.Errorf("unsupported flat key '%s' in config; use sectioned tables (see config.toml.example)", k) - }</span> - } - <span class="cov3" title="4">if logger != nil </span><span class="cov3" title="4">{ - logger.Printf("loaded configuration from %s (TOML)", path) - }</span> + // Reject legacy flat keys at top-level (sectioned-only config is allowed) + <span class="cov3" title="4">legacy := map[string]struct{}{ + "max_tokens": {}, "context_mode": {}, "context_window_lines": {}, "max_context_tokens": {}, + "log_preview_limit": {}, "completion_debounce_ms": {}, "completion_throttle_ms": {}, + "manual_invoke_min_prefix": {}, "trigger_characters": {}, "inline_open": {}, "inline_close": {}, + "chat_suffix": {}, "chat_prefixes": {}, "coding_temperature": {}, "provider": {}, + "openai_model": {}, "openai_base_url": {}, "openai_temperature": {}, + "ollama_model": {}, "ollama_base_url": {}, "ollama_temperature": {}, + "copilot_model": {}, "copilot_base_url": {}, "copilot_temperature": {}, + } + for k := range raw </span><span class="cov6" title="27">{ + if _, isTable := map[string]struct{}{"general": {}, "logging": {}, "completion": {}, "triggers": {}, "inline": {}, "chat": {}, "provider": {}, "openai": {}, "copilot": {}, "ollama": {}, "prompts": {}}[k]; isTable </span><span class="cov6" title="27">{ + continue</span> + } + <span class="cov0" title="0">if _, isLegacy := legacy[k]; isLegacy </span><span class="cov0" title="0">{ + return nil, fmt.Errorf("unsupported flat key '%s' in config; use sectioned tables (see config.toml.example)", k) + }</span> + } - // Merge order: flat first, then tables (so tables win over zero flat values) - // Build App from tables only - <span class="cov3" title="4">tab := tables.toApp() - // Ensure explicit values from raw map are respected (defensive for ints) - if t, ok := raw["completion"].(map[string]any); ok </span><span class="cov2" title="3">{ - if v, present := t["manual_invoke_min_prefix"]; present </span><span class="cov2" title="3">{ - switch vv := v.(type) </span>{ - case int64:<span class="cov2" title="3"> - tab.ManualInvokeMinPrefix = int(vv)</span> - case int:<span class="cov0" title="0"> - tab.ManualInvokeMinPrefix = vv</span> - case float64:<span class="cov0" title="0"> - tab.ManualInvokeMinPrefix = int(vv)</span> - } + <span class="cov3" title="4">if logger != nil </span><span class="cov3" title="4">{ + logger.Printf("loaded configuration from %s (TOML)", path) + }</span> + + // Merge order: flat first, then tables (so tables win over zero flat values) + // Build App from tables only + <span class="cov3" title="4">tab := tables.toApp() + // Ensure explicit values from raw map are respected (defensive for ints) + if t, ok := raw["completion"].(map[string]any); ok </span><span class="cov2" title="3">{ + if v, present := t["manual_invoke_min_prefix"]; present </span><span class="cov2" title="3">{ + switch vv := v.(type) </span>{ + case int64:<span class="cov2" title="3"> + tab.ManualInvokeMinPrefix = int(vv)</span> + case int:<span class="cov0" title="0"> + tab.ManualInvokeMinPrefix = vv</span> + case float64:<span class="cov0" title="0"> + tab.ManualInvokeMinPrefix = int(vv)</span> + } + } } - } - <span class="cov3" title="4">if t, ok := raw["logging"].(map[string]any); ok </span><span class="cov2" title="3">{ - if v, present := t["log_preview_limit"]; present </span><span class="cov2" title="3">{ - switch vv := v.(type) </span>{ - case int64:<span class="cov2" title="3"> - tab.LogPreviewLimit = int(vv)</span> - case int:<span class="cov0" title="0"> - tab.LogPreviewLimit = vv</span> - case float64:<span class="cov0" title="0"> - tab.LogPreviewLimit = int(vv)</span> - } + <span class="cov3" title="4">if t, ok := raw["logging"].(map[string]any); ok </span><span class="cov2" title="3">{ + if v, present := t["log_preview_limit"]; present </span><span class="cov2" title="3">{ + switch vv := v.(type) </span>{ + case int64:<span class="cov2" title="3"> + tab.LogPreviewLimit = int(vv)</span> + case int:<span class="cov0" title="0"> + tab.LogPreviewLimit = vv</span> + case float64:<span class="cov0" title="0"> + tab.LogPreviewLimit = int(vv)</span> + } + } } - } - <span class="cov3" title="4">return &tab, nil</span> + <span class="cov3" title="4">return &tab, nil</span> } func (a *App) mergeWith(other *App) <span class="cov3" title="5">{ - a.mergeBasics(other) - a.mergeProviderFields(other) - a.mergePrompts(other) + a.mergeBasics(other) + a.mergeProviderFields(other) + a.mergePrompts(other) }</span> // mergeBasics merges general (non-provider) fields. @@ -742,65 +776,65 @@ func (a *App) mergeBasics(other *App) <span class="cov6" title="20">{ // mergePrompts copies non-empty prompt templates from other. func (a *App) mergePrompts(other *App) <span class="cov3" title="5">{ - // Completion - if strings.TrimSpace(other.PromptCompletionSystemGeneral) != "" </span><span class="cov1" title="1">{ - a.PromptCompletionSystemGeneral = other.PromptCompletionSystemGeneral - }</span> - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCompletionSystemParams) != "" </span><span class="cov1" title="1">{ - a.PromptCompletionSystemParams = other.PromptCompletionSystemParams - }</span> - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCompletionSystemInline) != "" </span><span class="cov1" title="1">{ - a.PromptCompletionSystemInline = other.PromptCompletionSystemInline - }</span> - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCompletionUserGeneral) != "" </span><span class="cov1" title="1">{ - a.PromptCompletionUserGeneral = other.PromptCompletionUserGeneral - }</span> - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCompletionUserParams) != "" </span><span class="cov1" title="1">{ - a.PromptCompletionUserParams = other.PromptCompletionUserParams - }</span> - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCompletionExtraHeader) != "" </span><span class="cov1" title="1">{ - a.PromptCompletionExtraHeader = other.PromptCompletionExtraHeader - }</span> - // Provider-native - <span class="cov3" title="5">if strings.TrimSpace(other.PromptNativeCompletion) != "" </span><span class="cov1" title="1">{ - a.PromptNativeCompletion = other.PromptNativeCompletion - }</span> - // Chat - <span class="cov3" title="5">if strings.TrimSpace(other.PromptChatSystem) != "" </span><span class="cov1" title="1">{ - a.PromptChatSystem = other.PromptChatSystem - }</span> - // Code actions - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionRewriteSystem) != "" </span><span class="cov1" title="1">{ - a.PromptCodeActionRewriteSystem = other.PromptCodeActionRewriteSystem - }</span> - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionDiagnosticsSystem) != "" </span><span class="cov1" title="1">{ - a.PromptCodeActionDiagnosticsSystem = other.PromptCodeActionDiagnosticsSystem - }</span> - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionDocumentSystem) != "" </span><span class="cov1" title="1">{ - a.PromptCodeActionDocumentSystem = other.PromptCodeActionDocumentSystem - }</span> - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionRewriteUser) != "" </span><span class="cov1" title="1">{ - a.PromptCodeActionRewriteUser = other.PromptCodeActionRewriteUser - }</span> - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionDiagnosticsUser) != "" </span><span class="cov1" title="1">{ - a.PromptCodeActionDiagnosticsUser = other.PromptCodeActionDiagnosticsUser - }</span> - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionDocumentUser) != "" </span><span class="cov1" title="1">{ - a.PromptCodeActionDocumentUser = other.PromptCodeActionDocumentUser - }</span> - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionGoTestSystem) != "" </span><span class="cov1" title="1">{ - a.PromptCodeActionGoTestSystem = other.PromptCodeActionGoTestSystem - }</span> - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionGoTestUser) != "" </span><span class="cov1" title="1">{ - a.PromptCodeActionGoTestUser = other.PromptCodeActionGoTestUser - }</span> - // CLI - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCLIDefaultSystem) != "" </span><span class="cov1" title="1">{ - a.PromptCLIDefaultSystem = other.PromptCLIDefaultSystem - }</span> - <span class="cov3" title="5">if strings.TrimSpace(other.PromptCLIExplainSystem) != "" </span><span class="cov1" title="1">{ - a.PromptCLIExplainSystem = other.PromptCLIExplainSystem - }</span> + // Completion + if strings.TrimSpace(other.PromptCompletionSystemGeneral) != "" </span><span class="cov1" title="1">{ + a.PromptCompletionSystemGeneral = other.PromptCompletionSystemGeneral + }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCompletionSystemParams) != "" </span><span class="cov1" title="1">{ + a.PromptCompletionSystemParams = other.PromptCompletionSystemParams + }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCompletionSystemInline) != "" </span><span class="cov1" title="1">{ + a.PromptCompletionSystemInline = other.PromptCompletionSystemInline + }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCompletionUserGeneral) != "" </span><span class="cov1" title="1">{ + a.PromptCompletionUserGeneral = other.PromptCompletionUserGeneral + }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCompletionUserParams) != "" </span><span class="cov1" title="1">{ + a.PromptCompletionUserParams = other.PromptCompletionUserParams + }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCompletionExtraHeader) != "" </span><span class="cov1" title="1">{ + a.PromptCompletionExtraHeader = other.PromptCompletionExtraHeader + }</span> + // Provider-native + <span class="cov3" title="5">if strings.TrimSpace(other.PromptNativeCompletion) != "" </span><span class="cov1" title="1">{ + a.PromptNativeCompletion = other.PromptNativeCompletion + }</span> + // Chat + <span class="cov3" title="5">if strings.TrimSpace(other.PromptChatSystem) != "" </span><span class="cov1" title="1">{ + a.PromptChatSystem = other.PromptChatSystem + }</span> + // Code actions + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionRewriteSystem) != "" </span><span class="cov1" title="1">{ + a.PromptCodeActionRewriteSystem = other.PromptCodeActionRewriteSystem + }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionDiagnosticsSystem) != "" </span><span class="cov1" title="1">{ + a.PromptCodeActionDiagnosticsSystem = other.PromptCodeActionDiagnosticsSystem + }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionDocumentSystem) != "" </span><span class="cov1" title="1">{ + a.PromptCodeActionDocumentSystem = other.PromptCodeActionDocumentSystem + }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionRewriteUser) != "" </span><span class="cov1" title="1">{ + a.PromptCodeActionRewriteUser = other.PromptCodeActionRewriteUser + }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionDiagnosticsUser) != "" </span><span class="cov1" title="1">{ + a.PromptCodeActionDiagnosticsUser = other.PromptCodeActionDiagnosticsUser + }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionDocumentUser) != "" </span><span class="cov1" title="1">{ + a.PromptCodeActionDocumentUser = other.PromptCodeActionDocumentUser + }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionGoTestSystem) != "" </span><span class="cov1" title="1">{ + a.PromptCodeActionGoTestSystem = other.PromptCodeActionGoTestSystem + }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCodeActionGoTestUser) != "" </span><span class="cov1" title="1">{ + a.PromptCodeActionGoTestUser = other.PromptCodeActionGoTestUser + }</span> + // CLI + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCLIDefaultSystem) != "" </span><span class="cov1" title="1">{ + a.PromptCLIDefaultSystem = other.PromptCLIDefaultSystem + }</span> + <span class="cov3" title="5">if strings.TrimSpace(other.PromptCLIExplainSystem) != "" </span><span class="cov1" title="1">{ + a.PromptCLIExplainSystem = other.PromptCLIExplainSystem + }</span> } // mergeProviderFields merges per-provider configuration. @@ -834,33 +868,33 @@ func (a *App) mergeProviderFields(other *App) <span class="cov5" title="14">{ }</span> } -func getConfigPath() (string, error) <span class="cov4" title="8">{ +func getConfigPath() (string, error) <span class="cov4" title="9">{ 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="cov1" title="1"> { + }</span> else<span class="cov2" title="2"> { 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="cov1" title="1">configPath = filepath.Join(home, ".config", "hexai", "config.toml")</span> + <span class="cov2" title="2">configPath = filepath.Join(home, ".config", "hexai", "config.toml")</span> } - <span class="cov4" title="8">return configPath, nil</span> + <span class="cov4" title="9">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="7">{ +func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="8">{ var out App var any bool // helpers - getenv := func(k string) string </span><span class="cov10" title="168">{ return strings.TrimSpace(os.Getenv(k)) }</span> - <span class="cov4" title="7">parseInt := func(k string) (int, bool) </span><span class="cov7" title="49">{ + getenv := func(k string) string </span><span class="cov10" title="192">{ return strings.TrimSpace(os.Getenv(k)) }</span> + <span class="cov4" title="8">parseInt := func(k string) (int, bool) </span><span class="cov7" title="56">{ v := getenv(k) - if v == "" </span><span class="cov7" title="42">{ + if v == "" </span><span class="cov7" title="49">{ return 0, false }</span> <span class="cov4" title="7">n, err := strconv.Atoi(v) @@ -872,9 +906,9 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="7">{ } <span class="cov4" title="7">return n, true</span> } - <span class="cov4" title="7">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov6" title="28">{ + <span class="cov4" title="8">parseFloatPtr := func(k string) (*float64, bool) </span><span class="cov6" title="32">{ v := getenv(k) - if v == "" </span><span class="cov6" title="24">{ + if v == "" </span><span class="cov6" title="28">{ return nil, false }</span> <span class="cov3" title="4">f, err := strconv.ParseFloat(v, 64) @@ -887,43 +921,43 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="7">{ <span class="cov3" title="4">return &f, true</span> } - <span class="cov4" title="7">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="8">if n, ok := parseInt("HEXAI_MAX_TOKENS"); ok </span><span class="cov1" title="1">{ out.MaxTokens = n any = true }</span> - <span class="cov4" title="7">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="8">if s := getenv("HEXAI_CONTEXT_MODE"); s != "" </span><span class="cov1" title="1">{ out.ContextMode = s any = true }</span> - <span class="cov4" title="7">if n, ok := parseInt("HEXAI_CONTEXT_WINDOW_LINES"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="8">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="7">if n, ok := parseInt("HEXAI_MAX_CONTEXT_TOKENS"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="8">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="7">if n, ok := parseInt("HEXAI_LOG_PREVIEW_LIMIT"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="8">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="7">if n, ok := parseInt("HEXAI_MANUAL_INVOKE_MIN_PREFIX"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="8">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="7">if n, ok := parseInt("HEXAI_COMPLETION_DEBOUNCE_MS"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="8">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="7">if n, ok := parseInt("HEXAI_COMPLETION_THROTTLE_MS"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="8">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="7">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="8">if f, ok := parseFloatPtr("HEXAI_CODING_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.CodingTemperature = f any = true }</span> - <span class="cov4" title="7">if s := getenv("HEXAI_TRIGGER_CHARACTERS"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="8">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">{ @@ -933,19 +967,19 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="7">{ } <span class="cov1" title="1">any = true</span> } - <span class="cov4" title="7">if s := getenv("HEXAI_INLINE_OPEN"); s != "" </span><span class="cov0" title="0">{ + <span class="cov4" title="8">if s := getenv("HEXAI_INLINE_OPEN"); s != "" </span><span class="cov0" title="0">{ out.InlineOpen = s any = true }</span> - <span class="cov4" title="7">if s := getenv("HEXAI_INLINE_CLOSE"); s != "" </span><span class="cov0" title="0">{ + <span class="cov4" title="8">if s := getenv("HEXAI_INLINE_CLOSE"); s != "" </span><span class="cov0" title="0">{ out.InlineClose = s any = true }</span> - <span class="cov4" title="7">if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" </span><span class="cov0" title="0">{ + <span class="cov4" title="8">if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" </span><span class="cov0" title="0">{ out.ChatSuffix = s any = true }</span> - <span class="cov4" title="7">if s := getenv("HEXAI_CHAT_PREFIXES"); s != "" </span><span class="cov0" title="0">{ + <span class="cov4" title="8">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">{ @@ -955,75 +989,473 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="7">{ } <span class="cov0" title="0">any = true</span> } - <span class="cov4" title="7">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="8">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov1" title="1">{ out.Provider = s any = true }</span> // Provider-specific - <span class="cov4" title="7">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="8">if s := getenv("HEXAI_OPENAI_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OpenAIBaseURL = s any = true }</span> - <span class="cov4" title="7">if s := getenv("HEXAI_OPENAI_MODEL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="8">if s := getenv("HEXAI_OPENAI_MODEL"); s != "" </span><span class="cov1" title="1">{ out.OpenAIModel = s any = true }</span> - <span class="cov4" title="7">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="8">if f, ok := parseFloatPtr("HEXAI_OPENAI_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.OpenAITemperature = f any = true }</span> - <span class="cov4" title="7">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="8">if s := getenv("HEXAI_OLLAMA_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.OllamaBaseURL = s any = true }</span> - <span class="cov4" title="7">if s := getenv("HEXAI_OLLAMA_MODEL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="8">if s := getenv("HEXAI_OLLAMA_MODEL"); s != "" </span><span class="cov1" title="1">{ out.OllamaModel = s any = true }</span> - <span class="cov4" title="7">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="8">if f, ok := parseFloatPtr("HEXAI_OLLAMA_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.OllamaTemperature = f any = true }</span> - <span class="cov4" title="7">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="8">if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" </span><span class="cov1" title="1">{ out.CopilotBaseURL = s any = true }</span> - <span class="cov4" title="7">if s := getenv("HEXAI_COPILOT_MODEL"); s != "" </span><span class="cov1" title="1">{ + <span class="cov4" title="8">if s := getenv("HEXAI_COPILOT_MODEL"); s != "" </span><span class="cov1" title="1">{ out.CopilotModel = s any = true }</span> - <span class="cov4" title="7">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{ + <span class="cov4" title="8">if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok </span><span class="cov1" title="1">{ out.CopilotTemperature = f any = true }</span> - <span class="cov4" title="7">if !any </span><span class="cov4" title="6">{ + <span class="cov4" title="8">if !any </span><span class="cov4" title="7">{ return nil }</span> <span class="cov1" title="1">return &out</span> } </pre> - <pre class="file" id="file3" style="display: none">// Summary: Hexai CLI runner; reads input, creates an LLM client, builds messages, + <pre class="file" id="file4" style="display: none">package hexaiaction + +import ( + "bufio" + "io" + "strings" + + "codeberg.org/snonux/hexai/internal/textutil" +) + +// ParseInput splits raw stdin into optional diagnostics and selection/code. +// Format: +// +// Diagnostics:\n +// <one per line>\n +// <blank line> (optional)\n +// <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">{ + 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)) + if raw == "" </span><span class="cov0" title="0">{ + return InputParts{Selection: ""}, nil + }</span> + <span class="cov4" title="2">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">{ + 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">{ + 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">{ + t := strings.TrimSpace(lines[i]) + if t == "" </span><span class="cov1" title="1">{ + i++ + break</span> + } + <span class="cov4" title="2">diags = append(diags, t)</span> + } + <span class="cov1" title="1">sel := strings.Join(lines[i:], "\n") + sel = strings.TrimSpace(sel) + return InputParts{Selection: sel, Diagnostics: diags}, nil</span> +} + +// 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> + +// findFirstInstructionInLine follows the same precedence as LSP: +// - ;text; (strict) +// - /* text */ (single-line) +// - <!-- text --> (single-line) +// - // text +// - # text +// - -- text +// helpers moved to textutil +</pre> + + <pre class="file" id="file5" style="display: none">package hexaiaction + +import ( + "context" + "strings" + "time" + + "codeberg.org/snonux/hexai/internal/appconfig" + "codeberg.org/snonux/hexai/internal/llm" + "codeberg.org/snonux/hexai/internal/textutil" +) + +// Render performs simple {{var}} replacement like LSP. +func Render(t string, vars map[string]string) string <span class="cov10" title="8">{ return textutil.RenderTemplate(t, vars) }</span> + +// StripFences removes surrounding markdown code fences. +func StripFences(s string) string <span class="cov10" title="8">{ 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="cov4" title="2">{ + sys := cfg.PromptCodeActionRewriteSystem + user := Render(cfg.PromptCodeActionRewriteUser, map[string]string{"instruction": instruction, "selection": selection}) + return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) +}</span> + +func runDiagnostics(ctx context.Context, cfg appconfig.App, client chatDoer, diags []string, selection string) (string, error) <span class="cov1" title="1">{ + var b strings.Builder + for i, d := range diags </span><span class="cov4" title="2">{ + if strings.TrimSpace(d) == "" </span><span class="cov0" title="0">{ + continue</span> + } + <span class="cov4" title="2">b.WriteString(strings.TrimSpace(d)) + if i < len(diags)-1 </span><span class="cov1" title="1">{ + b.WriteString("\n") + }</span> + } + <span class="cov1" title="1">sys := cfg.PromptCodeActionDiagnosticsSystem + user := Render(cfg.PromptCodeActionDiagnosticsUser, map[string]string{"diagnostics": b.String(), "selection": selection}) + return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg))</span> +} + +func runDocument(ctx context.Context, cfg appconfig.App, client chatDoer, selection string) (string, error) <span class="cov4" title="2">{ + sys := cfg.PromptCodeActionDocumentSystem + user := Render(cfg.PromptCodeActionDocumentUser, map[string]string{"selection": selection}) + return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) +}</span> + +func runGoTest(ctx context.Context, cfg appconfig.App, client chatDoer, funcCode string) (string, error) <span class="cov4" title="2">{ + sys := cfg.PromptCodeActionGoTestSystem + user := Render(cfg.PromptCodeActionGoTestUser, map[string]string{"function": funcCode}) + return runOnceWithOpts(ctx, client, sys, user, reqOptsFrom(cfg)) +}</span> + +func runOnce(ctx context.Context, client chatDoer, sys, user string) (string, error) <span class="cov0" title="0">{ + msgs := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} + txt, err := client.Chat(ctx, msgs) + if err != nil </span><span class="cov0" title="0">{ + return "", err + }</span> + <span class="cov0" title="0">return strings.TrimSpace(StripFences(txt)), nil</span> +} + +func runOnceWithOpts(ctx context.Context, client chatDoer, sys, user string, opts []llm.RequestOption) (string, error) <span class="cov9" title="7">{ + 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="cov9" title="7">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="cov9" title="7">{ + opts := []llm.RequestOption{llm.WithMaxTokens(cfg.MaxTokens)} + if cfg.CodingTemperature != nil </span><span class="cov5" title="3">{ + opts = append(opts, llm.WithTemperature(*cfg.CodingTemperature)) + }</span> + <span class="cov9" title="7">return opts</span> +} + +// Timeout helpers to mirror LSP behavior. +func timeout10s(parent context.Context) (context.Context, context.CancelFunc) <span class="cov4" title="2">{ + return context.WithTimeout(parent, 10*time.Second) +}</span> + +func timeout8s(parent context.Context) (context.Context, context.CancelFunc) <span class="cov1" title="1">{ + return context.WithTimeout(parent, 8*time.Second) +}</span> +</pre> + + <pre class="file" id="file6" style="display: none">package hexaiaction + +import ( + "context" + "fmt" + "io" + "log" + "strings" + + "codeberg.org/snonux/hexai/internal/appconfig" + "codeberg.org/snonux/hexai/internal/logging" + "codeberg.org/snonux/hexai/internal/llmutils" +) + +// Run executes the hexai-action command flow. +func Run(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error <span class="cov0" title="0">{ + 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="cov0" title="0">{ + 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) + 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">{ + 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) + if err != nil </span><span class="cov0" title="0">{ + return err + }</span> + <span class="cov0" title="0">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">{ + switch kind </span>{ + case ActionSkip:<span class="cov1" title="1"> + return parts.Selection, nil</span> + case ActionRewrite:<span class="cov1" title="1"> + 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) + defer cancel() + return runRewrite(cctx, cfg, client, instr, cleaned)</span> + case ActionDiagnostics:<span class="cov0" title="0"> + cctx, cancel := timeout10s(ctx) + defer cancel() + return runDiagnostics(cctx, cfg, client, parts.Diagnostics, parts.Selection)</span> + case ActionDocument:<span class="cov1" title="1"> + cctx, cancel := timeout10s(ctx) + defer cancel() + return runDocument(cctx, cfg, client, parts.Selection)</span> + case ActionGoTest:<span class="cov1" title="1"> + cctx, cancel := timeout8s(ctx) + defer cancel() + return runGoTest(cctx, cfg, client, parts.Selection)</span> + default:<span class="cov0" title="0"> + return parts.Selection, nil</span> + } +} + +// client construction is shared via internal/llmutils +</pre> + + <pre class="file" id="file7" style="display: none">package hexaiaction + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" +) + +// item implements list.Item +type item struct { + title, desc string + kind ActionKind + hotkey rune +} + +func (i item) Title() string <span class="cov0" title="0">{ return i.title }</span> +func (i item) Description() string <span class="cov0" title="0">{ return i.desc }</span> +func (i item) FilterValue() string <span class="cov1" title="1">{ return i.title }</span> + +type model struct { + list list.Model + chosen ActionKind + done bool +} + +func newModel() model <span class="cov10" title="3">{ + items := []list.Item{ + item{title: "Rewrite selection", desc: "", kind: ActionRewrite, hotkey: 'r'}, + item{title: "Document code", desc: "", kind: ActionDocument, hotkey: 'c'}, + item{title: "Generate Go unit test(s)", desc: "", kind: ActionGoTest, hotkey: 't'}, + item{title: "Skip", desc: "", kind: ActionSkip, hotkey: 's'}, + } + l := list.New(items, oneLineDelegate{}, 0, 0) + l.Title = "Select Hexai Action" + l.SetShowHelp(false) + l.SetShowStatusBar(false) + l.SetFilteringEnabled(false) + return model{list: l} +}</span> + +func (m model) Init() tea.Cmd <span class="cov0" title="0">{ return nil }</span> + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) <span class="cov0" title="0">{ + switch msg := msg.(type) </span>{ + case tea.KeyMsg:<span class="cov0" title="0"> + return handleKey(m, msg)</span> + case tea.WindowSizeMsg:<span class="cov0" title="0"> + m.list.SetSize(msg.Width, msg.Height)</span> + } + <span class="cov0" title="0">var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd</span> +} + +func handleKey(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) <span class="cov10" title="3">{ + raw := msg.String() + low := strings.ToLower(raw) + switch low </span>{ + case "esc", "q":<span class="cov1" title="1"> + // Treat ESC and q as Skip/quit + m.chosen = ActionSkip + m.done = true + return m, tea.Quit</span> + case "enter":<span class="cov0" title="0"> + if it, ok := m.list.SelectedItem().(item); ok </span><span class="cov0" title="0">{ + m.chosen = it.kind + m.done = true + return m, tea.Quit + }</span> + case "j", "down":<span class="cov0" title="0"> + m.list.CursorDown()</span> + case "k", "up":<span class="cov0" title="0"> + m.list.CursorUp()</span> + case "g", "home":<span class="cov1" title="1"> + m.list.Select(0)</span> + case "end":<span class="cov0" title="0"> + if n := len(m.list.Items()); n > 0 </span><span class="cov0" title="0">{ m.list.Select(n - 1) }</span> + case "s", "r", "c", "t":<span class="cov1" title="1"> + items := m.list.Items() + for i := 0; i < len(items); i++ </span><span class="cov1" title="1">{ + if it, ok := items[i].(item); ok && strings.ToLower(string(it.hotkey)) == low </span><span class="cov1" title="1">{ + m.list.Select(i) + m.chosen = it.kind + m.done = true + return m, tea.Quit + }</span> + } + } + <span class="cov1" title="1">if raw == "G" </span><span class="cov1" title="1">{ // Shift+G jumps to end + if n := len(m.list.Items()); n > 0 </span><span class="cov1" title="1">{ m.list.Select(n - 1) }</span> + } + <span class="cov1" title="1">return m, nil</span> +} + +func (m model) View() string <span class="cov0" title="0">{ + if m.done </span><span class="cov0" title="0">{ + return "" + }</span> + <span class="cov0" title="0">return m.list.View()</span> +} + +// RunTUI returns the chosen ActionKind. +func RunTUI() (ActionKind, error) <span class="cov0" title="0">{ + p := tea.NewProgram(newModel()) + md, err := p.Run() + if err != nil </span><span class="cov0" title="0">{ + return ActionSkip, err + }</span> + <span class="cov0" title="0">if m, ok := md.(model); ok </span><span class="cov0" title="0">{ + if m.chosen == "" </span><span class="cov0" title="0">{ + return ActionSkip, nil + }</span> + <span class="cov0" title="0">return m.chosen, nil</span> + } + <span class="cov0" title="0">return ActionSkip, fmt.Errorf("unexpected model type")</span> +} +</pre> + + <pre class="file" id="file8" style="display: none">package hexaiaction + +import ( + "fmt" + "io" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// oneLineDelegate renders a single compact line per item, no spacing. +type oneLineDelegate struct{} + +var ( + hotStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205")) + cursorStyle = lipgloss.NewStyle().Bold(true) +) + +func (oneLineDelegate) Height() int <span class="cov8" title="10">{ return 1 }</span> +func (oneLineDelegate) Spacing() int <span class="cov10" title="16">{ return 0 }</span> +func (oneLineDelegate) Update(tea.Msg, *list.Model) tea.Cmd <span class="cov0" title="0">{ return nil }</span> +func (oneLineDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) <span class="cov1" title="1">{ + title := listItem.FilterValue() + hk := '?' + if it, ok := listItem.(item); ok </span><span class="cov1" title="1">{ + hk = it.hotkey + }</span> + <span class="cov1" title="1">hot := hotStyle.Render(fmt.Sprintf(" (%c)", hk)) + cursor := " " + if index == m.Index() </span><span class="cov1" title="1">{ + cursor = cursorStyle.Render("> ") + }</span> + <span class="cov1" title="1">fmt.Fprintf(w, "%s%s%s", cursor, title, hot)</span> +} +</pre> + + <pre class="file" id="file9" style="display: none">// Summary: Hexai CLI runner; reads input, creates an LLM client, builds messages, // streams or collects the model output, and prints a short summary to stderr. package hexaicli import ( - "bufio" - "context" - "fmt" - "io" - "log" - "os" - "strings" - "time" + "bufio" + "context" + "fmt" + "io" + "log" + "os" + "strings" + "time" - "codeberg.org/snonux/hexai/internal/appconfig" - "codeberg.org/snonux/hexai/internal/llm" - "codeberg.org/snonux/hexai/internal/logging" + "codeberg.org/snonux/hexai/internal/appconfig" + "codeberg.org/snonux/hexai/internal/logging" + "codeberg.org/snonux/hexai/internal/llm" + "codeberg.org/snonux/hexai/internal/llmutils" ) // Run executes the Hexai CLI behavior given arguments and I/O streams. @@ -1032,24 +1464,24 @@ func Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io. // Load configuration with a logger so file-based config is respected. logger := log.New(stderr, "hexai ", log.LstdFlags|log.Lmsgprefix) cfg := appconfig.Load(logger) - client, err := newClientFromConfig(cfg) - if err != nil </span><span class="cov1" title="1">{ - fmt.Fprintf(stderr, logging.AnsiBase+"hexai: LLM disabled: %v"+logging.AnsiReset+"\n", err) - return err - }</span> - // Inline the flow here to use configured CLI prompts. - <span class="cov0" title="0">input, rerr := readInput(stdin, args) - if rerr != nil </span><span class="cov0" title="0">{ - fmt.Fprintln(stderr, logging.AnsiBase+rerr.Error()+logging.AnsiReset) - return rerr - }</span> - <span class="cov0" title="0">printProviderInfo(stderr, client) - msgs := buildMessagesFromConfig(cfg, input) - if err := runChat(ctx, client, msgs, input, stdout, stderr); err != nil </span><span class="cov0" title="0">{ - fmt.Fprintf(stderr, logging.AnsiBase+"hexai: error: %v"+logging.AnsiReset+"\n", err) - return err - }</span> - <span class="cov0" title="0">return nil</span> + client, err := llmutils.NewClientFromApp(cfg) + if err != nil </span><span class="cov1" title="1">{ + fmt.Fprintf(stderr, logging.AnsiBase+"hexai: LLM disabled: %v"+logging.AnsiReset+"\n", err) + return err + }</span> + // Inline the flow here to use configured CLI prompts. + <span class="cov0" title="0">input, rerr := readInput(stdin, args) + if rerr != nil </span><span class="cov0" title="0">{ + fmt.Fprintln(stderr, logging.AnsiBase+rerr.Error()+logging.AnsiReset) + return rerr + }</span> + <span class="cov0" title="0">printProviderInfo(stderr, client) + msgs := buildMessagesFromConfig(cfg, input) + if err := runChat(ctx, client, msgs, input, stdout, stderr); err != nil </span><span class="cov0" title="0">{ + fmt.Fprintf(stderr, logging.AnsiBase+"hexai: error: %v"+logging.AnsiReset+"\n", err) + return err + }</span> + <span class="cov0" title="0">return nil</span> } // RunWithClient executes the CLI flow using an already-constructed client. @@ -1061,7 +1493,7 @@ func RunWithClient(ctx context.Context, args []string, stdin io.Reader, stdout, return err }</span> <span class="cov1" title="1">printProviderInfo(stderr, client) - msgs := buildMessages(input) + msgs := buildMessages(input) if err := runChat(ctx, client, msgs, input, stdout, stderr); err != nil </span><span class="cov1" title="1">{ fmt.Fprintf(stderr, logging.AnsiBase+"hexai: error: %v"+logging.AnsiReset+"\n", err) return err @@ -1090,31 +1522,7 @@ func readInput(stdin io.Reader, args []string) (string, error) <span class="cov9 } // newClientFromConfig builds an LLM client from the app config and env keys. -func newClientFromConfig(cfg appconfig.App) (llm.Client, error) <span class="cov6" title="3">{ - 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) == "" </span><span class="cov6" title="3">{ - oaKey = os.Getenv("OPENAI_API_KEY") - }</span> - // Prefer HEXAI_COPILOT_API_KEY; fall back to COPILOT_API_KEY - <span class="cov6" title="3">cpKey := os.Getenv("HEXAI_COPILOT_API_KEY") - if strings.TrimSpace(cpKey) == "" </span><span class="cov6" title="3">{ - cpKey = os.Getenv("COPILOT_API_KEY") - }</span> - <span class="cov6" title="3">return llm.NewFromConfig(llmCfg, oaKey, cpKey)</span> -} +// client construction moved to internal/llmutils // buildMessages creates system and user messages based on input content. func buildMessages(input string) []llm.Message <span class="cov10" title="6">{ @@ -1130,33 +1538,33 @@ func buildMessages(input string) []llm.Message <span class="cov10" title="6">{ } // buildMessagesFromConfig uses configured CLI system prompts. -func buildMessagesFromConfig(cfg appconfig.App, input string) []llm.Message <span class="cov0" title="0">{ - lower := strings.ToLower(input) - system := cfg.PromptCLIDefaultSystem - if strings.Contains(lower, "explain") </span><span class="cov0" title="0">{ - if strings.TrimSpace(cfg.PromptCLIExplainSystem) != "" </span><span class="cov0" title="0">{ - system = cfg.PromptCLIExplainSystem +func buildMessagesFromConfig(cfg appconfig.App, input string) []llm.Message <span class="cov4" title="2">{ + lower := strings.ToLower(input) + system := cfg.PromptCLIDefaultSystem + if strings.Contains(lower, "explain") </span><span class="cov1" title="1">{ + if strings.TrimSpace(cfg.PromptCLIExplainSystem) != "" </span><span class="cov1" title="1">{ + system = cfg.PromptCLIExplainSystem + }</span> + } + <span class="cov4" title="2">return []llm.Message{ + {Role: "system", Content: system}, + {Role: "user", Content: input}, }</span> - } - <span class="cov0" title="0">return []llm.Message{ - {Role: "system", Content: system}, - {Role: "user", Content: input}, - }</span> } // runChat executes the chat request, handling streaming and summary output. -func runChat(ctx context.Context, client llm.Client, msgs []llm.Message, input string, out io.Writer, errw io.Writer) error <span class="cov7" title="4">{ +func runChat(ctx context.Context, client llm.Client, msgs []llm.Message, input string, out io.Writer, errw io.Writer) error <span class="cov9" title="5">{ start := time.Now() var output string - if s, ok := client.(llm.Streamer); ok </span><span class="cov1" title="1">{ + if s, ok := client.(llm.Streamer); ok </span><span class="cov4" title="2">{ var b strings.Builder - if err := s.ChatStream(ctx, msgs, func(chunk string) </span><span class="cov6" title="3">{ + if err := s.ChatStream(ctx, msgs, func(chunk string) </span><span class="cov9" title="5">{ b.WriteString(chunk) fmt.Fprint(out, chunk) }</span>); err != nil <span class="cov0" title="0">{ return err }</span> - <span class="cov1" title="1">output = b.String()</span> + <span class="cov4" title="2">output = b.String()</span> } else<span class="cov6" title="3"> { txt, err := client.Chat(ctx, msgs) if err != nil </span><span class="cov4" title="2">{ @@ -1165,7 +1573,7 @@ func runChat(ctx context.Context, client llm.Client, msgs []llm.Message, input s <span class="cov1" title="1">output = txt fmt.Fprint(out, output)</span> } - <span class="cov4" title="2">dur := time.Since(start) + <span class="cov6" title="3">dur := time.Since(start) fmt.Fprintf(errw, "\n"+logging.AnsiBase+"done provider=%s model=%s time=%s in_bytes=%d out_bytes=%d"+logging.AnsiReset+"\n", client.Name(), client.DefaultModel(), dur.Round(time.Millisecond), len(input), len(output)) return nil</span> @@ -1173,11 +1581,16 @@ func runChat(ctx context.Context, client llm.Client, msgs []llm.Message, input s // printProviderInfo writes the provider/model line to stderr. func printProviderInfo(errw io.Writer, client llm.Client) <span class="cov4" title="2">{ - fmt.Fprintf(errw, logging.AnsiBase+"provider=%s model=%s"+logging.AnsiReset+"\n", client.Name(), client.DefaultModel()) + fmt.Fprintf(errw, logging.AnsiBase+"provider=%s model=%s"+logging.AnsiReset+"\n", client.Name(), client.DefaultModel()) +}</span> + +// newClientFromConfig is kept for tests; delegates to llmutils. +func newClientFromConfig(cfg appconfig.App) (llm.Client, error) <span class="cov4" title="2">{ + return llmutils.NewClientFromApp(cfg) }</span> </pre> - <pre class="file" id="file4" style="display: none">// Summary: Hexai LSP runner; configures logging, loads config, builds the LLM client, + <pre class="file" id="file10" style="display: none">// 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 @@ -1218,7 +1631,7 @@ func Run(logPath string, stdin io.Reader, stdout io.Writer, stderr io.Writer) er // 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 <span class="cov10" title="6">{ +func RunWithFactory(logPath string, stdin io.Reader, stdout io.Writer, logger *log.Logger, cfg appconfig.App, client llm.Client, factory ServerFactory) error <span class="cov10" title="7">{ normalizeLoggingConfig(&cfg) client = buildClientIfNil(cfg, client) factory = ensureFactory(factory) @@ -1228,23 +1641,23 @@ func RunWithFactory(logPath string, stdin io.Reader, stdout io.Writer, logger *l if err := server.Run(); err != nil </span><span class="cov0" title="0">{ logger.Fatalf("server error: %v", err) }</span> - <span class="cov10" title="6">return nil</span> + <span class="cov10" title="7">return nil</span> } // --- helpers to keep RunWithFactory small --- -func normalizeLoggingConfig(cfg *appconfig.App) <span class="cov10" title="6">{ +func normalizeLoggingConfig(cfg *appconfig.App) <span class="cov10" title="7">{ cfg.ContextMode = strings.ToLower(strings.TrimSpace(cfg.ContextMode)) - if cfg.LogPreviewLimit >= 0 </span><span class="cov10" title="6">{ + if cfg.LogPreviewLimit >= 0 </span><span class="cov10" title="7">{ logging.SetLogPreviewLimit(cfg.LogPreviewLimit) }</span> } -func buildClientIfNil(cfg appconfig.App, client llm.Client) llm.Client <span class="cov10" title="6">{ +func buildClientIfNil(cfg appconfig.App, client llm.Client) llm.Client <span class="cov10" title="7">{ if client != nil </span><span class="cov0" title="0">{ return client }</span> - <span class="cov10" title="6">llmCfg := llm.Config{ + <span class="cov10" title="7">llmCfg := llm.Config{ Provider: cfg.Provider, OpenAIBaseURL: cfg.OpenAIBaseURL, OpenAIModel: cfg.OpenAIModel, @@ -1258,25 +1671,25 @@ func buildClientIfNil(cfg appconfig.App, client llm.Client) llm.Client <span cla } // Prefer HEXAI_OPENAI_API_KEY; fall back to OPENAI_API_KEY oaKey := os.Getenv("HEXAI_OPENAI_API_KEY") - if strings.TrimSpace(oaKey) == "" </span><span class="cov10" title="6">{ + if strings.TrimSpace(oaKey) == "" </span><span class="cov10" title="7">{ oaKey = os.Getenv("OPENAI_API_KEY") }</span> // Prefer HEXAI_COPILOT_API_KEY; fall back to COPILOT_API_KEY - <span class="cov10" title="6">cpKey := os.Getenv("HEXAI_COPILOT_API_KEY") - if strings.TrimSpace(cpKey) == "" </span><span class="cov10" title="6">{ + <span class="cov10" title="7">cpKey := os.Getenv("HEXAI_COPILOT_API_KEY") + if strings.TrimSpace(cpKey) == "" </span><span class="cov10" title="7">{ cpKey = os.Getenv("COPILOT_API_KEY") }</span> - <span class="cov10" title="6">if c, err := llm.NewFromConfig(llmCfg, oaKey, cpKey); err != nil </span><span class="cov1" title="1">{ + <span class="cov10" title="7">if c, err := llm.NewFromConfig(llmCfg, oaKey, cpKey); err != nil </span><span class="cov1" title="1">{ logging.Logf("lsp ", "llm disabled: %v", err) return nil - }</span> else<span class="cov9" title="5"> { + }</span> else<span class="cov9" title="6"> { logging.Logf("lsp ", "llm enabled provider=%s model=%s", c.Name(), c.DefaultModel()) return c }</span> } -func ensureFactory(factory ServerFactory) ServerFactory <span class="cov10" title="6">{ - if factory != nil </span><span class="cov9" title="5">{ +func ensureFactory(factory ServerFactory) ServerFactory <span class="cov10" title="7">{ + if factory != nil </span><span class="cov9" title="6">{ return factory }</span> <span class="cov1" title="1">return func(r io.Reader, w io.Writer, logger *log.Logger, opts lsp.ServerOptions) ServerRunner </span><span class="cov1" title="1">{ @@ -1284,46 +1697,46 @@ func ensureFactory(factory ServerFactory) ServerFactory <span class="cov10" titl }</span> } -func makeServerOptions(cfg appconfig.App, logContext bool, client llm.Client) lsp.ServerOptions <span class="cov10" title="6">{ - 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, - } +func makeServerOptions(cfg appconfig.App, logContext bool, client llm.Client) lsp.ServerOptions <span class="cov10" title="7">{ + 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, + } }</span> </pre> - <pre class="file" id="file5" style="display: none">// Summary: GitHub Copilot client for chat and Codex-style code completion. + <pre class="file" id="file11" style="display: none">// Summary: GitHub Copilot client for chat and Codex-style code completion. package llm import ( @@ -1388,16 +1801,16 @@ type copilotChatResponse struct { } // Constructor (kept among the first functions by convention) -func newCopilot(baseURL, model, apiKey string, defaultTemp *float64) Client <span class="cov3" title="8">{ - if strings.TrimSpace(baseURL) == "" </span><span class="cov0" title="0">{ +func newCopilot(baseURL, model, apiKey string, defaultTemp *float64) Client <span class="cov3" title="9">{ + if strings.TrimSpace(baseURL) == "" </span><span class="cov1" title="1">{ baseURL = "https://api.githubcopilot.com" }</span> - <span class="cov3" title="8">if strings.TrimSpace(model) == "" </span><span class="cov0" title="0">{ + <span class="cov3" title="9">if strings.TrimSpace(model) == "" </span><span class="cov0" title="0">{ // GitHub Models (Copilot API) commonly supports gpt-4o/gpt-4o-mini. // Default to a broadly available, cost-effective option. model = "gpt-4o-mini" }</span> - <span class="cov3" title="8">return copilotClient{ + <span class="cov3" title="9">return copilotClient{ httpClient: &http.Client{Timeout: 30 * time.Second}, apiKey: apiKey, baseURL: strings.TrimRight(baseURL, "/"), @@ -1718,7 +2131,7 @@ func (c copilotClient) CodeCompletion(ctx context.Context, prompt string, suffix // (no streaming decoder needed; we parse whole body lines) </pre> - <pre class="file" id="file6" style="display: none">// Summary: Ollama client against a local server; supports chat responses and streaming via /api/chat. + <pre class="file" id="file12" style="display: none">// Summary: Ollama client against a local server; supports chat responses and streaming via /api/chat. package llm import ( @@ -1761,14 +2174,14 @@ type ollamaChatResponse struct { } // Constructor (kept among the first functions by convention) -func newOllama(baseURL, model string, defaultTemp *float64) Client <span class="cov9" title="11">{ - if strings.TrimSpace(baseURL) == "" </span><span class="cov0" title="0">{ +func newOllama(baseURL, model string, defaultTemp *float64) Client <span class="cov10" title="13">{ + if strings.TrimSpace(baseURL) == "" </span><span class="cov3" title="2">{ baseURL = "http://localhost:11434" }</span> - <span class="cov9" title="11">if strings.TrimSpace(model) == "" </span><span class="cov0" title="0">{ + <span class="cov10" title="13">if strings.TrimSpace(model) == "" </span><span class="cov3" title="2">{ model = "qwen3-coder:30b-a3b-q4_K_M`" }</span> - <span class="cov9" title="11">return ollamaClient{ + <span class="cov10" title="13">return ollamaClient{ httpClient: &http.Client{Timeout: 30 * time.Second}, baseURL: strings.TrimRight(baseURL, "/"), defaultModel: model, @@ -1821,8 +2234,8 @@ func (c ollamaClient) Chat(ctx context.Context, messages []Message, opts ...Requ } // Provider metadata -func (c ollamaClient) Name() string <span class="cov1" title="1">{ return "ollama" }</span> -func (c ollamaClient) DefaultModel() string <span class="cov1" title="1">{ return c.defaultModel }</span> +func (c ollamaClient) Name() string <span class="cov3" title="2">{ return "ollama" }</span> +func (c ollamaClient) DefaultModel() string <span class="cov3" title="2">{ return c.defaultModel }</span> // Streaming support (optional) func (c ollamaClient) ChatStream(ctx context.Context, messages []Message, onDelta func(string), opts ...RequestOption) error <span class="cov4" title="3">{ @@ -1855,7 +2268,7 @@ func (c ollamaClient) ChatStream(ctx context.Context, messages []Message, onDelt }</span> <span class="cov4" title="3">dec := json.NewDecoder(resp.Body) - for </span><span class="cov6" title="4">{ + for </span><span class="cov5" title="4">{ var ev ollamaChatResponse if err := dec.Decode(&ev); err != nil </span><span class="cov1" title="1">{ if errors.Is(err, io.EOF) </span><span class="cov0" title="0">{ @@ -1888,28 +2301,28 @@ func (c ollamaClient) logStart(stream bool, o Options, messages []Message) <span <span class="cov8" title="9">c.chatLogger.LogStart(stream, o.Model, o.Temperature, o.MaxTokens, o.Stop, logMessages)</span> } -func buildOllamaRequest(o Options, messages []Message, defaultTemp *float64, stream bool) ollamaChatRequest <span class="cov10" title="12">{ +func buildOllamaRequest(o Options, messages []Message, defaultTemp *float64, stream bool) ollamaChatRequest <span class="cov9" title="12">{ req := ollamaChatRequest{Model: o.Model, Stream: stream} req.Messages = make([]oaMessage, len(messages)) - for i, m := range messages </span><span class="cov10" title="12">{ + for i, m := range messages </span><span class="cov9" title="12">{ req.Messages[i] = oaMessage{Role: m.Role, Content: m.Content} }</span> - <span class="cov10" title="12">optsMap := map[string]any{} + <span class="cov9" title="12">optsMap := map[string]any{} if o.Temperature != 0 </span><span class="cov1" title="1">{ optsMap["temperature"] = o.Temperature }</span> else<span class="cov9" title="11"> if defaultTemp != nil </span><span class="cov4" title="3">{ optsMap["temperature"] = *defaultTemp }</span> - <span class="cov10" title="12">if o.MaxTokens > 0 </span><span class="cov3" title="2">{ + <span class="cov9" title="12">if o.MaxTokens > 0 </span><span class="cov3" title="2">{ optsMap["num_predict"] = o.MaxTokens }</span> - <span class="cov10" title="12">if len(o.Stop) > 0 </span><span class="cov3" title="2">{ + <span class="cov9" title="12">if len(o.Stop) > 0 </span><span class="cov3" title="2">{ optsMap["stop"] = o.Stop }</span> - <span class="cov10" title="12">if len(optsMap) > 0 </span><span class="cov6" title="4">{ + <span class="cov9" title="12">if len(optsMap) > 0 </span><span class="cov5" title="4">{ req.Options = optsMap }</span> - <span class="cov10" title="12">return req</span> + <span class="cov9" title="12">return req</span> } func (c ollamaClient) doJSON(ctx context.Context, url string, body []byte) (*http.Response, error) <span class="cov8" title="9">{ @@ -1922,7 +2335,7 @@ func (c ollamaClient) doJSON(ctx context.Context, url string, body []byte) (*htt } func handleOllamaNon2xx(resp *http.Response, start time.Time) error <span class="cov8" title="9">{ - if resp.StatusCode >= 200 && resp.StatusCode < 300 </span><span class="cov8" title="7">{ + if resp.StatusCode >= 200 && resp.StatusCode < 300 </span><span class="cov7" title="7">{ return nil }</span> <span class="cov3" title="2">var apiErr ollamaChatResponse @@ -1936,7 +2349,7 @@ func handleOllamaNon2xx(resp *http.Response, start time.Time) error <span class= } </pre> - <pre class="file" id="file7" style="display: none">// Summary: OpenAI client implementation for chat completions with optional streaming and detailed logging. + <pre class="file" id="file13" style="display: none">// Summary: OpenAI client implementation for chat completions with optional streaming and detailed logging. package llm import ( @@ -2013,14 +2426,14 @@ type oaStreamChunk struct { // Constructor (kept among the first functions by convention) // newOpenAI constructs an OpenAI client using explicit configuration values. // The apiKey may be empty; calls will fail until a valid key is supplied. -func newOpenAI(baseURL, model, apiKey string, defaultTemp *float64) Client <span class="cov10" title="16">{ - if strings.TrimSpace(baseURL) == "" </span><span class="cov6" title="5">{ +func newOpenAI(baseURL, model, apiKey string, defaultTemp *float64) Client <span class="cov10" title="17">{ + if strings.TrimSpace(baseURL) == "" </span><span class="cov6" title="6">{ baseURL = "https://api.openai.com/v1" }</span> - <span class="cov10" title="16">if strings.TrimSpace(model) == "" </span><span class="cov6" title="5">{ + <span class="cov10" title="17">if strings.TrimSpace(model) == "" </span><span class="cov6" title="6">{ model = "gpt-4.1" }</span> - <span class="cov10" title="16">return openAIClient{ + <span class="cov10" title="17">return openAIClient{ httpClient: &http.Client{Timeout: 30 * time.Second}, apiKey: apiKey, baseURL: baseURL, @@ -2125,12 +2538,12 @@ func (c openAIClient) ChatStream(ctx context.Context, messages []Message, onDelt func (c openAIClient) logf(format string, args ...any) <span class="cov0" title="0">{ logging.Logf("llm/openai ", format, args...) }</span> // helpers extracted to keep methods small -func (c openAIClient) logStart(stream bool, o Options, messages []Message) <span class="cov8" title="9">{ +func (c openAIClient) logStart(stream bool, o Options, messages []Message) <span class="cov7" title="9">{ logMessages := make([]struct{ Role, Content string }, len(messages)) - for i, m := range messages </span><span class="cov8" title="9">{ + for i, m := range messages </span><span class="cov7" title="9">{ logMessages[i] = struct{ Role, Content string }{m.Role, m.Content} }</span> - <span class="cov8" title="9">c.chatLogger.LogStart(stream, o.Model, o.Temperature, o.MaxTokens, o.Stop, logMessages)</span> + <span class="cov7" title="9">c.chatLogger.LogStart(stream, o.Model, o.Temperature, o.MaxTokens, o.Stop, logMessages)</span> } func buildOAChatRequest(o Options, messages []Message, defaultTemp *float64, stream bool) oaChatRequest <span class="cov8" title="11">{ @@ -2239,7 +2652,7 @@ func parseOpenAIStream(resp *http.Response, start time.Time, onDelta func(string } </pre> - <pre class="file" id="file8" style="display: none">// Summary: LLM provider interfaces, request options, configuration, and factory to build a client from config. + <pre class="file" id="file14" style="display: none">// Summary: LLM provider interfaces, request options, configuration, and factory to build a client from config. package llm import ( @@ -2297,8 +2710,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="cov1" title="1">{ return func(o *Options) </span><span class="cov1" title="1">{ o.Temperature = t }</span> } -func WithMaxTokens(n int) RequestOption <span class="cov10" title="25">{ return func(o *Options) </span><span class="cov1" title="1">{ o.MaxTokens = n }</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 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> } @@ -2323,44 +2736,44 @@ type Config struct { // NewFromConfig creates an LLM client using only the supplied configuration. // The OpenAI API key is supplied separately and may be read from the environment // by the caller; other environment-based configuration is not used. -func NewFromConfig(cfg Config, openAIAPIKey, copilotAPIKey string) (Client, error) <span class="cov8" title="14">{ +func NewFromConfig(cfg Config, openAIAPIKey, copilotAPIKey string) (Client, error) <span class="cov8" title="18">{ p := strings.ToLower(strings.TrimSpace(cfg.Provider)) if p == "" </span><span class="cov6" title="7">{ p = "openai" }</span> - <span class="cov8" title="14">switch p </span>{ - case "openai":<span class="cov7" title="10"> + <span class="cov8" title="18">switch p </span>{ + case "openai":<span class="cov7" title="11"> if strings.TrimSpace(openAIAPIKey) == "" </span><span class="cov4" title="4">{ return nil, errors.New("missing OPENAI_API_KEY for provider openai") }</span> // Set coding-friendly default temperature if none provided - <span class="cov6" title="6">if cfg.OpenAITemperature == nil </span><span class="cov4" title="4">{ + <span class="cov6" title="7">if cfg.OpenAITemperature == nil </span><span class="cov5" title="5">{ t := 0.2 cfg.OpenAITemperature = &t }</span> - <span class="cov6" title="6">return newOpenAI(cfg.OpenAIBaseURL, cfg.OpenAIModel, openAIAPIKey, cfg.OpenAITemperature), nil</span> - case "ollama":<span class="cov1" title="1"> - if cfg.OllamaTemperature == nil </span><span class="cov1" title="1">{ + <span class="cov6" title="7">return newOpenAI(cfg.OpenAIBaseURL, cfg.OpenAIModel, openAIAPIKey, cfg.OpenAITemperature), nil</span> + case "ollama":<span class="cov3" title="3"> + if cfg.OllamaTemperature == nil </span><span class="cov2" title="2">{ t := 0.2 cfg.OllamaTemperature = &t }</span> - <span class="cov1" title="1">return newOllama(cfg.OllamaBaseURL, cfg.OllamaModel, cfg.OllamaTemperature), nil</span> - case "copilot":<span class="cov2" title="2"> + <span class="cov3" title="3">return newOllama(cfg.OllamaBaseURL, cfg.OllamaModel, cfg.OllamaTemperature), nil</span> + case "copilot":<span class="cov3" title="3"> if strings.TrimSpace(copilotAPIKey) == "" </span><span class="cov1" title="1">{ return nil, errors.New("missing COPILOT_API_KEY for provider copilot") }</span> - <span class="cov1" title="1">if cfg.CopilotTemperature == nil </span><span class="cov1" title="1">{ + <span class="cov2" title="2">if cfg.CopilotTemperature == nil </span><span class="cov2" title="2">{ t := 0.2 cfg.CopilotTemperature = &t }</span> - <span class="cov1" title="1">return newCopilot(cfg.CopilotBaseURL, cfg.CopilotModel, copilotAPIKey, cfg.CopilotTemperature), nil</span> + <span class="cov2" title="2">return newCopilot(cfg.CopilotBaseURL, cfg.CopilotModel, copilotAPIKey, cfg.CopilotTemperature), nil</span> default:<span class="cov1" title="1"> return nil, errors.New("unknown LLM provider: " + p)</span> } } </pre> - <pre class="file" id="file9" style="display: none">package llm + <pre class="file" id="file15" style="display: none">package llm import "errors" @@ -2368,7 +2781,44 @@ import "errors" func nilStringErr(msg string) (string, error) <span class="cov10" title="2">{ return "", errors.New(msg) }</span> </pre> - <pre class="file" id="file10" style="display: none">package logging + <pre class="file" id="file16" style="display: none">package llmutils + +import ( + "os" + "strings" + + "codeberg.org/snonux/hexai/internal/appconfig" + "codeberg.org/snonux/hexai/internal/llm" +) + +// NewClientFromApp builds an llm.Client using app config and environment keys. +func NewClientFromApp(cfg appconfig.App) (llm.Client, error) <span class="cov10" title="5">{ + 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, + } + oaKey := os.Getenv("HEXAI_OPENAI_API_KEY") + if strings.TrimSpace(oaKey) == "" </span><span class="cov8" title="4">{ + oaKey = os.Getenv("OPENAI_API_KEY") + }</span> + <span class="cov10" title="5">cpKey := os.Getenv("HEXAI_COPILOT_API_KEY") + if strings.TrimSpace(cpKey) == "" </span><span class="cov10" title="5">{ + cpKey = os.Getenv("COPILOT_API_KEY") + }</span> + <span class="cov10" title="5">return llm.NewFromConfig(llmCfg, oaKey, cpKey)</span> +} + +</pre> + + <pre class="file" id="file17" style="display: none">package logging // ChatLogger provides a structured way to log chat interactions. type ChatLogger struct { @@ -2376,7 +2826,7 @@ type ChatLogger struct { } // NewChatLogger creates a new ChatLogger for a given provider. -func NewChatLogger(provider string) ChatLogger <span class="cov10" title="36">{ +func NewChatLogger(provider string) ChatLogger <span class="cov10" title="40">{ return ChatLogger{Provider: provider} }</span> @@ -2392,14 +2842,14 @@ func (cl ChatLogger) LogStart(stream bool, model string, temp float64, maxTokens }</span> <span class="cov8" title="24">Logf("llm/"+cl.Provider+" ", "%s start model=%s temp=%.2f max_tokens=%d stop=%d messages=%d", chatOrStream, model, temp, maxTokens, len(stop), len(messages)) - for i, m := range messages </span><span class="cov9" title="25">{ + for i, m := range messages </span><span class="cov8" title="24">{ Logf("llm/"+cl.Provider+" ", "msg[%d] role=%s size=%d preview=%s%s%s", i, m.Role, len(m.Content), AnsiCyan, PreviewForLog(m.Content), AnsiBase) }</span> } </pre> - <pre class="file" id="file11" style="display: none">// Summary: ANSI-styled logging utilities with a bound standard logger and configurable preview truncation. + <pre class="file" id="file18" style="display: none">// Summary: ANSI-styled logging utilities with a bound standard logger and configurable preview truncation. package logging import ( @@ -2425,14 +2875,14 @@ const AnsiBase = AnsiBgBlack + AnsiGrey var std *log.Logger // Bind sets the underlying standard logger to use for Logf. -func Bind(l *log.Logger) <span class="cov3" title="4">{ std = l }</span> +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">{ - if std == nil </span><span class="cov9" title="102">{ + if std == nil </span><span class="cov9" title="103">{ return }</span> - <span class="cov7" title="41">msg := fmt.Sprintf(format, args...) + <span class="cov7" title="40">msg := fmt.Sprintf(format, args...) std.Print(AnsiBase + prefix + msg + AnsiReset)</span> } @@ -2441,21 +2891,21 @@ var logPreviewLimit int // 0 means unlimited // SetLogPreviewLimit sets the maximum number of characters to log for // request/response previews. Set to 0 for unlimited. -func SetLogPreviewLimit(n int) <span class="cov5" title="10">{ logPreviewLimit = n }</span> +func SetLogPreviewLimit(n int) <span class="cov4" title="9">{ logPreviewLimit = n }</span> // PreviewForLog returns the string truncated to the configured preview limit. -func PreviewForLog(s string) string <span class="cov7" title="35">{ - if logPreviewLimit > 0 </span><span class="cov3" title="5">{ - if len(s) <= logPreviewLimit </span><span class="cov2" title="2">{ +func PreviewForLog(s string) string <span class="cov7" title="32">{ + if logPreviewLimit > 0 </span><span class="cov2" title="3">{ + if len(s) <= logPreviewLimit </span><span class="cov0" title="0">{ return s }</span> <span class="cov2" title="3">return s[:logPreviewLimit] + "…"</span> } - <span class="cov7" title="30">return s</span> + <span class="cov7" title="29">return s</span> } </pre> - <pre class="file" id="file12" style="display: none">// Summary: Builds additional context snippets based on configured mode and truncates text by token heuristic. + <pre class="file" id="file19" style="display: none">// Summary: Builds additional context snippets based on configured mode and truncates text by token heuristic. package lsp import ( @@ -2541,7 +2991,7 @@ func truncateToApproxTokens(text string, maxTokens int) string <span class="cov8 } </pre> - <pre class="file" id="file13" style="display: none">// Summary: In-memory document model for the LSP; tracks text, lines, and applies edits. + <pre class="file" id="file20" style="display: none">// Summary: In-memory document model for the LSP; tracks text, lines, and applies edits. package lsp import ( @@ -2567,20 +3017,20 @@ func (s *Server) deleteDocument(uri string) <span class="cov1" title="1">{ delete(s.docs, uri) }</span> -func (s *Server) markActivity() <span class="cov3" title="4">{ +func (s *Server) markActivity() <span class="cov4" title="4">{ s.mu.Lock() s.lastInput = time.Now() s.mu.Unlock() }</span> -func (s *Server) getDocument(uri string) *document <span class="cov9" title="51">{ +func (s *Server) getDocument(uri string) *document <span class="cov10" title="52">{ s.mu.RLock() defer s.mu.RUnlock() return s.docs[uri] }</span> // splitLines splits the input string into lines, normalizing line endings to '\n'. -func splitLines(sx string) []string <span class="cov10" title="76">{ +func splitLines(sx string) []string <span class="cov9" title="42">{ sx = strings.ReplaceAll(sx, "\r\n", "\n") return strings.Split(sx, "\n") }</span> @@ -2590,28 +3040,28 @@ func (s *Server) lineContext(uri string, pos Position) (above, current, below, f if d == nil || len(d.lines) == 0 </span><span class="cov1" title="1">{ return "", "", "", "" }</span> - <span class="cov3" title="4">idx := pos.Line + <span class="cov4" title="4">idx := pos.Line if idx < 0 </span><span class="cov0" title="0">{ idx = 0 }</span> - <span class="cov3" title="4">if idx >= len(d.lines) </span><span class="cov0" title="0">{ + <span class="cov4" title="4">if idx >= len(d.lines) </span><span class="cov0" title="0">{ idx = len(d.lines) - 1 }</span> - <span class="cov3" title="4">current = d.lines[idx] - if idx-1 >= 0 </span><span class="cov3" title="4">{ + <span class="cov4" title="4">current = d.lines[idx] + if idx-1 >= 0 </span><span class="cov4" title="4">{ above = d.lines[idx-1] }</span> - <span class="cov3" title="4">if idx+1 < len(d.lines) </span><span class="cov3" title="4">{ + <span class="cov4" title="4">if idx+1 < len(d.lines) </span><span class="cov4" title="4">{ below = d.lines[idx+1] }</span> - <span class="cov3" title="4">for i := idx; i >= 0; i-- </span><span class="cov4" title="6">{ + <span class="cov4" title="4">for i := idx; i >= 0; i-- </span><span class="cov5" title="6">{ line := strings.TrimSpace(d.lines[i]) - if hasAny(line, []string{"func ", "def ", "class ", "fn ", "procedure ", "sub "}) </span><span class="cov3" title="4">{ + if hasAny(line, []string{"func ", "def ", "class ", "fn ", "procedure ", "sub "}) </span><span class="cov4" title="4">{ funcCtx = line break</span> } } - <span class="cov3" title="4">return above, current, below, funcCtx</span> + <span class="cov4" title="4">return above, current, below, funcCtx</span> } // isDefiningNewFunction returns true when the cursor appears to be within @@ -2646,7 +3096,7 @@ func (s *Server) isDefiningNewFunction(uri string, pos Position) bool <span clas return false }</span> // Scan for '{' from sigStart up to cursor position; if found before or at cursor, we're in body - <span class="cov3" title="3">for i := sigStart; i <= idx; i++ </span><span class="cov3" title="4">{ + <span class="cov3" title="3">for i := sigStart; i <= idx; i++ </span><span class="cov4" title="4">{ line := d.lines[i] brace := strings.Index(line, "{") if brace >= 0 </span><span class="cov2" title="2">{ @@ -2662,39 +3112,39 @@ func (s *Server) isDefiningNewFunction(uri string, pos Position) bool <span clas <span class="cov1" title="1">return true</span> } -func hasAny(s string, needles []string) bool <span class="cov4" title="6">{ - for _, n := range needles </span><span class="cov6" title="16">{ - if strings.Contains(s, n) </span><span class="cov3" title="4">{ +func hasAny(s string, needles []string) bool <span class="cov5" title="6">{ + for _, n := range needles </span><span class="cov7" title="16">{ + if strings.Contains(s, n) </span><span class="cov4" title="4">{ return true }</span> } <span class="cov2" title="2">return false</span> } -func trimLen(s string) string <span class="cov8" title="40">{ +func trimLen(s string) string <span class="cov9" title="42">{ s = strings.TrimSpace(s) if len(s) > 200 </span><span class="cov1" title="1">{ return s[:200] + "…" }</span> - <span class="cov8" title="39">return s</span> + <span class="cov9" title="41">return s</span> } -func firstLine(s string) string <span class="cov7" title="22">{ +func firstLine(s string) string <span class="cov8" title="25">{ s = strings.ReplaceAll(s, "\r\n", "\n") - if idx := strings.IndexByte(s, '\n'); idx >= 0 </span><span class="cov4" title="5">{ + if idx := strings.IndexByte(s, '\n'); idx >= 0 </span><span class="cov5" title="6">{ return s[:idx] }</span> - <span class="cov6" title="17">return s</span> + <span class="cov7" title="19">return s</span> } </pre> - <pre class="file" id="file14" style="display: none">// Summary: LSP JSON-RPC handlers; implements core methods and integrates with the LLM client when enabled. + <pre class="file" id="file21" style="display: none">// Summary: LSP JSON-RPC handlers; implements core methods and integrates with the LLM client when enabled. package lsp import ( - "encoding/json" - "fmt" - "strings" + "encoding/json" + "fmt" + "strings" ) func (s *Server) handle(req Request) <span class="cov2" title="2">{ @@ -2716,14 +3166,14 @@ func (s *Server) handle(req Request) <span class="cov2" title="2">{ // a line comment (//, #, --). Returns the instruction string and the selection // text cleaned of the matched instruction marker or comment. func instructionFromSelection(sel string) (string, string) <span class="cov3" title="3">{ - lines := splitLines(sel) - for idx, line := range lines </span><span class="cov3" title="3">{ - if instr, cleaned, ok := findFirstInstructionInLine(line); ok && strings.TrimSpace(instr) != "" </span><span class="cov1" title="1">{ - lines[idx] = cleaned - return instr, strings.Join(lines, "\n") - }</span> - } - <span class="cov2" title="2">return "", sel</span> + lines := splitLines(sel) + for idx, line := range lines </span><span class="cov3" title="3">{ + if instr, cleaned, ok := findFirstInstructionInLine(line); ok && strings.TrimSpace(instr) != "" </span><span class="cov1" title="1">{ + lines[idx] = cleaned + return instr, strings.Join(lines, "\n") + }</span> + } + <span class="cov2" title="2">return "", sel</span> } // findFirstInstructionInLine returns the earliest instruction marker on the @@ -2736,51 +3186,51 @@ func instructionFromSelection(sel string) (string, string) <span class="cov3" ti // - # text // - -- text func findFirstInstructionInLine(line string) (instr string, cleaned string, ok bool) <span class="cov8" title="22">{ - type cand struct { - start, end int - text string - } - cands := []cand{} - if t, l, r, ok := findStrictInlineTag(line); ok </span><span class="cov5" title="6">{ - cands = append(cands, cand{start: l, end: r, text: t}) - }</span> - <span class="cov8" title="22">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 - text := strings.TrimSpace(line[i+2 : i+2+j]) - cands = append(cands, cand{start: start, end: end, text: text}) - }</span> - } - <span class="cov8" title="22">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 - text := strings.TrimSpace(line[i+4 : i+4+j]) - cands = append(cands, cand{start: start, end: end, text: text}) - }</span> - } - <span class="cov8" title="22">if i := strings.Index(line, "//"); i >= 0 </span><span class="cov4" title="4">{ - cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])}) - }</span> - <span class="cov8" title="22">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:])}) + type cand struct { + start, end int + text string + } + cands := []cand{} + if t, l, r, ok := findStrictInlineTag(line); ok </span><span class="cov5" title="6">{ + cands = append(cands, cand{start: l, end: r, text: t}) + }</span> + <span class="cov8" title="22">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 + text := strings.TrimSpace(line[i+2 : i+2+j]) + cands = append(cands, cand{start: start, end: end, text: text}) }</span> - <span class="cov8" title="22">if i := strings.Index(line, "--"); i >= 0 </span><span class="cov4" title="4">{ - cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])}) + } + <span class="cov8" title="22">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 + text := strings.TrimSpace(line[i+4 : i+4+j]) + cands = append(cands, cand{start: start, end: end, text: text}) }</span> - <span class="cov8" title="22">if len(cands) == 0 </span><span class="cov5" title="6">{ - return "", line, false + } + <span class="cov8" title="22">if i := strings.Index(line, "//"); i >= 0 </span><span class="cov4" title="4">{ + cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])}) + }</span> + <span class="cov8" title="22">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="cov8" title="22">if i := strings.Index(line, "--"); i >= 0 </span><span class="cov4" title="4">{ + cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])}) + }</span> + <span class="cov8" title="22">if len(cands) == 0 </span><span class="cov5" title="6">{ + return "", line, false + }</span> + // pick earliest start index + <span class="cov7" title="16">best := cands[0] + for _, c := range cands[1:] </span><span class="cov4" title="4">{ + if c.start >= 0 && (best.start < 0 || c.start < best.start) </span><span class="cov1" title="1">{ + best = c }</span> - // pick earliest start index - <span class="cov7" title="16">best := cands[0] - for _, c := range cands[1:] </span><span class="cov4" title="4">{ - if c.start >= 0 && (best.start < 0 || c.start < best.start) </span><span class="cov1" title="1">{ - best = c - }</span> - } - <span class="cov7" title="16">cleaned = strings.TrimRight(line[:best.start]+line[best.end:], " \t") - return best.text, cleaned, true</span> + } + <span class="cov7" title="16">cleaned = strings.TrimRight(line[:best.start]+line[best.end:], " \t") + return best.text, cleaned, true</span> } // diagnosticsInRange parses the CodeAction context and returns diagnostics @@ -3135,7 +3585,7 @@ func (s *Server) fallbackCompletionItems(docStr string) []CompletionItem <span c }</span> </pre> - <pre class="file" id="file15" style="display: none">// Summary: Code Action handlers and helpers split from handlers.go for clarity. + <pre class="file" id="file22" style="display: none">// Summary: Code Action handlers and helpers split from handlers.go for clarity. package lsp import ( @@ -3202,7 +3652,7 @@ func (s *Server) buildRewriteCodeAction(p CodeActionParams, sel string) *CodeAct <span class="cov2" title="2">return nil</span> } -func (s *Server) buildDiagnosticsCodeAction(p CodeActionParams, sel string) *CodeAction <span class="cov4" title="4">{ +func (s *Server) buildDiagnosticsCodeAction(p CodeActionParams, sel string) *CodeAction <span class="cov3" title="4">{ diags := s.diagnosticsInRange(p.Context, p.Range) if len(diags) == 0 </span><span class="cov2" title="2">{ return nil @@ -3219,11 +3669,11 @@ func (s *Server) buildDiagnosticsCodeAction(p CodeActionParams, sel string) *Cod return &ca</span> } -func (s *Server) resolveCodeAction(ca CodeAction) (CodeAction, bool) <span class="cov6" title="12">{ +func (s *Server) resolveCodeAction(ca CodeAction) (CodeAction, bool) <span class="cov5" title="12">{ if s.llmClient == nil || len(ca.Data) == 0 </span><span class="cov0" title="0">{ return ca, false }</span> - <span class="cov6" title="12">var payload struct { + <span class="cov5" title="12">var payload struct { Type string `json:"type"` URI string `json:"uri"` Range Range `json:"range"` @@ -3234,16 +3684,16 @@ func (s *Server) resolveCodeAction(ca CodeAction) (CodeAction, bool) <span class if err := json.Unmarshal(ca.Data, &payload); err != nil </span><span class="cov0" title="0">{ return ca, false }</span> - <span class="cov6" title="12">switch payload.Type </span>{ - case "rewrite":<span class="cov4" title="4"> - sys := s.promptRewriteSystem - user := renderTemplate(s.promptRewriteUser, map[string]string{"instruction": payload.Instruction, "selection": payload.Selection}) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} + <span class="cov5" title="12">switch payload.Type </span>{ + case "rewrite":<span class="cov3" title="4"> + sys := s.promptRewriteSystem + user := renderTemplate(s.promptRewriteUser, map[string]string{"instruction": payload.Instruction, "selection": payload.Selection}) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} opts := s.llmRequestOpts() - if text, err := s.llmClient.Chat(ctx, messages, opts...); err == nil </span><span class="cov4" title="4">{ - if out := stripCodeFences(strings.TrimSpace(text)); out != "" </span><span class="cov4" title="4">{ + if text, err := s.llmClient.Chat(ctx, messages, opts...); err == nil </span><span class="cov3" title="4">{ + if out := stripCodeFences(strings.TrimSpace(text)); out != "" </span><span class="cov3" title="4">{ edit := WorkspaceEdit{Changes: map[string][]TextEdit{payload.URI: {{Range: payload.Range, NewText: out}}}} ca.Edit = &edit return ca, true @@ -3251,37 +3701,37 @@ func (s *Server) resolveCodeAction(ca CodeAction) (CodeAction, bool) <span class } else<span class="cov0" title="0"> { logging.Logf("lsp ", "codeAction rewrite llm error: %v", err) }</span> - case "diagnostics":<span class="cov4" title="5"> - sys := s.promptDiagnosticsSystem - var b strings.Builder - for i, dgn := range payload.Diagnostics </span><span class="cov5" title="6">{ - if dgn.Source != "" </span><span class="cov0" title="0">{ - fmt.Fprintf(&b, "%d. [%s] %s\n", i+1, dgn.Source, dgn.Message) - }</span> else<span class="cov5" title="6"> { - fmt.Fprintf(&b, "%d. %s\n", i+1, dgn.Message) - }</span> - } - <span class="cov4" title="5">diagList := b.String() - user := renderTemplate(s.promptDiagnosticsUser, map[string]string{"diagnostics": diagList, "selection": payload.Selection}) - ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) - defer cancel() - messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} - opts := s.llmRequestOpts() - if text, err := s.llmClient.Chat(ctx, messages, opts...); err == nil </span><span class="cov4" title="5">{ - if out := stripCodeFences(strings.TrimSpace(text)); out != "" </span><span class="cov4" title="5">{ - edit := WorkspaceEdit{Changes: map[string][]TextEdit{payload.URI: {{Range: payload.Range, NewText: out}}}} - ca.Edit = &edit - return ca, true - }</span> - } else<span class="cov0" title="0"> { - logging.Logf("lsp ", "codeAction diagnostics llm error: %v", err) - }</span> - case "document":<span class="cov3" title="3"> - sys := s.promptDocumentSystem - user := renderTemplate(s.promptDocumentUser, map[string]string{"selection": payload.Selection}) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} + case "diagnostics":<span class="cov4" title="5"> + sys := s.promptDiagnosticsSystem + var b strings.Builder + for i, dgn := range payload.Diagnostics </span><span class="cov4" title="6">{ + if dgn.Source != "" </span><span class="cov0" title="0">{ + fmt.Fprintf(&b, "%d. [%s] %s\n", i+1, dgn.Source, dgn.Message) + }</span> else<span class="cov4" title="6"> { + fmt.Fprintf(&b, "%d. %s\n", i+1, dgn.Message) + }</span> + } + <span class="cov4" title="5">diagList := b.String() + user := renderTemplate(s.promptDiagnosticsUser, map[string]string{"diagnostics": diagList, "selection": payload.Selection}) + ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) + defer cancel() + messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} + opts := s.llmRequestOpts() + if text, err := s.llmClient.Chat(ctx, messages, opts...); err == nil </span><span class="cov4" title="5">{ + if out := stripCodeFences(strings.TrimSpace(text)); out != "" </span><span class="cov4" title="5">{ + edit := WorkspaceEdit{Changes: map[string][]TextEdit{payload.URI: {{Range: payload.Range, NewText: out}}}} + ca.Edit = &edit + return ca, true + }</span> + } else<span class="cov0" title="0"> { + logging.Logf("lsp ", "codeAction diagnostics llm error: %v", err) + }</span> + case "document":<span class="cov3" title="3"> + sys := s.promptDocumentSystem + user := renderTemplate(s.promptDocumentUser, map[string]string{"selection": payload.Selection}) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} opts := s.llmRequestOpts() if text, err := s.llmClient.Chat(ctx, messages, opts...); err == nil </span><span class="cov3" title="3">{ if out := stripCodeFences(strings.TrimSpace(text)); out != "" </span><span class="cov3" title="3">{ @@ -3324,58 +3774,58 @@ func (s *Server) handleCodeActionResolve(req Request) <span class="cov2" title=" // diagnosticsInRange parses the CodeAction context and returns diagnostics // that overlap the given selection range. If the context is missing or does // not contain diagnostics, returns an empty slice. -func (s *Server) diagnosticsInRange(ctxRaw json.RawMessage, sel Range) []Diagnostic <span class="cov4" title="5">{ - if len(ctxRaw) == 0 </span><span class="cov2" title="2">{ +func (s *Server) diagnosticsInRange(ctxRaw json.RawMessage, sel Range) []Diagnostic <span class="cov4" title="7">{ + if len(ctxRaw) == 0 </span><span class="cov3" title="3">{ return nil }</span> - <span class="cov3" title="3">var ctx CodeActionContext + <span class="cov3" title="4">var ctx CodeActionContext if err := json.Unmarshal(ctxRaw, &ctx); err != nil </span><span class="cov0" title="0">{ return nil }</span> - <span class="cov3" title="3">if len(ctx.Diagnostics) == 0 </span><span class="cov0" title="0">{ + <span class="cov3" title="4">if len(ctx.Diagnostics) == 0 </span><span class="cov0" title="0">{ return nil }</span> - <span class="cov3" title="3">out := make([]Diagnostic, 0, len(ctx.Diagnostics)) - for _, d := range ctx.Diagnostics </span><span class="cov5" title="6">{ - if rangesOverlap(d.Range, sel) </span><span class="cov3" title="3">{ + <span class="cov3" title="4">out := make([]Diagnostic, 0, len(ctx.Diagnostics)) + for _, d := range ctx.Diagnostics </span><span class="cov4" title="7">{ + if rangesOverlap(d.Range, sel) </span><span class="cov3" title="4">{ out = append(out, d) }</span> } - <span class="cov3" title="3">return out</span> + <span class="cov3" title="4">return out</span> } // rangesOverlap reports whether two LSP ranges overlap at all. -func rangesOverlap(a, b Range) bool <span class="cov5" title="8">{ +func rangesOverlap(a, b Range) bool <span class="cov5" title="10">{ // Normalize ordering if greaterPos(a.Start, a.End) </span><span class="cov0" title="0">{ a.Start, a.End = a.End, a.Start }</span> - <span class="cov5" title="8">if greaterPos(b.Start, b.End) </span><span class="cov0" title="0">{ + <span class="cov5" title="10">if greaterPos(b.Start, b.End) </span><span class="cov0" title="0">{ b.Start, b.End = b.End, b.Start }</span> // a ends before b starts - <span class="cov5" title="8">if lessPos(a.End, b.Start) </span><span class="cov3" title="3">{ + <span class="cov5" title="10">if lessPos(a.End, b.Start) </span><span class="cov3" title="3">{ return false }</span> // b ends before a starts - <span class="cov4" title="5">if lessPos(b.End, a.Start) </span><span class="cov1" title="1">{ + <span class="cov4" title="7">if lessPos(b.End, a.Start) </span><span class="cov1" title="1">{ return false }</span> - <span class="cov4" title="4">return true</span> + <span class="cov4" title="6">return true</span> } -func lessPos(p, q Position) bool <span class="cov7" title="14">{ - if p.Line != q.Line </span><span class="cov6" title="10">{ +func lessPos(p, q Position) bool <span class="cov6" title="19">{ + if p.Line != q.Line </span><span class="cov6" title="14">{ return p.Line < q.Line }</span> - <span class="cov4" title="4">return p.Character < q.Character</span> + <span class="cov4" title="5">return p.Character < q.Character</span> } -func greaterPos(p, q Position) bool <span class="cov7" title="17">{ - if p.Line != q.Line </span><span class="cov5" title="7">{ +func greaterPos(p, q Position) bool <span class="cov6" title="22">{ + if p.Line != q.Line </span><span class="cov5" title="11">{ return p.Line > q.Line }</span> - <span class="cov6" title="10">return p.Character > q.Character</span> + <span class="cov5" title="11">return p.Character > q.Character</span> } // --- Go unit test code action --- @@ -3423,28 +3873,28 @@ func (s *Server) buildDocumentCodeAction(p CodeActionParams, sel string) *CodeAc return &ca</span> } -func (s *Server) resolveGoTest(uri string, pos Position) (WorkspaceEdit, string, Range, bool) <span class="cov2" title="2">{ +func (s *Server) resolveGoTest(uri string, pos Position) (WorkspaceEdit, string, Range, bool) <span class="cov3" title="3">{ path := strings.TrimPrefix(uri, "file://") if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") </span><span class="cov0" title="0">{ return WorkspaceEdit{}, "", Range{}, false }</span> // Load source text - <span class="cov2" title="2">_, lines := s.loadFileText(uri) + <span class="cov3" title="3">_, lines := s.loadFileText(uri) if len(lines) == 0 </span><span class="cov0" title="0">{ return WorkspaceEdit{}, "", Range{}, false }</span> - <span class="cov2" title="2">pkg := parseGoPackageName(lines) + <span class="cov3" title="3">pkg := parseGoPackageName(lines) fnStart, fnEnd := findGoFunctionAtLine(lines, pos.Line) if fnStart < 0 || fnEnd < fnStart </span><span class="cov0" title="0">{ return WorkspaceEdit{}, "", Range{}, false }</span> - <span class="cov2" title="2">funcCode := strings.Join(lines[fnStart:fnEnd+1], "\n") + <span class="cov3" title="3">funcCode := strings.Join(lines[fnStart:fnEnd+1], "\n") testFunc := s.generateGoTestFunction(funcCode) if strings.TrimSpace(testFunc) == "" </span><span class="cov0" title="0">{ return WorkspaceEdit{}, "", Range{}, false }</span> // Determine test file target - <span class="cov2" title="2">testPath := strings.TrimSuffix(path, ".go") + "_test.go" + <span class="cov3" title="3">testPath := strings.TrimSuffix(path, ".go") + "_test.go" testURI := "file://" + testPath // If test file exists, append test at EOF; otherwise, create a new file with package+import @@ -3482,11 +3932,11 @@ func (s *Server) resolveGoTest(uri string, pos Position) (WorkspaceEdit, string, return we, testURI, jump, true</span> } // Create new file content - <span class="cov1" title="1">var content strings.Builder + <span class="cov2" title="2">var content strings.Builder if pkg == "" </span><span class="cov0" title="0">{ pkg = filepath.Base(filepath.Dir(path)) }</span> - <span class="cov1" title="1">content.WriteString("package ") + <span class="cov2" title="2">content.WriteString("package ") content.WriteString(pkg) content.WriteString("\n\n") content.WriteString("import (\n\t\"testing\"\n)\n\n") @@ -3501,69 +3951,69 @@ func (s *Server) resolveGoTest(uri string, pos Position) (WorkspaceEdit, string, pre := content.String() idx := strings.Index(pre, "func Test") startLine := 0 - if idx > 0 </span><span class="cov1" title="1">{ + if idx > 0 </span><span class="cov2" title="2">{ before := pre[:idx] startLine = strings.Count(before, "\n") }</span> - <span class="cov1" title="1">jump := Range{Start: Position{Line: startLine, Character: 0}, End: Position{Line: startLine, Character: 0}} + <span class="cov2" title="2">jump := Range{Start: Position{Line: startLine, Character: 0}, End: Position{Line: startLine, Character: 0}} return we, testURI, jump, true</span> } // loadFileText returns the file content and lines. It prefers the open document; otherwise reads from disk. -func (s *Server) loadFileText(uri string) (string, []string) <span class="cov3" title="3">{ +func (s *Server) loadFileText(uri string) (string, []string) <span class="cov3" title="4">{ if d := s.getDocument(uri); d != nil </span><span class="cov2" title="2">{ return d.text, append([]string{}, d.lines...) }</span> - <span class="cov1" title="1">path := strings.TrimPrefix(uri, "file://") + <span class="cov2" title="2">path := strings.TrimPrefix(uri, "file://") b, err := os.ReadFile(path) if err != nil </span><span class="cov0" title="0">{ return "", nil }</span> - <span class="cov1" title="1">txt := string(b) + <span class="cov2" title="2">txt := string(b) return txt, splitLines(txt)</span> } -func fileExists(path string) bool <span class="cov2" title="2">{ +func fileExists(path string) bool <span class="cov3" title="3">{ if _, err := os.Stat(path); err == nil </span><span class="cov1" title="1">{ return true }</span> - <span class="cov1" title="1">return false</span> + <span class="cov2" title="2">return false</span> } // parseGoPackageName returns the package name from file lines, or empty if not found. -func parseGoPackageName(lines []string) string <span class="cov4" title="4">{ - for _, ln := range lines </span><span class="cov4" title="5">{ +func parseGoPackageName(lines []string) string <span class="cov4" title="5">{ + for _, ln := range lines </span><span class="cov4" title="6">{ t := strings.TrimSpace(ln) - if strings.HasPrefix(t, "package ") </span><span class="cov3" title="3">{ + if strings.HasPrefix(t, "package ") </span><span class="cov3" title="4">{ name := strings.TrimSpace(strings.TrimPrefix(t, "package ")) // strip inline comments if i := strings.Index(name, " "); i >= 0 </span><span class="cov1" title="1">{ name = name[:i] }</span> - <span class="cov3" title="3">if i := strings.Index(name, "\t"); i >= 0 </span><span class="cov0" title="0">{ + <span class="cov3" title="4">if i := strings.Index(name, "\t"); i >= 0 </span><span class="cov0" title="0">{ name = name[:i] }</span> - <span class="cov3" title="3">if i := strings.Index(name, "//"); i >= 0 </span><span class="cov0" title="0">{ + <span class="cov3" title="4">if i := strings.Index(name, "//"); i >= 0 </span><span class="cov0" title="0">{ name = strings.TrimSpace(name[:i]) }</span> - <span class="cov3" title="3">return name</span> + <span class="cov3" title="4">return name</span> } } <span class="cov1" title="1">return ""</span> } // findGoFunctionAtLine finds the function enclosing or preceding line idx. Returns start and end line indexes. -func findGoFunctionAtLine(lines []string, idx int) (int, int) <span class="cov2" title="2">{ +func findGoFunctionAtLine(lines []string, idx int) (int, int) <span class="cov3" title="4">{ if idx < 0 </span><span class="cov0" title="0">{ idx = 0 }</span> - <span class="cov2" title="2">if idx >= len(lines) </span><span class="cov0" title="0">{ + <span class="cov3" title="4">if idx >= len(lines) </span><span class="cov0" title="0">{ idx = len(lines) - 1 }</span> // find signature start - <span class="cov2" title="2">start := -1 - for i := idx; i >= 0; i-- </span><span class="cov2" title="2">{ - if strings.Contains(lines[i], "func ") </span><span class="cov2" title="2">{ + <span class="cov3" title="4">start := -1 + for i := idx; i >= 0; i-- </span><span class="cov3" title="4">{ + if strings.Contains(lines[i], "func ") </span><span class="cov3" title="4">{ start = i break</span> } @@ -3571,45 +4021,45 @@ func findGoFunctionAtLine(lines []string, idx int) (int, int) <span class="cov2" break</span> } } - <span class="cov2" title="2">if start == -1 </span><span class="cov0" title="0">{ + <span class="cov3" title="4">if start == -1 </span><span class="cov0" title="0">{ return -1, -1 }</span> // find first '{' - <span class="cov2" title="2">depth := 0 + <span class="cov3" title="4">depth := 0 seenOpen := false - for i := start; i < len(lines); i++ </span><span class="cov2" title="2">{ + for i := start; i < len(lines); i++ </span><span class="cov4" title="5">{ ln := lines[i] - for j := 0; j < len(ln); j++ </span><span class="cov10" title="47">{ + for j := 0; j < len(ln); j++ </span><span class="cov10" title="106">{ switch ln[j] </span>{ - case '{':<span class="cov2" title="2"> + case '{':<span class="cov3" title="3"> depth++ seenOpen = true</span> - case '}':<span class="cov2" title="2"> - if depth > 0 </span><span class="cov2" title="2">{ + case '}':<span class="cov3" title="3"> + if depth > 0 </span><span class="cov3" title="3">{ depth-- }</span> - <span class="cov2" title="2">if seenOpen && depth == 0 </span><span class="cov2" title="2">{ + <span class="cov3" title="3">if seenOpen && depth == 0 </span><span class="cov3" title="3">{ return start, i }</span> } } } // if never saw '{', assume single-line prototype; return that line - <span class="cov0" title="0">if !seenOpen </span><span class="cov0" title="0">{ + <span class="cov1" title="1">if !seenOpen </span><span class="cov1" title="1">{ return start, start }</span> <span class="cov0" title="0">return start, -1</span> } // generateGoTestFunction uses LLM to produce a test function; falls back to a stub when unavailable. -func (s *Server) generateGoTestFunction(funcCode string) string <span class="cov3" title="3">{ - if s.llmClient != nil </span><span class="cov2" title="2">{ - sys := s.promptGoTestSystem - user := renderTemplate(s.promptGoTestUser, map[string]string{"function": funcCode}) - ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) - defer cancel() - messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} - opts := s.llmRequestOpts() +func (s *Server) generateGoTestFunction(funcCode string) string <span class="cov3" title="4">{ + if s.llmClient != nil </span><span class="cov2" title="2">{ + sys := s.promptGoTestSystem + user := renderTemplate(s.promptGoTestUser, map[string]string{"function": funcCode}) + ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) + defer cancel() + messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} + opts := s.llmRequestOpts() if out, err := s.llmClient.Chat(ctx, messages, opts...); err == nil </span><span class="cov2" title="2">{ cleaned := strings.TrimSpace(stripCodeFences(out)) if cleaned != "" </span><span class="cov2" title="2">{ @@ -3620,22 +4070,22 @@ func (s *Server) generateGoTestFunction(funcCode string) string <span class="cov }</span> } // Fallback stub - <span class="cov1" title="1">name := deriveGoFuncName(funcCode) + <span class="cov2" title="2">name := deriveGoFuncName(funcCode) if name == "" </span><span class="cov0" title="0">{ name = "Function" }</span> - <span class="cov1" title="1">return fmt.Sprintf("func Test%s(t *testing.T) {\n\t// TODO: implement tests for %s\n}\n", exportName(name), name)</span> + <span class="cov2" title="2">return fmt.Sprintf("func Test%s(t *testing.T) {\n\t// TODO: implement tests for %s\n}\n", exportName(name), name)</span> } // deriveGoFuncName extracts function or method name from code. -func deriveGoFuncName(code string) string <span class="cov3" title="3">{ +func deriveGoFuncName(code string) string <span class="cov3" title="4">{ // look for line starting with func line := firstLine(code) line = strings.TrimSpace(line) if !strings.HasPrefix(line, "func ") </span><span class="cov0" title="0">{ return "" }</span> - <span class="cov3" title="3">rest := strings.TrimSpace(strings.TrimPrefix(line, "func ")) + <span class="cov3" title="4">rest := strings.TrimSpace(strings.TrimPrefix(line, "func ")) // method receiver if strings.HasPrefix(rest, "(") </span><span class="cov1" title="1">{ // find ")" @@ -3644,25 +4094,25 @@ func deriveGoFuncName(code string) string <span class="cov3" title="3">{ }</span> } // now rest should start with Name( - <span class="cov3" title="3">if i := strings.Index(rest, "("); i > 0 </span><span class="cov3" title="3">{ + <span class="cov3" title="4">if i := strings.Index(rest, "("); i > 0 </span><span class="cov3" title="4">{ return strings.TrimSpace(rest[:i]) }</span> <span class="cov0" title="0">return ""</span> } -func exportName(name string) string <span class="cov1" title="1">{ +func exportName(name string) string <span class="cov2" title="2">{ if name == "" </span><span class="cov0" title="0">{ return name }</span> - <span class="cov1" title="1">r := []rune(name) + <span class="cov2" title="2">r := []rune(name) if r[0] >= 'a' && r[0] <= 'z' </span><span class="cov0" title="0">{ r[0] = r[0] - ('a' - 'A') }</span> - <span class="cov1" title="1">return string(r)</span> + <span class="cov2" title="2">return string(r)</span> } </pre> - <pre class="file" id="file16" style="display: none">// Summary: Completion handlers split from handlers.go to reduce file size and isolate feature logic. + <pre class="file" id="file23" style="display: none">// Summary: Completion handlers split from handlers.go to reduce file size and isolate feature logic. package lsp import ( @@ -3889,13 +4339,13 @@ func (s *Server) tryProviderNativeCompletion(current string, p CompletionParams, if !ok </span><span class="cov6" title="6">{ return nil, false }</span> - <span class="cov6" title="5">before, after := s.docBeforeAfter(p.TextDocument.URI, p.Position) - path := strings.TrimPrefix(p.TextDocument.URI, "file://") - // Build provider-native prompt from template - prompt := renderTemplate(s.promptNativeCompletion, map[string]string{ - "path": path, - "before": before, - }) + <span class="cov6" title="5">before, after := s.docBeforeAfter(p.TextDocument.URI, p.Position) + path := strings.TrimPrefix(p.TextDocument.URI, "file://") + // Build provider-native prompt from template + prompt := renderTemplate(s.promptNativeCompletion, map[string]string{ + "path": path, + "before": before, + }) lang := "" temp := 0.0 if s.codingTemperature != nil </span><span class="cov0" title="0">{ @@ -4004,34 +4454,34 @@ func (s *Server) waitForThrottle(ctx context.Context) bool <span class="cov8" ti // buildCompletionMessages constructs the LLM messages for completion. func (s *Server) buildCompletionMessages(inlinePrompt, hasExtra bool, extraText string, inParams bool, p CompletionParams, above, current, below, funcCtx string) []llm.Message <span class="cov8" title="13">{ - // Vars for templates - vars := map[string]string{ - "file": p.TextDocument.URI, - "function": funcCtx, - "above": above, - "current": current, - "below": below, - "char": fmt.Sprintf("%d", p.Position.Character), - } - sys := s.promptCompSysGeneral - userTpl := s.promptCompUserGeneral - if inParams </span><span class="cov3" title="2">{ - sys = s.promptCompSysParams - userTpl = s.promptCompUserParams - }</span> - <span class="cov8" title="13">if inlinePrompt && strings.TrimSpace(s.promptCompSysInline) != "" </span><span class="cov1" title="1">{ - sys = s.promptCompSysInline - }</span> - <span class="cov8" title="13">user := renderTemplate(userTpl, vars) - messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} - if hasExtra && strings.TrimSpace(extraText) != "" </span><span class="cov1" title="1">{ - extra := renderTemplate(s.promptCompExtraHeader, map[string]string{"context": extraText}) - if strings.TrimSpace(extra) == "" </span><span class="cov0" title="0">{ - extra = extraText - }</span> - <span class="cov1" title="1">messages = append(messages, llm.Message{Role: "user", Content: extra})</span> - } - <span class="cov8" title="13">return messages</span> + // Vars for templates + vars := map[string]string{ + "file": p.TextDocument.URI, + "function": funcCtx, + "above": above, + "current": current, + "below": below, + "char": fmt.Sprintf("%d", p.Position.Character), + } + sys := s.promptCompSysGeneral + userTpl := s.promptCompUserGeneral + if inParams </span><span class="cov3" title="2">{ + sys = s.promptCompSysParams + userTpl = s.promptCompUserParams + }</span> + <span class="cov8" title="13">if inlinePrompt && strings.TrimSpace(s.promptCompSysInline) != "" </span><span class="cov1" title="1">{ + sys = s.promptCompSysInline + }</span> + <span class="cov8" title="13">user := renderTemplate(userTpl, vars) + messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} + if hasExtra && strings.TrimSpace(extraText) != "" </span><span class="cov1" title="1">{ + extra := renderTemplate(s.promptCompExtraHeader, map[string]string{"context": extraText}) + if strings.TrimSpace(extra) == "" </span><span class="cov0" title="0">{ + extra = extraText + }</span> + <span class="cov1" title="1">messages = append(messages, llm.Message{Role: "user", Content: extra})</span> + } + <span class="cov8" title="13">return messages</span> } // postProcessCompletion normalizes and deduplicates completion text and applies indentation rules. @@ -4057,7 +4507,7 @@ func (s *Server) postProcessCompletion(text string, leftOfCursor string, current } </pre> - <pre class="file" id="file17" style="display: none">// Summary: Document open/change/close and in-editor chat handlers split out of handlers.go. + <pre class="file" id="file24" style="display: none">// Summary: Document open/change/close and in-editor chat handlers split out of handlers.go. package lsp import ( @@ -4215,9 +4665,9 @@ func (s *Server) detectAndHandleChat(uri string) <span class="cov7" title="5">{ go func(prompt string, remove int) </span><span class="cov5" title="3">{ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() - sys := s.promptChatSystem - // Build short conversation history from the document above this line - history := s.buildChatHistory(uri, lineIdx, prompt) + sys := s.promptChatSystem + // Build short conversation history from the document above this line + history := s.buildChatHistory(uri, lineIdx, prompt) msgs := append([]llm.Message{{Role: "system", Content: sys}}, history...) opts := s.llmRequestOpts() logging.Logf("lsp ", "chat llm=requesting model=%s", s.llmClient.DefaultModel()) @@ -4386,7 +4836,7 @@ func (s *Server) deferShowDocument(uri string, sel Range) <span class="cov1" tit } </pre> - <pre class="file" id="file18" style="display: none">// Summary: ExecuteCommand handler to support post-edit navigation (jump to generated test). + <pre class="file" id="file25" style="display: none">// Summary: ExecuteCommand handler to support post-edit navigation (jump to generated test). package lsp import ( @@ -4422,7 +4872,7 @@ func (s *Server) handleExecuteCommand(req Request) <span class="cov8" title="1"> } </pre> - <pre class="file" id="file19" style="display: none">// Summary: Initialization and lifecycle handlers split from handlers.go. + <pre class="file" id="file26" style="display: none">// Summary: Initialization and lifecycle handlers split from handlers.go. package lsp import ( @@ -4465,7 +4915,7 @@ func (s *Server) handleExit() <span class="cov0" title="0">{ }</span> </pre> - <pre class="file" id="file20" style="display: none">// Summary: Generic LSP helpers shared across handlers (LLM opts, prompts, text utils, counters). + <pre class="file" id="file27" style="display: none">// Summary: Generic LSP helpers shared across handlers (LLM opts, prompts, text utils, counters). package lsp import ( @@ -4474,6 +4924,7 @@ import ( "codeberg.org/snonux/hexai/internal/llm" "codeberg.org/snonux/hexai/internal/logging" + "codeberg.org/snonux/hexai/internal/textutil" ) // Configurable inline trigger characters (default to '>') used by free helpers below. @@ -4530,27 +4981,17 @@ func (s *Server) logLLMStats() <span class="cov5" title="8">{ } // Completion prompt builders and filters -func inParamList(current string, cursor int) bool <span class="cov5" title="11">{ - if !strings.Contains(current, "func ") </span><span class="cov4" title="6">{ +func inParamList(current string, cursor int) bool <span class="cov6" title="13">{ + if !strings.Contains(current, "func ") </span><span class="cov4" title="7">{ return false }</span> - <span class="cov4" title="5">open := strings.Index(current, "(") + <span class="cov4" title="6">open := strings.Index(current, "(") close := strings.Index(current, ")") return open >= 0 && cursor > open && (close == -1 || cursor <= close)</span> } // renderTemplate performs simple {{var}} replacement in a template string. -func renderTemplate(t string, vars map[string]string) string <span class="cov7" title="33">{ - if t == "" </span><span class="cov5" title="9">{ - return t - }</span> - <span class="cov7" title="24">out := t - for k, v := range vars </span><span class="cov9" title="79">{ - placeholder := "{{" + k + "}}" - out = strings.ReplaceAll(out, placeholder, v) - }</span> - <span class="cov7" title="24">return out</span> -} +func renderTemplate(t string, vars map[string]string) string <span class="cov7" title="33">{ return textutil.RenderTemplate(t, vars) }</span> func computeTextEditAndFilter(cleaned string, inParams bool, current string, p CompletionParams) (*TextEdit, string) <span class="cov6" title="17">{ if inParams </span><span class="cov3" title="3">{ @@ -4579,63 +5020,63 @@ func computeTextEditAndFilter(cleaned string, inParams bool, current string, p C return te, filter</span> } -func computeWordStart(current string, at int) int <span class="cov7" title="23">{ +func computeWordStart(current string, at int) int <span class="cov7" title="24">{ if at > len(current) </span><span class="cov0" title="0">{ at = len(current) }</span> - <span class="cov7" title="23">for at > 0 </span><span class="cov8" title="39">{ + <span class="cov7" title="24">for at > 0 </span><span class="cov8" title="49">{ ch := current[at-1] - if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' </span><span class="cov6" title="21">{ + if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' </span><span class="cov7" title="31">{ at-- continue</span> } <span class="cov6" title="18">break</span> } - <span class="cov7" title="23">return at</span> + <span class="cov7" title="24">return at</span> } -func isIdentChar(ch byte) bool <span class="cov7" title="24">{ +func isIdentChar(ch byte) bool <span class="cov7" title="26">{ return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' }</span> // Inline prompt utilities -func lineHasInlinePrompt(line string) bool <span class="cov6" title="20">{ - if _, _, _, ok := findStrictInlineTag(line); ok </span><span class="cov3" title="3">{ +func lineHasInlinePrompt(line string) bool <span class="cov6" title="21">{ + if _, _, _, ok := findStrictInlineTag(line); ok </span><span class="cov3" title="4">{ return true }</span> <span class="cov6" title="17">return hasDoubleOpenTrigger(line)</span> } -func leadingIndent(line string) string <span class="cov3" title="3">{ +func leadingIndent(line string) string <span class="cov3" title="4">{ i := 0 - for i < len(line) </span><span class="cov5" title="10">{ - if line[i] == ' ' || line[i] == '\t' </span><span class="cov4" title="7">{ + for i < len(line) </span><span class="cov6" title="14">{ + if line[i] == ' ' || line[i] == '\t' </span><span class="cov5" title="10">{ i++ continue</span> } - <span class="cov3" title="3">break</span> + <span class="cov3" title="4">break</span> } - <span class="cov3" title="3">if i == 0 </span><span class="cov0" title="0">{ + <span class="cov3" title="4">if i == 0 </span><span class="cov0" title="0">{ return "" }</span> - <span class="cov3" title="3">return line[:i]</span> + <span class="cov3" title="4">return line[:i]</span> } -func applyIndent(indent, suggestion string) string <span class="cov3" title="3">{ +func applyIndent(indent, suggestion string) string <span class="cov3" title="4">{ if indent == "" || suggestion == "" </span><span class="cov0" title="0">{ return suggestion }</span> - <span class="cov3" title="3">lines := splitLines(suggestion) - for i, ln := range lines </span><span class="cov5" title="8">{ + <span class="cov3" title="4">lines := splitLines(suggestion) + for i, ln := range lines </span><span class="cov5" title="10">{ if strings.TrimSpace(ln) == "" </span><span class="cov1" title="1">{ continue</span> } - <span class="cov4" title="7">if strings.HasPrefix(ln, indent) </span><span class="cov0" title="0">{ + <span class="cov5" title="9">if strings.HasPrefix(ln, indent) </span><span class="cov0" title="0">{ continue</span> } - <span class="cov4" title="7">lines[i] = indent + ln</span> + <span class="cov5" title="9">lines[i] = indent + ln</span> } - <span class="cov3" title="3">return strings.Join(lines, "\n")</span> + <span class="cov3" title="4">return strings.Join(lines, "\n")</span> } // --- Inline marker parsing and general string utilities --- @@ -4643,36 +5084,36 @@ func applyIndent(indent, suggestion string) string <span class="cov3" title="3"> // findStrictInlineTag finds >text> (configurable), with no space after the first // opening marker and no space immediately before the closing marker. Returns the // text between markers, the start index, the end index just after closing, and ok. -func findStrictInlineTag(line string) (string, int, int, bool) <span class="cov8" title="49">{ +func findStrictInlineTag(line string) (string, int, int, bool) <span class="cov8" title="50">{ pos := 0 - for pos < len(line) </span><span class="cov9" title="64">{ + for pos < len(line) </span><span class="cov9" title="65">{ // find opening marker j := strings.IndexByte(line[pos:], inlineOpenChar) if j < 0 </span><span class="cov7" title="27">{ return "", 0, 0, false }</span> - <span class="cov8" title="37">j += pos + <span class="cov8" title="38">j += pos // ensure single open (not double) and non-space after if j+1 >= len(line) || line[j+1] == inlineOpenChar || line[j+1] == ' ' </span><span class="cov6" title="21">{ pos = j + 1 continue</span> } // find closing marker - <span class="cov6" title="16">k := strings.IndexByte(line[j+1:], inlineCloseChar) + <span class="cov6" title="17">k := strings.IndexByte(line[j+1:], inlineCloseChar) if k < 0 </span><span class="cov1" title="1">{ return "", 0, 0, false }</span> - <span class="cov6" title="15">closeIdx := j + 1 + k + <span class="cov6" title="16">closeIdx := j + 1 + k if closeIdx-1 < 0 || line[closeIdx-1] == ' ' </span><span class="cov1" title="1">{ pos = closeIdx + 1 continue</span> } - <span class="cov6" title="14">inner := strings.TrimSpace(line[j+1 : closeIdx]) + <span class="cov6" title="15">inner := strings.TrimSpace(line[j+1 : closeIdx]) if inner == "" </span><span class="cov0" title="0">{ pos = closeIdx + 1 continue</span> } - <span class="cov6" title="14">end := closeIdx + 1 + <span class="cov6" title="15">end := closeIdx + 1 return inner, j, end, true</span> } <span class="cov4" title="7">return "", 0, 0, false</span> @@ -4768,57 +5209,33 @@ func isIdentBoundary(ch byte) bool <span class="cov10" title="100">{ }</span> // stripCodeFences removes surrounding Markdown code fences from a model response. -func stripCodeFences(s string) string <span class="cov8" title="36">{ - t := strings.TrimSpace(s) - if t == "" </span><span class="cov0" title="0">{ - return t - }</span> - <span class="cov8" title="36">lines := splitLines(t) - start := 0 - for start < len(lines) && strings.TrimSpace(lines[start]) == "" </span><span class="cov0" title="0">{ - start++ - }</span> - <span class="cov8" title="36">end := len(lines) - 1 - for end >= 0 && strings.TrimSpace(lines[end]) == "" </span><span class="cov0" title="0">{ - end-- - }</span> - <span class="cov8" title="36">if start >= len(lines) || end < 0 || start > end </span><span class="cov0" title="0">{ - return t - }</span> - <span class="cov8" title="36">first := strings.TrimSpace(lines[start]) - last := strings.TrimSpace(lines[end]) - if strings.HasPrefix(first, "```") && last == "```" && end > start </span><span class="cov5" title="8">{ - inner := strings.Join(lines[start+1:end], "\n") - return inner - }</span> - <span class="cov7" title="28">return t</span> -} +func stripCodeFences(s string) string <span class="cov8" title="36">{ return textutil.StripCodeFences(s) }</span> // stripInlineCodeSpan returns the contents of the first inline backtick code span if present. -func stripInlineCodeSpan(s string) string <span class="cov5" title="10">{ +func stripInlineCodeSpan(s string) string <span class="cov5" title="11">{ t := strings.TrimSpace(s) if t == "" </span><span class="cov0" title="0">{ return t }</span> - <span class="cov5" title="10">i := strings.IndexByte(t, '`') + <span class="cov5" title="11">i := strings.IndexByte(t, '`') if i < 0 </span><span class="cov2" title="2">{ return t }</span> - <span class="cov5" title="8">jrel := strings.IndexByte(t[i+1:], '`') + <span class="cov5" title="9">jrel := strings.IndexByte(t[i+1:], '`') if jrel < 0 </span><span class="cov2" title="2">{ return t }</span> - <span class="cov4" title="6">j := i + 1 + jrel + <span class="cov4" title="7">j := i + 1 + jrel return t[i+1 : j]</span> } // labelForCompletion picks a short, readable label for the completion list. -func labelForCompletion(cleaned, filter string) string <span class="cov6" title="18">{ +func labelForCompletion(cleaned, filter string) string <span class="cov6" title="20">{ label := trimLen(firstLine(cleaned)) - if filter != "" && !strings.HasPrefix(strings.ToLower(label), strings.ToLower(filter)) </span><span class="cov3" title="4">{ + if filter != "" && !strings.HasPrefix(strings.ToLower(label), strings.ToLower(filter)) </span><span class="cov4" title="5">{ return filter }</span> - <span class="cov6" title="14">return label</span> + <span class="cov6" title="15">return label</span> } // extractRangeText returns the exact text within the given document range. @@ -4879,89 +5296,89 @@ func (s *Server) collectPromptRemovalEdits(uri string) []TextEdit <span class="c <span class="cov2" title="2">return edits</span> } -func promptRemovalEditsForLine(line string, lineNum int) []TextEdit <span class="cov5" title="10">{ - if hasDoubleOpenTrigger(line) </span><span class="cov3" title="3">{ +func promptRemovalEditsForLine(line string, lineNum int) []TextEdit <span class="cov5" title="11">{ + if hasDoubleOpenTrigger(line) </span><span class="cov3" title="4">{ return []TextEdit{{Range: Range{Start: Position{Line: lineNum, Character: 0}, End: Position{Line: lineNum, Character: len(line)}}, NewText: ""}} }</span> <span class="cov4" title="7">return collectSemicolonMarkers(line, lineNum)</span> } -func hasDoubleOpenTrigger(line string) bool <span class="cov8" title="55">{ +func hasDoubleOpenTrigger(line string) bool <span class="cov8" title="56">{ pos := 0 - for pos < len(line) </span><span class="cov8" title="57">{ + for pos < len(line) </span><span class="cov8" title="58">{ // look for double-open sequence dbl := string([]byte{inlineOpenChar, inlineOpenChar}) j := strings.Index(line[pos:], dbl) if j < 0 </span><span class="cov8" title="36">{ return false }</span> - <span class="cov6" title="21">j += pos + <span class="cov7" title="22">j += pos contentStart := j + len(dbl) if contentStart >= len(line) </span><span class="cov4" title="7">{ return false }</span> - <span class="cov6" title="14">first := line[contentStart] + <span class="cov6" title="15">first := line[contentStart] if first == ' ' || first == inlineOpenChar </span><span class="cov3" title="4">{ pos = contentStart + 1 continue</span> } // find closing - <span class="cov5" title="10">k := strings.IndexByte(line[contentStart+1:], inlineCloseChar) + <span class="cov5" title="11">k := strings.IndexByte(line[contentStart+1:], inlineCloseChar) if k < 0 </span><span class="cov0" title="0">{ return false }</span> - <span class="cov5" title="10">closeIdx := contentStart + 1 + k + <span class="cov5" title="11">closeIdx := contentStart + 1 + k if closeIdx-1 >= 0 && line[closeIdx-1] == ' ' </span><span class="cov1" title="1">{ pos = closeIdx + 1 continue</span> } - <span class="cov5" title="9">return true</span> + <span class="cov5" title="10">return true</span> } <span class="cov3" title="3">return false</span> } -func collectSemicolonMarkers(line string, lineNum int) []TextEdit <span class="cov5" title="8">{ +func collectSemicolonMarkers(line string, lineNum int) []TextEdit <span class="cov5" title="9">{ var edits []TextEdit startSemi := 0 - for startSemi < len(line) </span><span class="cov5" title="12">{ + for startSemi < len(line) </span><span class="cov6" title="14">{ j := strings.IndexByte(line[startSemi:], inlineOpenChar) - if j < 0 </span><span class="cov4" title="7">{ + if j < 0 </span><span class="cov5" title="8">{ break</span> } - <span class="cov4" title="5">j += startSemi + <span class="cov4" title="6">j += startSemi k := strings.IndexByte(line[j+1:], inlineCloseChar) if k < 0 </span><span class="cov0" title="0">{ break</span> } - <span class="cov4" title="5">if j+1 >= len(line) || line[j+1] == ' ' </span><span class="cov0" title="0">{ + <span class="cov4" title="6">if j+1 >= len(line) || line[j+1] == ' ' </span><span class="cov0" title="0">{ startSemi = j + 1 continue</span> } - <span class="cov4" title="5">if line[j+1] == inlineOpenChar </span><span class="cov0" title="0">{ // skip double-open start + <span class="cov4" title="6">if line[j+1] == inlineOpenChar </span><span class="cov0" title="0">{ // skip double-open start startSemi = j + 2 continue</span> } - <span class="cov4" title="5">closeIdx := j + 1 + k + <span class="cov4" title="6">closeIdx := j + 1 + k if closeIdx-1 < 0 || line[closeIdx-1] == ' ' </span><span class="cov0" title="0">{ startSemi = closeIdx + 1 continue</span> } - <span class="cov4" title="5">if closeIdx-(j+1) < 1 </span><span class="cov0" title="0">{ + <span class="cov4" title="6">if closeIdx-(j+1) < 1 </span><span class="cov0" title="0">{ startSemi = closeIdx + 1 continue</span> } - <span class="cov4" title="5">endChar := closeIdx + 1 + <span class="cov4" title="6">endChar := closeIdx + 1 if endChar < len(line) && line[endChar] == ' ' </span><span class="cov3" title="4">{ endChar++ }</span> - <span class="cov4" title="5">edits = append(edits, TextEdit{Range: Range{Start: Position{Line: lineNum, Character: j}, End: Position{Line: lineNum, Character: endChar}}, NewText: ""}) + <span class="cov4" title="6">edits = append(edits, TextEdit{Range: Range{Start: Position{Line: lineNum, Character: j}, End: Position{Line: lineNum, Character: endChar}}, NewText: ""}) startSemi = endChar</span> } - <span class="cov5" title="8">return edits</span> + <span class="cov5" title="9">return edits</span> } </pre> - <pre class="file" id="file21" style="display: none">// Summary: Minimal LSP server over stdio; manages documents, dispatches requests, and tracks stats. + <pre class="file" id="file28" style="display: none">// Summary: Minimal LSP server over stdio; manages documents, dispatches requests, and tracks stats. package lsp import ( @@ -5021,29 +5438,29 @@ type Server struct { inlineOpen string inlineClose string chatSuffix string - chatPrefixes []string - - // Prompt templates - // Completion - promptCompSysGeneral string - promptCompSysParams string - promptCompSysInline string - promptCompUserGeneral string - promptCompUserParams string - promptCompExtraHeader string - // Provider-native code completion - promptNativeCompletion string - // In-editor chat - promptChatSystem string - // Code actions - promptRewriteSystem string - promptDiagnosticsSystem string - promptDocumentSystem string - promptRewriteUser string - promptDiagnosticsUser string - promptDocumentUser string - promptGoTestSystem string - promptGoTestUser string + chatPrefixes []string + + // Prompt templates + // Completion + promptCompSysGeneral string + promptCompSysParams string + promptCompSysInline string + promptCompUserGeneral string + promptCompUserParams string + promptCompExtraHeader string + // Provider-native code completion + promptNativeCompletion string + // In-editor chat + promptChatSystem string + // Code actions + promptRewriteSystem string + promptDiagnosticsSystem string + promptDocumentSystem string + promptRewriteUser string + promptDiagnosticsUser string + promptDocumentUser string + promptGoTestSystem string + promptGoTestUser string } // ServerOptions collects configuration for NewServer to avoid long parameter lists. @@ -5064,26 +5481,26 @@ type ServerOptions struct { // Inline/chat triggers InlineOpen string InlineClose string - ChatSuffix string - ChatPrefixes []string - - // Prompt templates - PromptCompSysGeneral string - PromptCompSysParams string - PromptCompSysInline string - PromptCompUserGeneral string - PromptCompUserParams string - PromptCompExtraHeader string - PromptNativeCompletion string - PromptChatSystem string - PromptRewriteSystem string - PromptDiagnosticsSystem string - PromptDocumentSystem string - PromptRewriteUser string - PromptDiagnosticsUser string - PromptDocumentUser string - PromptGoTestSystem string - PromptGoTestUser string + ChatSuffix string + ChatPrefixes []string + + // Prompt templates + PromptCompSysGeneral string + PromptCompSysParams string + PromptCompSysInline string + PromptCompUserGeneral string + PromptCompUserParams string + PromptCompExtraHeader string + PromptNativeCompletion string + PromptChatSystem string + PromptRewriteSystem string + PromptDiagnosticsSystem string + PromptDocumentSystem string + PromptRewriteUser string + PromptDiagnosticsUser string + PromptDocumentUser string + PromptGoTestSystem string + PromptGoTestUser string } func NewServer(r io.Reader, w io.Writer, logger *log.Logger, opts ServerOptions) *Server <span class="cov10" title="6">{ @@ -5142,29 +5559,29 @@ func NewServer(r io.Reader, w io.Writer, logger *log.Logger, opts ServerOptions) }</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">{ - s.chatPrefixes = []string{"?", "!", ":", ";"} - }</span> else<span class="cov6" title="3"> { - s.chatPrefixes = append([]string{}, opts.ChatPrefixes...) - }</span> + <span class="cov10" title="6">if len(opts.ChatPrefixes) == 0 </span><span class="cov6" title="3">{ + 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 - s.promptCompSysParams = opts.PromptCompSysParams - s.promptCompSysInline = opts.PromptCompSysInline - s.promptCompUserGeneral = opts.PromptCompUserGeneral - s.promptCompUserParams = opts.PromptCompUserParams - s.promptCompExtraHeader = opts.PromptCompExtraHeader - s.promptNativeCompletion = opts.PromptNativeCompletion - s.promptChatSystem = opts.PromptChatSystem - s.promptRewriteSystem = opts.PromptRewriteSystem - s.promptDiagnosticsSystem = opts.PromptDiagnosticsSystem - s.promptDocumentSystem = opts.PromptDocumentSystem - s.promptRewriteUser = opts.PromptRewriteUser - s.promptDiagnosticsUser = opts.PromptDiagnosticsUser - s.promptDocumentUser = opts.PromptDocumentUser - s.promptGoTestSystem = opts.PromptGoTestSystem - s.promptGoTestUser = opts.PromptGoTestUser + // Prompts + <span class="cov10" title="6">s.promptCompSysGeneral = opts.PromptCompSysGeneral + s.promptCompSysParams = opts.PromptCompSysParams + s.promptCompSysInline = opts.PromptCompSysInline + s.promptCompUserGeneral = opts.PromptCompUserGeneral + s.promptCompUserParams = opts.PromptCompUserParams + s.promptCompExtraHeader = opts.PromptCompExtraHeader + s.promptNativeCompletion = opts.PromptNativeCompletion + s.promptChatSystem = opts.PromptChatSystem + s.promptRewriteSystem = opts.PromptRewriteSystem + s.promptDiagnosticsSystem = opts.PromptDiagnosticsSystem + s.promptDocumentSystem = opts.PromptDocumentSystem + s.promptRewriteUser = opts.PromptRewriteUser + s.promptDiagnosticsUser = opts.PromptDiagnosticsUser + s.promptDocumentUser = opts.PromptDocumentUser + s.promptGoTestSystem = opts.PromptGoTestSystem + s.promptGoTestUser = opts.PromptGoTestUser // Assign package-level inline trigger chars for free helper functions if s.inlineOpen != "" </span><span class="cov10" title="6">{ @@ -5222,7 +5639,7 @@ func (s *Server) Run() error <span class="cov1" title="1">{ } </pre> - <pre class="file" id="file22" style="display: none">// Summary: LSP transport utilities to read and write JSON-RPC messages with Content-Length framing. + <pre class="file" id="file29" style="display: none">// Summary: LSP transport utilities to read and write JSON-RPC messages with Content-Length framing. package lsp import ( @@ -5290,7 +5707,7 @@ func (s *Server) writeMessage(v any) <span class="cov10" title="18">{ } </pre> - <pre class="file" id="file23" style="display: none">package testutil + <pre class="file" id="file30" style="display: none">package testutil // MultilineDocBlock returns a realistic multi-line documentation block. func MultilineDocBlock() string <span class="cov8" title="1">{ @@ -5318,6 +5735,122 @@ func MalformedJSON() string <span class="cov0" title="0">{ }</span> </pre> + <pre class="file" id="file31" style="display: none">package textutil + +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">{ + 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">{ + out = strings.ReplaceAll(out, "{{"+k+"}}", v) + }</span> + <span class="cov7" title="33">return out</span> +} + +// StripCodeFences removes surrounding Markdown triple-backtick fences. +func StripCodeFences(s string) string <span class="cov8" title="49">{ + t := strings.TrimSpace(s) + if t == "" </span><span class="cov0" title="0">{ + return t + }</span> + <span class="cov8" title="49">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="49">end := len(lines) - 1 + for end >= 0 && strings.TrimSpace(lines[end]) == "" </span><span class="cov0" title="0">{ + end-- + }</span> + <span class="cov8" title="49">if start >= len(lines) || end < 0 || start > end </span><span class="cov0" title="0">{ + return t + }</span> + <span class="cov8" title="49">first := strings.TrimSpace(lines[start]) + last := strings.TrimSpace(lines[end]) + if strings.HasPrefix(first, "```") && last == "```" && end > start </span><span class="cov6" title="19">{ + inner := strings.Join(lines[start+1:end], "\n") + return inner + }</span> + <span class="cov7" title="30">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">{ + 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">{ + lines[idx] = cleaned + return instr, strings.Join(lines, "\n") + }</span> + } + <span class="cov0" title="0">return "", sel</span> +} + +// FindFirstInstructionInLine returns (instruction, cleaned, ok) for a single line. +func FindFirstInstructionInLine(line string) (instr, cleaned string, ok bool) <span class="cov6" title="14">{ + type cand struct{ start, end int; text string } + cands := []cand{} + if t, l, r, ok := FindStrictInlineTag(line); ok </span><span class="cov3" title="4">{ + 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">{ + if j := strings.Index(line[i+2:], "*/"); j >= 0 </span><span class="cov2" title="2">{ + start := i + end := i + 2 + j + 2 + text := strings.TrimSpace(line[i+2 : i+2+j]) + 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">{ + if j := strings.Index(line[i+4:], "-->"); j >= 0 </span><span class="cov2" title="2">{ + start := i + end := i + 4 + j + 3 + text := strings.TrimSpace(line[i+4 : i+4+j]) + 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">{ + 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">{ + 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">{ + 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] + 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") + 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">{ + 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">{ + 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="cov5" title="11">return "", -1, -1, false</span> +} + +</pre> + </div> </body> <script> |
