summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-06 10:25:36 +0300
committerPaul Buetow <paul@buetow.org>2025-09-06 10:25:36 +0300
commit5be9532cfa630f4aacd8d879c3e4f5cc316da0fa (patch)
tree0a901680fccd1e2703ffdbd9284ccff932be1d67
parent70f1d0e78c57dfa5beae779b3d392b6e6fa44c14 (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
-rw-r--r--AGENTS.md2
-rw-r--r--Magefile.go32
-rw-r--r--PROJECTSTATUS.md4
-rw-r--r--README.md7
-rw-r--r--config.json.example4
-rw-r--r--docs/configuration.md26
-rw-r--r--docs/coverage.html647
-rw-r--r--docs/coverage.out6918
-rw-r--r--docs/usage.md (renamed from docs/usage-examples.md)10
-rw-r--r--internal/appconfig/config.go49
-rw-r--r--internal/hexailsp/run.go4
-rw-r--r--internal/llm/copilot_http_test.go5
-rw-r--r--internal/llm/ollama_test.go8
-rw-r--r--internal/llm/openai_http_test.go8
-rw-r--r--internal/llm/openai_sse_negative_test.go3
-rw-r--r--internal/lsp/codeaction_test.go2
-rw-r--r--internal/lsp/completion_prefix_strip_test.go102
-rw-r--r--internal/lsp/debounce_throttle_more_test.go36
-rw-r--r--internal/lsp/document_test.go27
-rw-r--r--internal/lsp/handlers.go15
-rw-r--r--internal/lsp/handlers_completion.go51
-rw-r--r--internal/lsp/handlers_document.go79
-rw-r--r--internal/lsp/handlers_end_to_end_test.go4
-rw-r--r--internal/lsp/handlers_helpers_test.go56
-rw-r--r--internal/lsp/handlers_test.go78
-rw-r--r--internal/lsp/handlers_utils.go259
-rw-r--r--internal/lsp/helpers_inline_prompt_test.go58
-rw-r--r--internal/lsp/helpers_more_test.go22
-rw-r--r--internal/lsp/init_and_trigger_test.go5
-rw-r--r--internal/lsp/instruction_table_test.go3
-rw-r--r--internal/lsp/llm_stats_test.go11
-rw-r--r--internal/lsp/postprocess_indent_test.go5
-rw-r--r--internal/lsp/provider_native_success_test.go21
-rw-r--r--internal/lsp/server.go40
-rw-r--r--internal/lsp/transport_test.go15
-rw-r--r--internal/lsp/triggers_config_test.go74
36 files changed, 4758 insertions, 3932 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 5c1bbd1..fe3f8ca 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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)
diff --git a/README.md b/README.md
index cc15ba4..f9b9864 100644
--- a/README.md
+++ b/README.md
@@ -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: &gt;text&gt; and &gt;&gt;text&gt;)
+ InlineOpen string `json:"inline_open"`
+ InlineClose string `json:"inline_close"`
+ // In-editor chat triggers (default: suffix "&gt;" 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: "&gt;",
+ InlineClose: "&gt;",
+ ChatSuffix: "&gt;",
+ 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 &gt; 0 </span><span class="cov3" title="3">{ a.CompletionDebounceMs = other.CompletionDebounceMs }</span>
<span class="cov3" title="4">if other.CompletionThrottleMs &gt; 0 </span><span class="cov3" title="3">{ a.CompletionThrottleMs = other.CompletionThrottleMs }</span>
- <span class="cov3" title="4">if len(other.TriggerCharacters) &gt; 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) &gt; 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) &gt; 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 &amp;&amp; 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 &gt;= 0 </span><span class="cov2" title="2">{
+ <span class="cov8" title="22">if i := strings.Index(line, "/*"); i &gt;= 0 </span><span class="cov2" title="2">{
if j := strings.Index(line[i+2:], "*/"); j &gt;= 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, "&lt;!--"); i &gt;= 0 </span><span class="cov2" title="2">{
+ <span class="cov8" title="22">if i := strings.Index(line, "&lt;!--"); i &gt;= 0 </span><span class="cov2" title="2">{
if j := strings.Index(line[i+4:], "--&gt;"); j &gt;= 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 &gt;= 0 </span><span class="cov4" title="4">{
+ <span class="cov8" title="22">if i := strings.Index(line, "//"); i &gt;= 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 &gt;= 0 </span><span class="cov2" title="2">{
+ <span class="cov8" title="22">if i := strings.Index(line, "#"); i &gt;= 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 &gt;= 0 </span><span class="cov4" title="4">{
+ <span class="cov8" title="22">if i := strings.Index(line, "--"); i &gt;= 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 &gt; 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 &lt; 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) &gt; 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, &amp;ctx)
}</span> else<span class="cov1" title="1"> {
b, _ := json.Marshal(p.Context)
_ = json.Unmarshal(b, &amp;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, ";;") &amp;&amp; !hasDoubleSemicolonTrigger(current) </span><span class="cov1" title="1">{
+ // If configured and the line contains a bare double-open marker (e.g., '&gt;&gt;' with no '&gt;&gt;text&gt;'),
+ // do not treat as a trigger source.
+ <span class="cov6" title="8">if s.inlineOpen != "" &amp;&amp; strings.Contains(current, s.inlineOpen+s.inlineOpen) &amp;&amp; !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 &lt;= 0 || idx &gt; len(current) </span><span class="cov0" title="0">{
return false
}</span>