diff options
Diffstat (limited to 'internal/lsp/handlers_completion.go')
| -rw-r--r-- | internal/lsp/handlers_completion.go | 98 |
1 files changed, 65 insertions, 33 deletions
diff --git a/internal/lsp/handlers_completion.go b/internal/lsp/handlers_completion.go index 6142a30..df541cc 100644 --- a/internal/lsp/handlers_completion.go +++ b/internal/lsp/handlers_completion.go @@ -13,6 +13,21 @@ import ( "codeberg.org/snonux/hexai/internal/stats" ) +type completionPlan struct { + params CompletionParams + above string + current string + below string + funcCtx string + docStr string + hasExtra bool + extraText string + inlinePrompt bool + inParams bool + manualInvoke bool + cacheKey string +} + func (s *Server) handleCompletion(req Request) { var p CompletionParams var docStr string @@ -75,44 +90,59 @@ func (s *Server) tryLLMCompletion(p CompletionParams, above, current, below, fun ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) defer cancel() - inlinePrompt := lineHasInlinePrompt(current) - if !inlinePrompt && !s.isTriggerEvent(p, current) { - logging.Logf("lsp ", "%scompletion skip=no-trigger line=%d char=%d current=%q%s", logging.AnsiYellow, p.Position.Line, p.Position.Character, trimLen(current), logging.AnsiBase) - return []CompletionItem{}, true + plan, items, handled := s.prepareCompletionPlan(p, above, current, below, funcCtx, docStr, hasExtra, extraText) + if handled { + return items, true } - if s.shouldSuppressForChatTriggerEOL(current, p) { - return []CompletionItem{}, true + + if items, ok := s.tryProviderNativeCompletion(current, p, above, below, funcCtx, docStr, hasExtra, extraText, plan.inParams); ok { + return items, true } - inParams := inParamList(current, p.Position.Character) - manualInvoke := parseManualInvoke(p.Context) + return s.executeChatCompletion(ctx, plan) +} - // Cache fast-path - key := s.completionCacheKey(p, above, current, below, funcCtx, inParams, hasExtra, extraText) - if cleaned, ok := s.completionCacheGet(key); ok && strings.TrimSpace(cleaned) != "" { +func (s *Server) prepareCompletionPlan(p CompletionParams, above, current, below, funcCtx, docStr string, hasExtra bool, extraText string) (completionPlan, []CompletionItem, bool) { + plan := completionPlan{ + params: p, + above: above, + current: current, + below: below, + funcCtx: funcCtx, + docStr: docStr, + hasExtra: hasExtra, + extraText: extraText, + } + plan.inlinePrompt = lineHasInlinePrompt(current, s.inlineOpenChar, s.inlineCloseChar) + if !plan.inlinePrompt && !s.isTriggerEvent(p, current) { + logging.Logf("lsp ", "%scompletion skip=no-trigger line=%d char=%d current=%q%s", logging.AnsiYellow, p.Position.Line, p.Position.Character, trimLen(current), logging.AnsiBase) + return plan, []CompletionItem{}, true + } + if s.shouldSuppressForChatTriggerEOL(current, p) { + return plan, []CompletionItem{}, true + } + 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 s.makeCompletionItems(cleaned, inParams, current, p, docStr), true + return plan, s.makeCompletionItems(cleaned, plan.inParams, current, p, docStr), true } - if isBareDoubleOpen(current) || isBareDoubleOpen(below) { + if isBareDoubleOpen(current, s.inlineOpenChar, s.inlineCloseChar) || isBareDoubleOpen(below, s.inlineOpenChar, s.inlineCloseChar) { 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 []CompletionItem{}, true + return plan, []CompletionItem{}, true } - - if !inParams && !s.prefixHeuristicAllows(inlinePrompt, current, p, manualInvoke) { + if !plan.inParams && !s.prefixHeuristicAllows(plan.inlinePrompt, current, p, plan.manualInvoke) { logging.Logf("lsp ", "%scompletion skip=short-prefix line=%d char=%d current=%q%s", logging.AnsiYellow, p.Position.Line, p.Position.Character, trimLen(current), logging.AnsiBase) - return []CompletionItem{}, true - } - - // Provider-native path - if items, ok := s.tryProviderNativeCompletion(current, p, above, below, funcCtx, docStr, hasExtra, extraText, inParams); ok { - return items, true + return plan, []CompletionItem{}, true } + return plan, nil, false +} - // Chat path - messages := s.buildCompletionMessages(inlinePrompt, hasExtra, extraText, inParams, p, above, current, below, funcCtx) - // Counters and options +func (s *Server) executeChatCompletion(ctx context.Context, plan completionPlan) ([]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) @@ -122,13 +152,14 @@ 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 } + if s.llmClient == nil { + return nil, false + } logging.Logf("lsp ", "completion llm=requesting model=%s", s.llmClient.DefaultModel()) - text, err := s.llmClient.Chat(ctx, messages, opts...) if err != nil { logging.Logf("lsp ", "llm completion error: %v", err) @@ -137,13 +168,14 @@ func (s *Server) tryLLMCompletion(p CompletionParams, above, current, below, fun } s.incRecvCounters(len(text)) s.logLLMStats() - - cleaned := s.postProcessCompletion(strings.TrimSpace(text), current[:p.Position.Character], current) + trimmed := strings.TrimSpace(text) + cleaned := s.postProcessCompletion(trimmed, plan.current[:plan.params.Position.Character], plan.current) if cleaned == "" { return nil, false } - s.completionCachePut(key, cleaned) - return s.makeCompletionItems(cleaned, inParams, current, p, docStr), true + s.completionCachePut(plan.cacheKey, cleaned) + items := s.makeCompletionItems(cleaned, plan.inParams, plan.current, plan.params, plan.docStr) + return items, true } // parseManualInvoke inspects the LSP completion context and reports whether the user manually invoked completion. @@ -269,7 +301,7 @@ func (s *Server) tryProviderNativeCompletion(current string, p CompletionParams, if cleaned != "" { cleaned = stripDuplicateGeneralPrefix(current[:p.Position.Character], cleaned) } - if cleaned != "" && hasDoubleOpenTrigger(current) { + if cleaned != "" && hasDoubleOpenTrigger(current, s.inlineOpenChar, s.inlineCloseChar) { indent := leadingIndent(current) if indent != "" { cleaned = applyIndent(indent, cleaned) @@ -398,7 +430,7 @@ func (s *Server) postProcessCompletion(text string, leftOfCursor string, current if cleaned != "" { cleaned = stripDuplicateGeneralPrefix(leftOfCursor, cleaned) } - if cleaned != "" && hasDoubleOpenTrigger(currentLine) { + if cleaned != "" && hasDoubleOpenTrigger(currentLine, s.inlineOpenChar, s.inlineCloseChar) { if indent := leadingIndent(currentLine); indent != "" { cleaned = applyIndent(indent, cleaned) } |
