diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-06 10:25:36 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-06 10:25:36 +0300 |
| commit | 5be9532cfa630f4aacd8d879c3e4f5cc316da0fa (patch) | |
| tree | 0a901680fccd1e2703ffdbd9284ccff932be1d67 | |
| parent | 70f1d0e78c57dfa5beae779b3d392b6e6fa44c14 (diff) | |
feat(lsp): configurable inline/chat triggers; switch inline markers to >text>/>>text>; update docs and example config; tests updated to new triggers and raise LSP coverage to >=85%; chore: remove semicolon legacy; chore(mage): auto-refresh coverage daily if docs/coverage.out is older than 24h
36 files changed, 4758 insertions, 3932 deletions
@@ -18,7 +18,7 @@ ## Coding Style & Naming Conventions -- Aim for at least 80% unit test coverage of all source code. +- Aim for at least 85% unit test coverage of all source code. - Ensure that all unit tests pass before merging any changes. - If possible, construct individual methods so that they can be unit tested. But only if it doesn't add too much boilerplate to the code base. - There should be no source code file larger than 1000 lines. If so, split it up into multiple. diff --git a/Magefile.go b/Magefile.go index bf8de52..6acc882 100644 --- a/Magefile.go +++ b/Magefile.go @@ -90,6 +90,8 @@ func Install() error { // printCoverage prints a warning if an existing coverage profile shows total < coverateThreshold. func printCoverage() { + // Ensure the top-level coverage profile is refreshed at least once per day. + ensureDailyCoverage(24 * time.Hour) select { case coveragePrinted <- struct{}{}: default: @@ -117,6 +119,23 @@ func printCoverage() { } } +// ensureDailyCoverage regenerates the main coverage profile when it's missing +// or older than maxAge. It writes to docs/coverage.out via the Coverage target. +func ensureDailyCoverage(maxAge time.Duration) { + const prof = "docs/coverage.out" + st, err := os.Stat(prof) + if err == nil { + age := time.Since(st.ModTime()) + if age <= maxAge { + return // fresh enough + } + } + // Missing or stale; attempt to refresh. Do not hard-fail builds if coverage fails. + if err := Coverage(); err != nil { + fmt.Println("[coverage] refresh skipped due to error:", err) + } +} + // totalCoveragePercent returns the parsed total percentage from a coverage profile using `go tool cover -func`. func totalCoveragePercent(profile string) (float64, bool) { out, err := sh.Output("go", "tool", "cover", "-func="+profile) @@ -196,16 +215,9 @@ func DevInstall() error { } // CoverCheck enforces minimum per-package coverage. -// Default threshold is 80.0; override with HEXAI_COVER_THRESH. // Exceptions: any package whose import path contains "/cmd/" and any substring // provided via HEXAI_COVER_EXCEPT (comma-separated). func CoverCheck() error { - threshold := 80.0 - if v := strings.TrimSpace(os.Getenv("HEXAI_COVER_THRESH")); v != "" { - if f, err := strconv.ParseFloat(v, 64); err == nil { - threshold = f - } - } except := []string{"/cmd/"} if v := strings.TrimSpace(os.Getenv("HEXAI_COVER_EXCEPT")); v != "" { parts := strings.Split(v, ",") @@ -256,12 +268,12 @@ func CoverCheck() error { total = 0 } all = append(all, res{pkg, total}) - if total < threshold { + if total < coverageThreshold { bad = append(bad, res{pkg, total}) } time.Sleep(10 * time.Millisecond) } - fmt.Printf("Per-package coverage (threshold %.1f%%)\n", threshold) + fmt.Printf("Per-package coverage (threshold %.1f%%)\n", coverageThreshold) for _, r := range all { fmt.Printf("- %s: %.1f%%\n", r.pkg, r.total) } @@ -270,7 +282,7 @@ func CoverCheck() error { for _, r := range bad { fmt.Printf("- %s: %.1f%%\n", r.pkg, r.total) } - return fmt.Errorf("coverage check failed (%d package(s) < %.1f%%)", len(bad), threshold) + return fmt.Errorf("coverage check failed (%d package(s) < %.1f%%)", len(bad), coverageThreshold) } fmt.Println("All packages meet coverage threshold.") return nil diff --git a/PROJECTSTATUS.md b/PROJECTSTATUS.md index b9d815f..02ed875 100644 --- a/PROJECTSTATUS.md +++ b/PROJECTSTATUS.md @@ -2,7 +2,7 @@ ## Code quality -* [/] TODO's in the code to be addressed +* [X] TODO's in the code to be addressed * [/] No more than 1000 LOC per source file * [/] No more than 50 LOC per function * [/] Each struct type in his own file @@ -13,7 +13,7 @@ ### Improvements -* [ ] Modify the LLM triggers to be more consistenc. E.g. use >>text here> or >text here> instead of semicolons? +* [X] Modify the LLM triggers to be more consistenc. E.g. use >>text here> or >text here> instead of semicolons? * [X] Include unit test coverage reports * [ ] Change inline triggers to include > to be more consistent with other triggers * [ ] Use are more stricter linter for auto-generated code (gofumpt i think is such a linter) @@ -13,12 +13,10 @@ It has got improved capabilities for Go code understanding (for example, create * Stand-alone command line tool for LLM interaction * Support for OpenAI, GitHub Copilot, and Ollama -AI coded it under human orchestration and supervision following best practices with manual code reviews. - ## Documentation * [Configuration guide](docs/configuration.md) -* [Usage examples](docs/usage-examples.md) +* [Usage examples](docs/usage.md) * [Source structure](docs/source-structure.md) ## Build and tasks @@ -29,6 +27,9 @@ Hexai uses Mage for developer tasks. Install Mage, then run targets like build, - Build binaries: `mage build` (produces `hexai` and `hexai-lsp`) - Dev build (+ tests, vet, lint): `mage dev` - Run tests: `mage test` +- Run tests with coverage: `go test ./... -cover` +- In restricted sandboxes/CI (no sockets), skip network-based tests: + - `HEXAI_TEST_SKIP_NET=1 go test ./... -cover` - Install binaries to `GOPATH/bin`: `mage install` Note: `mage lint` uses `golangci-lint`. Install via `mage devinstall` if needed. diff --git a/config.json.example b/config.json.example index d0e6ed7..7a4298c 100644 --- a/config.json.example +++ b/config.json.example @@ -8,6 +8,10 @@ "completion_throttle_ms": 0, "no_disk_io": true, "trigger_characters": [".", ":", "/", "_", " "], + "inline_open": ">", + "inline_close": ">", + "chat_suffix": ">", + "chat_prefixes": ["?", "!", ":", ";"], "coding_temperature": 0.2, "provider": "openai", diff --git a/docs/configuration.md b/docs/configuration.md index e5e7dfa..3b862af 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -21,6 +21,10 @@ The config file is optional. "completion_throttle_ms": 0, "no_disk_io": true, "trigger_characters": [".", ":", "/", "_", " " ], + "inline_open": ">", + "inline_close": ">", + "chat_suffix": ">", + "chat_prefixes": ["?", "!", ":", ";"], "coding_temperature": 0.2, "provider": "ollama", "copilot_model": "gpt-4o-mini", @@ -47,9 +51,29 @@ Key fields: - manual_invoke_min_prefix: minimum typed identifier chars required for manual invoke to proceed without structural triggers (0 allows always). - no_disk_io: avoid reading files from disk when building context. - trigger_characters: LSP completion trigger characters. +- inline_open / inline_close: characters that bracket inline prompts (default `>`/`>`). Inline prompts support `>text>` and a double-open variant `>>text>`. Single-character markers are required. +- chat_suffix / chat_prefixes: in-editor chat triggers (default suffix `>` and prefixes `["?","!",":",";"]`). A line ending with one of these prefixes immediately followed by the suffix triggers a chat reply (e.g., `What?>`). Prefixes must be single characters. - coding_temperature: optional override for LSP calls. - provider: `openai` | `copilot` | `ollama`. +### Trigger customization + +Defaults use `>` for inline prompts and chat suffix. You can change them, e.g.: + +```json +{ + "inline_open": "<", + "inline_close": ">", + "chat_suffix": "/", + "chat_prefixes": ["?", "!"], + "trigger_characters": [".", ":", "/", "_", " "] +} +``` + +Notes: +- `inline_open`/`inline_close` must be single characters; `>>text>` is the double‑open variant. +- `chat_prefixes` items must be single characters. + ## Environment overrides - All config-file options can be overridden by environment variables prefixed with `HEXAI_`. @@ -59,6 +83,8 @@ Key fields: - `HEXAI_CODING_TEMPERATURE` - `HEXAI_COMPLETION_DEBOUNCE_MS`, `HEXAI_COMPLETION_THROTTLE_MS` - `HEXAI_TRIGGER_CHARACTERS` (comma-separated, e.g., `".,:,_ , "`) + - `HEXAI_INLINE_OPEN`, `HEXAI_INLINE_CLOSE` + - `HEXAI_CHAT_SUFFIX`, `HEXAI_CHAT_PREFIXES` (comma-separated) - `HEXAI_OPENAI_MODEL`, `HEXAI_OPENAI_BASE_URL`, `HEXAI_OPENAI_TEMPERATURE` - `HEXAI_COPILOT_MODEL`, `HEXAI_COPILOT_BASE_URL`, `HEXAI_COPILOT_TEMPERATURE` - `HEXAI_OLLAMA_MODEL`, `HEXAI_OLLAMA_BASE_URL`, `HEXAI_OLLAMA_TEMPERATURE` diff --git a/docs/coverage.html b/docs/coverage.html index df02a90..d940029 100644 --- a/docs/coverage.html +++ b/docs/coverage.html @@ -59,7 +59,7 @@ <option value="file1">codeberg.org/snonux/hexai/cmd/hexai/main.go (0.0%)</option> - <option value="file2">codeberg.org/snonux/hexai/internal/appconfig/config.go (94.6%)</option> + <option value="file2">codeberg.org/snonux/hexai/internal/appconfig/config.go (86.9%)</option> <option value="file3">codeberg.org/snonux/hexai/internal/hexaicli/run.go (91.4%)</option> @@ -83,21 +83,21 @@ <option value="file13">codeberg.org/snonux/hexai/internal/lsp/document.go (90.1%)</option> - <option value="file14">codeberg.org/snonux/hexai/internal/lsp/handlers.go (91.3%)</option> + <option value="file14">codeberg.org/snonux/hexai/internal/lsp/handlers.go (90.5%)</option> <option value="file15">codeberg.org/snonux/hexai/internal/lsp/handlers_codeaction.go (81.2%)</option> - <option value="file16">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (85.1%)</option> + <option value="file16">codeberg.org/snonux/hexai/internal/lsp/handlers_completion.go (86.1%)</option> - <option value="file17">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (88.9%)</option> + <option value="file17">codeberg.org/snonux/hexai/internal/lsp/handlers_document.go (87.4%)</option> <option value="file18">codeberg.org/snonux/hexai/internal/lsp/handlers_execute.go (75.0%)</option> <option value="file19">codeberg.org/snonux/hexai/internal/lsp/handlers_init.go (55.6%)</option> - <option value="file20">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (88.1%)</option> + <option value="file20">codeberg.org/snonux/hexai/internal/lsp/handlers_utils.go (88.2%)</option> - <option value="file21">codeberg.org/snonux/hexai/internal/lsp/server.go (68.8%)</option> + <option value="file21">codeberg.org/snonux/hexai/internal/lsp/server.go (77.9%)</option> <option value="file22">codeberg.org/snonux/hexai/internal/lsp/transport.go (71.4%)</option> @@ -216,6 +216,13 @@ type App struct { TriggerCharacters []string `json:"trigger_characters"` Provider string `json:"provider"` + // Inline prompt trigger characters (default: >text> and >>text>) + InlineOpen string `json:"inline_open"` + InlineClose string `json:"inline_close"` + // In-editor chat triggers (default: suffix ">" after one of [?, !, :, ;]) + ChatSuffix string `json:"chat_suffix"` + ChatPrefixes []string `json:"chat_prefixes"` + // Provider-specific options OpenAIBaseURL string `json:"openai_base_url"` OpenAIModel string `json:"openai_model"` @@ -249,12 +256,17 @@ func newDefaultConfig() App <span class="cov5" title="9">{ ManualInvokeMinPrefix: 0, CompletionDebounceMs: 200, CompletionThrottleMs: 0, + // Inline/chat trigger defaults + InlineOpen: ">", + InlineClose: ">", + ChatSuffix: ">", + ChatPrefixes: []string{"?", "!", ":", ";"}, } }</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="8">{ +func Load(logger *log.Logger) App <span class="cov4" title="8">{ cfg := newDefaultConfig() if logger == nil </span><span class="cov3" title="3">{ return cfg // Return defaults if no logger is provided (e.g. in tests) @@ -331,12 +343,24 @@ func (a *App) mergeBasics(other *App) <span class="cov3" title="4">{ }</span> <span class="cov3" title="4">if other.CompletionDebounceMs > 0 </span><span class="cov3" title="3">{ a.CompletionDebounceMs = other.CompletionDebounceMs }</span> <span class="cov3" title="4">if other.CompletionThrottleMs > 0 </span><span class="cov3" title="3">{ a.CompletionThrottleMs = other.CompletionThrottleMs }</span> - <span class="cov3" title="4">if len(other.TriggerCharacters) > 0 </span><span class="cov3" title="3">{ - a.TriggerCharacters = slices.Clone(other.TriggerCharacters) - }</span> - <span class="cov3" title="4">if s := strings.TrimSpace(other.Provider); s != "" </span><span class="cov3" title="4">{ - a.Provider = s - }</span> + <span class="cov3" title="4">if len(other.TriggerCharacters) > 0 </span><span class="cov3" title="3">{ + a.TriggerCharacters = slices.Clone(other.TriggerCharacters) + }</span> + <span class="cov3" title="4">if s := strings.TrimSpace(other.InlineOpen); s != "" </span><span class="cov0" title="0">{ + a.InlineOpen = s + }</span> + <span class="cov3" title="4">if s := strings.TrimSpace(other.InlineClose); s != "" </span><span class="cov0" title="0">{ + a.InlineClose = s + }</span> + <span class="cov3" title="4">if s := strings.TrimSpace(other.ChatSuffix); s != "" </span><span class="cov0" title="0">{ + a.ChatSuffix = s + }</span> + <span class="cov3" title="4">if len(other.ChatPrefixes) > 0 </span><span class="cov0" title="0">{ + a.ChatPrefixes = slices.Clone(other.ChatPrefixes) + }</span> + <span class="cov3" title="4">if s := strings.TrimSpace(other.Provider); s != "" </span><span class="cov3" title="4">{ + a.Provider = s + }</span> } // mergeProviderFields merges per-provider configuration. @@ -393,7 +417,7 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="5">{ var any bool // helpers - getenv := func(k string) string </span><span class="cov10" title="100">{ return strings.TrimSpace(os.Getenv(k)) }</span> + getenv := func(k string) string </span><span class="cov10" title="120">{ return strings.TrimSpace(os.Getenv(k)) }</span> <span class="cov4" title="5">parseInt := func(k string) (int, bool) </span><span class="cov7" title="35">{ v := getenv(k) if v == "" </span><span class="cov7" title="28">{ return 0, false }</span> @@ -449,6 +473,19 @@ func loadFromEnv(logger *log.Logger) *App <span class="cov4" title="5">{ } <span class="cov1" title="1">any = true</span> } + <span class="cov4" title="5">if s := getenv("HEXAI_INLINE_OPEN"); s != "" </span><span class="cov0" title="0">{ out.InlineOpen = s; any = true }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_INLINE_CLOSE"); s != "" </span><span class="cov0" title="0">{ out.InlineClose = s; any = true }</span> + <span class="cov4" title="5">if s := getenv("HEXAI_CHAT_SUFFIX"); s != "" </span><span class="cov0" title="0">{ out.ChatSuffix = s; any = true }</span> + <span class="cov4" title="5">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">{ + if t := strings.TrimSpace(p); t != "" </span><span class="cov0" title="0">{ + out.ChatPrefixes = append(out.ChatPrefixes, t) + }</span> + } + <span class="cov0" title="0">any = true</span> + } <span class="cov4" title="5">if s := getenv("HEXAI_PROVIDER"); s != "" </span><span class="cov1" title="1">{ out.Provider = s; any = true }</span> @@ -737,6 +774,10 @@ func makeServerOptions(cfg appconfig.App, logContext bool, client llm.Client) ls ManualInvokeMinPrefix: cfg.ManualInvokeMinPrefix, CompletionDebounceMs: cfg.CompletionDebounceMs, CompletionThrottleMs: cfg.CompletionThrottleMs, + InlineOpen: cfg.InlineOpen, + InlineClose: cfg.InlineClose, + ChatSuffix: cfg.ChatSuffix, + ChatPrefixes: cfg.ChatPrefixes, } }</span> </pre> @@ -2088,9 +2129,9 @@ func (s *Server) handle(req Request) <span class="cov2" title="2">{ // Preference order on each line: strict ;text; marker (no inner spaces), then // 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="cov4" title="3">{ +func instructionFromSelection(sel string) (string, string) <span class="cov3" title="3">{ lines := splitLines(sel) - for idx, line := range lines </span><span class="cov4" title="3">{ + 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") @@ -2108,7 +2149,7 @@ func instructionFromSelection(sel string) (string, string) <span class="cov4" ti // - // text // - # text // - -- text -func findFirstInstructionInLine(line string) (instr string, cleaned string, ok bool) <span class="cov9" title="22">{ +func findFirstInstructionInLine(line string) (instr string, cleaned string, ok bool) <span class="cov8" title="22">{ type cand struct { start, end int text string @@ -2117,7 +2158,7 @@ func findFirstInstructionInLine(line string) (instr string, cleaned string, ok b if t, l, r, ok := findStrictSemicolonTag(line); ok </span><span class="cov5" title="6">{ cands = append(cands, cand{start: l, end: r, text: t}) }</span> - <span class="cov9" title="22">if i := strings.Index(line, "/*"); i >= 0 </span><span class="cov2" title="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+2:], "*/"); j >= 0 </span><span class="cov2" title="2">{ start := i end := i + 2 + j + 2 @@ -2125,7 +2166,7 @@ func findFirstInstructionInLine(line string) (instr string, cleaned string, ok b cands = append(cands, cand{start: start, end: end, text: text}) }</span> } - <span class="cov9" title="22">if i := strings.Index(line, "<!--"); i >= 0 </span><span class="cov2" title="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 @@ -2133,16 +2174,16 @@ func findFirstInstructionInLine(line string) (instr string, cleaned string, ok b cands = append(cands, cand{start: start, end: end, text: text}) }</span> } - <span class="cov9" title="22">if i := strings.Index(line, "//"); i >= 0 </span><span class="cov4" title="4">{ + <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="cov9" title="22">if i := strings.Index(line, "#"); i >= 0 </span><span class="cov2" title="2">{ + <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="cov9" title="22">if i := strings.Index(line, "--"); i >= 0 </span><span class="cov4" title="4">{ + <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="cov9" title="22">if len(cands) == 0 </span><span class="cov5" title="6">{ + <span class="cov8" title="22">if len(cands) == 0 </span><span class="cov5" title="6">{ return "", line, false }</span> // pick earliest start index @@ -2251,33 +2292,33 @@ func (s *Server) reply(id json.RawMessage, result any, err *RespError) <span cla // --- small completion cache (last ~10 entries) --- -func (s *Server) completionCacheKey(p CompletionParams, above, current, below, funcCtx string, inParams bool, hasExtra bool, extraText string) string <span class="cov7" title="11">{ +func (s *Server) completionCacheKey(p CompletionParams, above, current, below, funcCtx string, inParams bool, hasExtra bool, extraText string) string <span class="cov7" title="12">{ // Normalize left-of-cursor by trimming trailing spaces/tabs idx := p.Position.Character if idx > len(current) </span><span class="cov0" title="0">{ idx = len(current) }</span> - <span class="cov7" title="11">left := strings.TrimRight(current[:idx], " \t") + <span class="cov7" title="12">left := strings.TrimRight(current[:idx], " \t") right := "" if idx < len(current) </span><span class="cov0" title="0">{ right = current[idx:] }</span> - <span class="cov7" title="11">prov := "" + <span class="cov7" title="12">prov := "" model := "" - if s.llmClient != nil </span><span class="cov7" title="11">{ + if s.llmClient != nil </span><span class="cov7" title="12">{ prov = s.llmClient.Name() model = s.llmClient.DefaultModel() }</span> - <span class="cov7" title="11">temp := "" + <span class="cov7" title="12">temp := "" if s.codingTemperature != nil </span><span class="cov0" title="0">{ temp = fmt.Sprintf("%.3f", *s.codingTemperature) }</span> - <span class="cov7" title="11">extra := "" + <span class="cov7" title="12">extra := "" if hasExtra </span><span class="cov0" title="0">{ extra = strings.TrimSpace(extraText) }</span> // Compose a key from essential context parts - <span class="cov7" title="11">return strings.Join([]string{ + <span class="cov7" title="12">return strings.Join([]string{ "v1", // version for future-proofing prov, model, @@ -2294,11 +2335,11 @@ func (s *Server) completionCacheKey(p CompletionParams, above, current, below, f }, "\x1f")</span> // use unit separator to avoid collisions } -func (s *Server) completionCacheGet(key string) (string, bool) <span class="cov7" title="9">{ +func (s *Server) completionCacheGet(key string) (string, bool) <span class="cov6" title="10">{ s.mu.Lock() defer s.mu.Unlock() v, ok := s.compCache[key] - if !ok </span><span class="cov6" title="8">{ + if !ok </span><span class="cov6" title="9">{ return "", false }</span> // move to most-recent @@ -2306,13 +2347,13 @@ func (s *Server) completionCacheGet(key string) (string, bool) <span class="cov7 return v, true</span> } -func (s *Server) completionCachePut(key, value string) <span class="cov7" title="9">{ +func (s *Server) completionCachePut(key, value string) <span class="cov6" title="9">{ s.mu.Lock() defer s.mu.Unlock() if s.compCache == nil </span><span class="cov1" title="1">{ s.compCache = make(map[string]string) }</span> - <span class="cov7" title="9">if _, exists := s.compCache[key]; !exists </span><span class="cov7" title="9">{ + <span class="cov6" title="9">if _, exists := s.compCache[key]; !exists </span><span class="cov6" title="9">{ s.compCacheOrder = append(s.compCacheOrder, key) s.compCache[key] = value if len(s.compCacheOrder) > 10 </span><span class="cov0" title="0">{ @@ -2321,7 +2362,7 @@ func (s *Server) completionCachePut(key, value string) <span class="cov7" title= s.compCacheOrder = s.compCacheOrder[1:] delete(s.compCache, old) }</span> - <span class="cov7" title="9">return</span> + <span class="cov6" title="9">return</span> } // update existing and mark most-recent <span class="cov0" title="0">s.compCache[key] = value @@ -2348,25 +2389,26 @@ func (s *Server) compCacheTouchLocked(key string) <span class="cov1" title="1">{ // by typing one of our configured trigger characters. It checks the LSP // CompletionContext if provided and also falls back to inspecting the character // immediately to the left of the cursor. -func (s *Server) isTriggerEvent(p CompletionParams, current string) bool <span class="cov9" title="21">{ +func (s *Server) isTriggerEvent(p CompletionParams, current string) bool <span class="cov8" title="21">{ // 1) Inspect LSP completion context if present if p.Context != nil </span><span class="cov6" title="8">{ var ctx struct { TriggerKind int `json:"triggerKind"` TriggerCharacter string `json:"triggerCharacter,omitempty"` } - if raw, ok := p.Context.(json.RawMessage); ok </span><span class="cov6" title="7">{ + if raw, ok := p.Context.(json.RawMessage); ok </span><span class="cov5" title="7">{ _ = json.Unmarshal(raw, &ctx) }</span> else<span class="cov1" title="1"> { b, _ := json.Marshal(p.Context) _ = json.Unmarshal(b, &ctx) }</span> - // If the line contains a bare ';;' (no ';;text;'), do not treat as a trigger source. - <span class="cov6" title="8">if strings.Contains(current, ";;") && !hasDoubleSemicolonTrigger(current) </span><span class="cov1" title="1">{ + // If configured and the line contains a bare double-open marker (e.g., '>>' with no '>>text>'), + // do not treat as a trigger source. + <span class="cov6" title="8">if s.inlineOpen != "" && strings.Contains(current, s.inlineOpen+s.inlineOpen) && !hasDoubleSemicolonTrigger(current) </span><span class="cov0" title="0">{ return false }</span> // TriggerKind 1 = Invoked (manual). Always allow manual invoke. - <span class="cov6" title="7">if ctx.TriggerKind == 1 </span><span class="cov5" title="5">{ + <span class="cov6" title="8">if ctx.TriggerKind == 1 </span><span class="cov5" title="6">{ return true }</span> // TriggerKind 2 is TriggerCharacter per LSP spec @@ -2385,32 +2427,32 @@ func (s *Server) isTriggerEvent(p CompletionParams, current string) bool <span c // For TriggerForIncomplete (3), require manual char check below } // 2) Fallback: check the character immediately prior to cursor - <span class="cov8" title="13">idx := p.Position.Character + <span class="cov7" title="13">idx := p.Position.Character if idx <= 0 || idx > len(current) </span><span class="cov0" title="0">{ return false }</span> - // Bare ';;' should not trigger via fallback char either - <span class="cov8" title="13">if strings.Contains(current, ";;") && !hasDoubleSemicolonTrigger(current) </span><span class="cov4" title="3">{ - return false - }</span> - <span class="cov7" title="10">ch := string(current[idx-1]) - for _, c := range s.triggerChars </span><span class="cov10" title="26">{ + // Bare double-open should not trigger via fallback char either (only when configured) + <span class="cov7" title="13">if s.inlineOpen != "" && strings.Contains(current, s.inlineOpen+s.inlineOpen) && !hasDoubleSemicolonTrigger(current) </span><span class="cov1" title="1">{ + return false + }</span> + <span class="cov7" title="12">ch := string(current[idx-1]) + for _, c := range s.triggerChars </span><span class="cov10" title="34">{ if c == ch </span><span class="cov5" title="5">{ return true }</span> } - <span class="cov5" title="5">return false</span> + <span class="cov5" title="7">return f |
