summaryrefslogtreecommitdiff
path: root/internal/lsp/handlers_completion.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/lsp/handlers_completion.go')
-rw-r--r--internal/lsp/handlers_completion.go196
1 files changed, 119 insertions, 77 deletions
diff --git a/internal/lsp/handlers_completion.go b/internal/lsp/handlers_completion.go
index d115741..237d34d 100644
--- a/internal/lsp/handlers_completion.go
+++ b/internal/lsp/handlers_completion.go
@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"strings"
+ "sync"
"time"
"codeberg.org/snonux/hexai/internal/llm"
@@ -94,17 +95,48 @@ func (s *Server) tryLLMCompletion(p CompletionParams, above, current, below, fun
if handled {
return items, true
}
-
- spec := s.buildRequestSpec(surfaceCompletion)
- client := s.clientFor(spec)
- if client == nil {
+ specs := s.buildRequestSpecs(surfaceCompletion)
+ if len(specs) == 0 {
return nil, false
}
- if items, ok := s.tryProviderNativeCompletion(spec, client, current, p, above, below, funcCtx, docStr, hasExtra, extraText, plan.inParams); ok {
- return items, true
+ type jobResult struct {
+ items []CompletionItem
+ ok bool
}
-
- return s.executeChatCompletion(ctx, plan, spec, client)
+ results := make([]jobResult, len(specs))
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ s.waitForDebounce(ctx)
+ if !s.waitForThrottle(ctx) {
+ return nil, false
+ }
+ for _, spec := range specs {
+ spec := spec
+ client := s.clientFor(spec)
+ if client == nil {
+ continue
+ }
+ wg.Add(1)
+ go func(idx int, spec requestSpec, client llm.Client) {
+ defer wg.Done()
+ items, ok := s.runCompletionForSpec(ctx, plan, spec, client)
+ mu.Lock()
+ results[idx] = jobResult{items: items, ok: ok}
+ mu.Unlock()
+ }(spec.index, spec, client)
+ }
+ wg.Wait()
+ accumulated := make([]CompletionItem, 0)
+ for _, res := range results {
+ if !res.ok {
+ continue
+ }
+ accumulated = append(accumulated, res.items...)
+ }
+ if len(accumulated) == 0 {
+ return nil, false
+ }
+ return accumulated, true
}
func (s *Server) prepareCompletionPlan(p CompletionParams, above, current, below, funcCtx, docStr string, hasExtra bool, extraText string) (completionPlan, []CompletionItem, bool) {
@@ -130,12 +162,6 @@ func (s *Server) prepareCompletionPlan(p CompletionParams, above, current, below
plan.inParams = inParamList(current, p.Position.Character)
plan.manualInvoke = parseManualInvoke(p.Context)
plan.cacheKey = s.completionCacheKey(p, above, current, below, funcCtx, plan.inParams, hasExtra, extraText)
- if cleaned, ok := s.completionCacheGet(plan.cacheKey); ok && strings.TrimSpace(cleaned) != "" {
- logging.Logf("lsp ", "completion cache hit uri=%s line=%d char=%d preview=%s%s%s",
- p.TextDocument.URI, p.Position.Line, p.Position.Character,
- logging.AnsiGreen, logging.PreviewForLog(cleaned), logging.AnsiBase)
- return plan, s.makeCompletionItems(cleaned, plan.inParams, current, p, docStr), true
- }
if isBareDoubleOpen(current, openChar, closeChar) || isBareDoubleOpen(below, openChar, closeChar) {
logging.Logf("lsp ", "%scompletion skip=empty-double-semicolon line=%d char=%d current=%q%s", logging.AnsiYellow, p.Position.Line, p.Position.Character, trimLen(current), logging.AnsiBase)
return plan, []CompletionItem{}, true
@@ -147,38 +173,58 @@ func (s *Server) prepareCompletionPlan(p CompletionParams, above, current, below
return plan, nil, false
}
-func (s *Server) executeChatCompletion(ctx context.Context, plan completionPlan, spec requestSpec, client llm.Client) ([]CompletionItem, bool) {
+func (s *Server) runCompletionForSpec(ctx context.Context, plan completionPlan, spec requestSpec, client llm.Client) ([]CompletionItem, bool) {
+ sortPrefix := fmt.Sprintf("%04d", spec.index)
+ modelKey := spec.effectiveModel(client.DefaultModel())
+ providerKey := spec.provider
+ if providerKey == "" {
+ providerKey = canonicalProvider(client.Name())
+ }
+ cacheKey := plan.cacheKey + "|" + providerKey + ":" + modelKey
+ if cached, ok := s.completionCacheGet(cacheKey); ok && strings.TrimSpace(cached) != "" {
+ logging.Logf("lsp ", "completion cache hit uri=%s line=%d char=%d preview=%s%s%s",
+ plan.params.TextDocument.URI, plan.params.Position.Line, plan.params.Position.Character,
+ logging.AnsiGreen, logging.PreviewForLog(cached), logging.AnsiBase)
+ detail := fmt.Sprintf("Hexai %s:%s", client.Name(), modelKey)
+ items := s.makeCompletionItems(cached, plan.inParams, plan.current, plan.params, plan.docStr, detail, sortPrefix)
+ return items, true
+ }
+ if items, ok := s.tryProviderNativeCompletion(ctx, plan, spec, client, sortPrefix); ok {
+ return items, true
+ }
+ return s.executeChatCompletion(ctx, plan, spec, client, sortPrefix)
+}
+
+func (s *Server) executeChatCompletion(ctx context.Context, plan completionPlan, spec requestSpec, client llm.Client, sortPrefix string) ([]CompletionItem, bool) {
messages := s.buildCompletionMessages(plan.inlinePrompt, plan.hasExtra, plan.extraText, plan.inParams, plan.params, plan.above, plan.current, plan.below, plan.funcCtx)
sentSize := 0
for _, m := range messages {
sentSize += len(m.Content)
}
s.incSentCounters(sentSize)
- opts := spec.options
- s.waitForDebounce(ctx)
- if !s.waitForThrottle(ctx) {
- return nil, false
- }
- modelUsed := spec.effectiveModel()
- if strings.TrimSpace(modelUsed) == "" {
- modelUsed = client.DefaultModel()
- }
- logging.Logf("lsp ", "completion llm=requesting model=%s", modelUsed)
- text, err := client.Chat(ctx, messages, opts...)
+ text, err := client.Chat(ctx, messages, spec.options...)
if err != nil {
logging.Logf("lsp ", "llm completion error: %v", err)
- s.logLLMStats(modelUsed)
+ s.logLLMStats("")
return nil, false
}
s.incRecvCounters(len(text))
+ modelUsed := spec.effectiveModel(client.DefaultModel())
+ _ = stats.Update(ctx, client.Name(), modelUsed, sentSize, len(text))
s.logLLMStats(modelUsed)
trimmed := strings.TrimSpace(text)
cleaned := s.postProcessCompletion(trimmed, plan.current[:plan.params.Position.Character], plan.current)
if cleaned == "" {
return nil, false
}
- s.completionCachePut(plan.cacheKey, cleaned)
- items := s.makeCompletionItems(cleaned, plan.inParams, plan.current, plan.params, plan.docStr)
+ detail := fmt.Sprintf("Hexai %s:%s", client.Name(), modelUsed)
+ providerKey := spec.provider
+ if providerKey == "" {
+ providerKey = canonicalProvider(client.Name())
+ }
+ cacheKey := plan.cacheKey + "|" + providerKey + ":" + modelUsed
+ s.completionCachePut(cacheKey, cleaned)
+ items := s.makeCompletionItems(cleaned, plan.inParams, plan.current, plan.params, plan.docStr, detail, sortPrefix)
return items, true
}
@@ -260,79 +306,75 @@ func (s *Server) prefixHeuristicAllows(inlinePrompt bool, current string, p Comp
}
// tryProviderNativeCompletion attempts provider-native completion and returns items when successful.
-func (s *Server) tryProviderNativeCompletion(spec requestSpec, client llm.Client, current string, p CompletionParams, above, below, funcCtx, docStr string, hasExtra bool, extraText string, inParams bool) ([]CompletionItem, bool) {
+func (s *Server) tryProviderNativeCompletion(ctx context.Context, plan completionPlan, spec requestSpec, client llm.Client, sortPrefix string) ([]CompletionItem, bool) {
cc, ok := client.(llm.CodeCompleter)
if !ok {
return nil, false
}
+ current := plan.current
+ p := plan.params
before, after := s.docBeforeAfter(p.TextDocument.URI, p.Position)
path := strings.TrimPrefix(p.TextDocument.URI, "file://")
- // Build provider-native prompt from template
cfg := s.currentConfig()
_, _, openChar, closeChar := s.inlineMarkers()
prompt := renderTemplate(cfg.PromptNativeCompletion, map[string]string{
"path": path,
"before": before,
})
- lang := ""
provider := spec.provider
if provider == "" {
provider = canonicalProvider(cfg.Provider)
}
logging.Logf("lsp ", "completion path=codex provider=%s uri=%s", provider, path)
- ctx2, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)
+ ctx2, cancel2 := context.WithTimeout(ctx, 15*time.Second)
defer cancel2()
-
- // Debounce and throttle prior to provider-native call
- s.waitForDebounce(ctx2)
- if !s.waitForThrottle(ctx2) {
- return nil, false
- }
- // Count approximate payload sizes: prompt+after sent; first suggestion received
sentBytes := len(prompt) + len(after)
- modelUsed := spec.effectiveModel()
- if strings.TrimSpace(modelUsed) == "" {
- modelUsed = client.DefaultModel()
- }
+ modelUsed := spec.effectiveModel(client.DefaultModel())
tempVal := 0.0
- if val, ok := chooseSurfaceTemperature(surfaceCompletion, cfg, provider, spec.modelOverride, spec.fallbackModel); ok {
+ if val, ok := chooseSurfaceTemperature(surfaceCompletion, cfg, spec.entry, provider, modelUsed); ok {
tempVal = val
}
- suggestions, err := cc.CodeCompletion(ctx2, prompt, after, 1, lang, tempVal)
- if err == nil && len(suggestions) > 0 {
- // Update counters and heartbeat
- s.incSentCounters(sentBytes)
- s.incRecvCounters(len(suggestions[0]))
- // Contribute to global stats (provider-native path)
- if client != nil {
- _ = stats.Update(ctx2, client.Name(), modelUsed, sentBytes, len(suggestions[0]))
+ suggestions, err := cc.CodeCompletion(ctx2, prompt, after, 1, "", tempVal)
+ if err != nil || len(suggestions) == 0 {
+ if err != nil {
+ logging.Logf("lsp ", "completion path=codex error=%v (falling back)", err)
}
- s.logLLMStats(modelUsed)
- cleaned := strings.TrimSpace(suggestions[0])
- if cleaned != "" {
- cleaned = stripDuplicateAssignmentPrefix(current[:p.Position.Character], cleaned)
- if cleaned != "" {
- cleaned = stripDuplicateGeneralPrefix(current[:p.Position.Character], cleaned)
- }
- if cleaned != "" && hasDoubleOpenTrigger(current, openChar, closeChar) {
- indent := leadingIndent(current)
- if indent != "" {
- cleaned = applyIndent(indent, cleaned)
- }
- }
- if strings.TrimSpace(cleaned) != "" {
- key := s.completionCacheKey(p, above, current, below, funcCtx, inParams, hasExtra, extraText)
- s.completionCachePut(key, cleaned)
- return s.makeCompletionItems(cleaned, inParams, current, p, docStr), true
- }
+ return nil, false
+ }
+ s.incSentCounters(sentBytes)
+ s.incRecvCounters(len(suggestions[0]))
+ _ = stats.Update(ctx2, client.Name(), modelUsed, sentBytes, len(suggestions[0]))
+ s.logLLMStats(modelUsed)
+ cleaned := strings.TrimSpace(suggestions[0])
+ if cleaned == "" {
+ return nil, false
+ }
+ cleaned = stripDuplicateAssignmentPrefix(current[:p.Position.Character], cleaned)
+ if cleaned == "" {
+ return nil, false
+ }
+ cleaned = stripDuplicateGeneralPrefix(current[:p.Position.Character], cleaned)
+ if cleaned == "" {
+ return nil, false
+ }
+ if strings.TrimSpace(cleaned) != "" && hasDoubleOpenTrigger(current, openChar, closeChar) {
+ indent := leadingIndent(current)
+ if indent != "" {
+ cleaned = applyIndent(indent, cleaned)
}
- } else if err != nil {
- logging.Logf("lsp ", "completion path=codex error=%v (falling back to chat)", err)
- // Still emit a heartbeat for visibility, even on error
- s.incSentCounters(sentBytes)
- s.logLLMStats(modelUsed)
}
- return nil, false
+ if strings.TrimSpace(cleaned) == "" {
+ return nil, false
+ }
+ detail := fmt.Sprintf("Hexai %s:%s", client.Name(), modelUsed)
+ providerKey := provider
+ if providerKey == "" {
+ providerKey = canonicalProvider(client.Name())
+ }
+ cacheKey := plan.cacheKey + "|" + providerKey + ":" + modelUsed
+ s.completionCachePut(cacheKey, cleaned)
+ items := s.makeCompletionItems(cleaned, plan.inParams, current, p, plan.docStr, detail, sortPrefix)
+ return items, true
}
// waitForDebounce sleeps until there has been no input activity for at least