summaryrefslogtreecommitdiff
path: root/internal/lsp/handlers_completion.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-03 16:00:26 +0300
committerPaul Buetow <paul@buetow.org>2025-09-03 16:00:26 +0300
commitffe9ed5531b6e62706ea555c48964ea0e560b780 (patch)
tree81974f771543827f4c0743f5f1d66f5fbd06a2bd /internal/lsp/handlers_completion.go
parent71f0d04bd558433cebf1b05845c9fa0e2957eba8 (diff)
Phase 2: add configurable completion debounce\n\n- App config: completion_debounce_ms (default 200)\n- Server: wait until no input for debounce before LLM calls\n- Applies to chat and provider-native completion paths\n- Tests: add debounce and adjust to verify behavior\n\nAll unit tests pass.
Diffstat (limited to 'internal/lsp/handlers_completion.go')
-rw-r--r--internal/lsp/handlers_completion.go86
1 files changed, 79 insertions, 7 deletions
diff --git a/internal/lsp/handlers_completion.go b/internal/lsp/handlers_completion.go
index 1c77024..576fc3d 100644
--- a/internal/lsp/handlers_completion.go
+++ b/internal/lsp/handlers_completion.go
@@ -2,13 +2,13 @@
package lsp
import (
- "context"
- "encoding/json"
- "fmt"
- "codeberg.org/snonux/hexai/internal/llm"
- "codeberg.org/snonux/hexai/internal/logging"
- "strings"
- "time"
+ "context"
+ "encoding/json"
+ "fmt"
+ "codeberg.org/snonux/hexai/internal/llm"
+ "codeberg.org/snonux/hexai/internal/logging"
+ "strings"
+ "time"
)
func (s *Server) handleCompletion(req Request) {
@@ -120,6 +120,11 @@ func (s *Server) tryLLMCompletion(p CompletionParams, above, current, below, fun
if s.codingTemperature != nil {
opts = append(opts, llm.WithTemperature(*s.codingTemperature))
}
+ // Debounce and throttle before making the LLM call
+ s.waitForDebounce(ctx)
+ if !s.waitForThrottle(ctx) {
+ return nil, false
+ }
logging.Logf("lsp ", "completion llm=requesting model=%s", s.llmClient.DefaultModel())
text, err := s.llmClient.Chat(ctx, messages, opts...)
@@ -226,6 +231,11 @@ func (s *Server) tryProviderNativeCompletion(current string, p CompletionParams,
ctx2, cancel2 := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel2()
+ // Debounce and throttle prior to provider-native call
+ s.waitForDebounce(ctx2)
+ if !s.waitForThrottle(ctx2) {
+ return nil, false
+ }
suggestions, err := cc.CodeCompletion(ctx2, prompt, after, 1, lang, temp)
if err == nil && len(suggestions) > 0 {
cleaned := strings.TrimSpace(suggestions[0])
@@ -252,6 +262,68 @@ func (s *Server) tryProviderNativeCompletion(current string, p CompletionParams,
return nil, false
}
+// waitForDebounce sleeps until there has been no input activity for at least
+// completionDebounce. If debounce is zero or ctx is done, it returns promptly.
+func (s *Server) waitForDebounce(ctx context.Context) {
+ d := s.completionDebounce
+ if d <= 0 {
+ return
+ }
+ for {
+ s.mu.RLock()
+ last := s.lastInput
+ s.mu.RUnlock()
+ if last.IsZero() {
+ return
+ }
+ since := time.Since(last)
+ if since >= d {
+ return
+ }
+ rem := d - since
+ timer := time.NewTimer(rem)
+ select {
+ case <-ctx.Done():
+ timer.Stop()
+ return
+ case <-timer.C:
+ // loop and re-evaluate in case input occurred during sleep
+ }
+ }
+}
+
+// waitForThrottle enforces a minimum spacing between LLM calls. Returns false
+// if the context is canceled while waiting.
+func (s *Server) waitForThrottle(ctx context.Context) bool {
+ interval := s.throttleInterval
+ if interval <= 0 {
+ return true
+ }
+ var wait time.Duration
+ for {
+ s.mu.Lock()
+ next := s.lastLLMCall.Add(interval)
+ now := time.Now()
+ if now.Before(next) {
+ wait = next.Sub(now)
+ s.mu.Unlock()
+ timer := time.NewTimer(wait)
+ select {
+ case <-ctx.Done():
+ timer.Stop()
+ return false
+ case <-timer.C:
+ // try again to set the next call time
+ continue
+ }
+ }
+ // we are allowed to proceed now; record this call as the latest
+ s.lastLLMCall = now
+ s.mu.Unlock()
+ return true
+ }
+}
+
// 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 {
sysPrompt, userPrompt := buildPrompts(inParams, p, above, current, below, funcCtx)