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.go90
1 files changed, 53 insertions, 37 deletions
diff --git a/internal/lsp/handlers_completion.go b/internal/lsp/handlers_completion.go
index e6d8951..fced629 100644
--- a/internal/lsp/handlers_completion.go
+++ b/internal/lsp/handlers_completion.go
@@ -1,4 +1,7 @@
-// Completion handlers split from handlers.go to reduce file size and isolate feature logic.
+// Completion request handling for the LSP server. These are methods on
+// completionService (the extracted code-completion subsystem), which owns the
+// cache/throttle state and reaches back into Server (via cs.srv, aliased to s)
+// for shared infrastructure such as LLM clients, document access and stats.
package lsp
import (
@@ -35,7 +38,8 @@ type completionJobResult struct {
ok bool
}
-func (s *Server) handleCompletion(req Request) {
+func (cs *completionService) handleCompletion(req Request) {
+ s := cs.srv
if s.completionDisabled() {
s.reply(req.ID, CompletionList{IsIncomplete: false, Items: nil}, nil)
return
@@ -60,14 +64,14 @@ func (s *Server) handleCompletion(req Request) {
logging.Logf("lsp ", "completion trigger kind=%d char=%q uri=%s line=%d char=%d",
tk, tch, p.TextDocument.URI, p.Position.Line, p.Position.Character)
above, current, below, funcCtx := s.lineContext(p.TextDocument.URI, p.Position)
- docStr = s.buildDocString(p, above, current, below, funcCtx)
+ docStr = cs.buildDocString(p, above, current, below, funcCtx)
if s.logContext {
- s.logCompletionContext(p, above, current, below, funcCtx)
+ cs.logCompletionContext(p, above, current, below, funcCtx)
}
if s.currentLLMClient() != nil {
newFunc := s.isDefiningNewFunction(p.TextDocument.URI, p.Position)
extra, has := s.buildAdditionalContext(newFunc, p.TextDocument.URI, p.Position)
- items, ok, incomplete := s.tryLLMCompletion(p, above, current, below, funcCtx, docStr, has, extra)
+ items, ok, incomplete := cs.tryLLMCompletion(p, above, current, below, funcCtx, docStr, has, extra)
if ok {
s.reply(req.ID, CompletionList{IsIncomplete: incomplete, Items: items}, nil)
return
@@ -105,22 +109,23 @@ func extractTriggerInfo(p CompletionParams) (kind int, ch string) {
// --- completion helpers ---
-func (s *Server) buildDocString(p CompletionParams, above, current, below, funcCtx string) string {
+func (cs *completionService) buildDocString(p CompletionParams, above, current, below, funcCtx string) string {
return fmt.Sprintf("file: %s\nline: %d\nabove: %s\ncurrent: %s\nbelow: %s\nfunction: %s",
p.TextDocument.URI, p.Position.Line, trimLen(above), trimLen(current), trimLen(below), trimLen(funcCtx))
}
-func (s *Server) logCompletionContext(p CompletionParams, above, current, below, funcCtx string) {
+func (cs *completionService) logCompletionContext(p CompletionParams, above, current, below, funcCtx string) {
logging.Logf("lsp ", "completion ctx uri=%s line=%d char=%d above=%q current=%q below=%q function=%q",
p.TextDocument.URI, p.Position.Line, p.Position.Character, trimLen(above), trimLen(current), trimLen(below), trimLen(funcCtx))
}
-func (s *Server) tryLLMCompletion(p CompletionParams, above, current, below, funcCtx, docStr string, hasExtra bool, extraText string) ([]CompletionItem, bool, bool) {
+func (cs *completionService) tryLLMCompletion(p CompletionParams, above, current, below, funcCtx, docStr string, hasExtra bool, extraText string) ([]CompletionItem, bool, bool) {
+ s := cs.srv
ctx, cancel := s.requestTimeoutContext(12 * time.Second)
var cancelOnce sync.Once
end := func() { cancelOnce.Do(cancel) }
- plan, items, handled := s.prepareCompletionPlan(p, above, current, below, funcCtx, docStr, hasExtra, extraText)
+ plan, items, handled := cs.prepareCompletionPlan(p, above, current, below, funcCtx, docStr, hasExtra, extraText)
if handled {
end()
return items, true, false
@@ -130,7 +135,7 @@ func (s *Server) tryLLMCompletion(p CompletionParams, above, current, below, fun
end()
return nil, false, false
}
- results, started, ok := s.startCompletionJobs(ctx, plan, specs)
+ results, started, ok := cs.startCompletionJobs(ctx, plan, specs)
if !ok || started == 0 {
end()
return nil, false, false
@@ -155,7 +160,7 @@ func (s *Server) tryLLMCompletion(p CompletionParams, above, current, below, fun
return combined, true, false
}
- firstItems, ok := s.firstCompletionAndStore(results, plan.cacheKey, end)
+ firstItems, ok := cs.firstCompletionAndStore(results, plan.cacheKey, end)
if !ok {
end()
return nil, false, false
@@ -163,9 +168,10 @@ func (s *Server) tryLLMCompletion(p CompletionParams, above, current, below, fun
return firstItems, true, true
}
-func (s *Server) startCompletionJobs(ctx context.Context, plan completionPlan, specs []requestSpec) (<-chan completionJobResult, int, bool) {
+func (cs *completionService) startCompletionJobs(ctx context.Context, plan completionPlan, specs []requestSpec) (<-chan completionJobResult, int, bool) {
+ s := cs.srv
results := make(chan completionJobResult, len(specs))
- s.waitForDebounce(ctx)
+ cs.waitForDebounce(ctx)
if !s.waitForThrottle(ctx) {
close(results)
return results, 0, false
@@ -182,7 +188,7 @@ func (s *Server) startCompletionJobs(ctx context.Context, plan completionPlan, s
wg.Add(1)
go func(spec requestSpec, client llm.Client) {
defer wg.Done()
- items, ok := s.runCompletionForSpec(ctx, plan, spec, client)
+ items, ok := cs.runCompletionForSpec(ctx, plan, spec, client)
results <- completionJobResult{items: items, ok: ok}
}(spec, client)
}
@@ -216,14 +222,15 @@ func collectCompletionResults(results <-chan completionJobResult) []CompletionIt
return combined
}
-func (s *Server) firstCompletionAndStore(results <-chan completionJobResult, cacheKey string, end func()) ([]CompletionItem, bool) {
+func (cs *completionService) firstCompletionAndStore(results <-chan completionJobResult, cacheKey string, end func()) ([]CompletionItem, bool) {
+ s := cs.srv
firstCh := make(chan []CompletionItem, 1)
// Track this goroutine in inflight so Run's deferred Wait() catches it
// and prevents use-after-close writes on shutdown.
s.inflight.Add(1)
go func() {
defer s.inflight.Done()
- s.collectFirstCompletion(results, cacheKey, firstCh, end)
+ cs.collectFirstCompletion(results, cacheKey, firstCh, end)
}()
firstItems, ok := <-firstCh
if !ok || len(firstItems) == 0 {
@@ -232,7 +239,8 @@ func (s *Server) firstCompletionAndStore(results <-chan completionJobResult, cac
return firstItems, true
}
-func (s *Server) collectFirstCompletion(results <-chan completionJobResult, cacheKey string, firstCh chan<- []CompletionItem, end func()) {
+func (cs *completionService) collectFirstCompletion(results <-chan completionJobResult, cacheKey string, firstCh chan<- []CompletionItem, end func()) {
+ s := cs.srv
defer end()
combined := make([]CompletionItem, 0)
firstSent := false
@@ -254,7 +262,8 @@ func (s *Server) collectFirstCompletion(results <-chan completionJobResult, cach
close(firstCh)
}
-func (s *Server) prepareCompletionPlan(p CompletionParams, above, current, below, funcCtx, docStr string, hasExtra bool, extraText string) (completionPlan, []CompletionItem, bool) {
+func (cs *completionService) prepareCompletionPlan(p CompletionParams, above, current, below, funcCtx, docStr string, hasExtra bool, extraText string) (completionPlan, []CompletionItem, bool) {
+ s := cs.srv
plan := completionPlan{
params: p,
above: above,
@@ -271,7 +280,7 @@ func (s *Server) prepareCompletionPlan(p CompletionParams, above, current, below
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) {
+ if cs.shouldSuppressForChatTriggerEOL(current, p) {
return plan, []CompletionItem{}, true
}
plan.inParams = inParamList(current, p.Position.Character)
@@ -284,14 +293,15 @@ func (s *Server) prepareCompletionPlan(p CompletionParams, above, current, below
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
}
- if !plan.inParams && !s.prefixHeuristicAllows(plan.inlinePrompt, current, p, plan.manualInvoke) {
+ if !plan.inParams && !cs.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 plan, []CompletionItem{}, true
}
return plan, nil, false
}
-func (s *Server) runCompletionForSpec(ctx context.Context, plan completionPlan, spec requestSpec, client llm.Client) ([]CompletionItem, bool) {
+func (cs *completionService) runCompletionForSpec(ctx context.Context, plan completionPlan, spec requestSpec, client llm.Client) ([]CompletionItem, bool) {
+ s := cs.srv
sortPrefix := fmt.Sprintf("%04d", spec.index)
modelKey := spec.effectiveModel(client.DefaultModel())
providerKey := spec.provider
@@ -307,14 +317,15 @@ func (s *Server) runCompletionForSpec(ctx context.Context, plan completionPlan,
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 {
+ if items, ok := cs.tryProviderNativeCompletion(ctx, plan, spec, client, sortPrefix); ok {
return items, true
}
- return s.executeChatCompletion(ctx, plan, spec, client, sortPrefix)
+ return cs.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)
+func (cs *completionService) executeChatCompletion(ctx context.Context, plan completionPlan, spec requestSpec, client llm.Client, sortPrefix string) ([]CompletionItem, bool) {
+ s := cs.srv
+ messages := cs.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)
@@ -334,7 +345,7 @@ func (s *Server) executeChatCompletion(ctx context.Context, plan completionPlan,
s.logLLMStats(modelUsed)
trimmed := strings.TrimSpace(text)
cursorByte := utf16OffsetToByteOffset(plan.current, plan.params.Position.Character)
- cleaned := s.postProcessCompletion(trimmed, plan.current[:cursorByte], plan.current)
+ cleaned := cs.postProcessCompletion(trimmed, plan.current[:cursorByte], plan.current)
if cleaned == "" {
return nil, false
}
@@ -373,7 +384,8 @@ func parseManualInvoke(ctx any) bool {
}
// shouldSuppressForChatTriggerEOL returns true when a chat trigger like ">" follows ?, !, :, or ; at EOL.
-func (s *Server) shouldSuppressForChatTriggerEOL(current string, p CompletionParams) bool {
+func (cs *completionService) shouldSuppressForChatTriggerEOL(current string, p CompletionParams) bool {
+ s := cs.srv
t := strings.TrimRight(current, " \t")
suffix, prefixes, _ := s.chatConfig()
if suffix == "" {
@@ -395,7 +407,8 @@ func (s *Server) shouldSuppressForChatTriggerEOL(current string, p CompletionPar
}
// prefixHeuristicAllows applies minimal prefix rules unless inlinePrompt or structural triggers apply.
-func (s *Server) prefixHeuristicAllows(inlinePrompt bool, current string, p CompletionParams, manualInvoke bool) bool {
+func (cs *completionService) prefixHeuristicAllows(inlinePrompt bool, current string, p CompletionParams, manualInvoke bool) bool {
+ s := cs.srv
// Convert UTF-16 offset to byte offset for correct multi-byte handling,
// then clamp to the line length.
idx := utf16OffsetToByteOffset(current, p.Position.Character)
@@ -443,7 +456,8 @@ func buildNativeCompletionCacheKey(planCacheKey, provider, modelUsed string, cli
// postProcessNativeCompletion strips duplicates and applies indentation to the raw suggestion.
// Returns the cleaned text, or an empty string when the suggestion should be discarded.
-func (s *Server) postProcessNativeCompletion(raw, current string, charOffset int) string {
+func (cs *completionService) postProcessNativeCompletion(raw, current string, charOffset int) string {
+ s := cs.srv
cleaned := strings.TrimSpace(raw)
if cleaned == "" {
return ""
@@ -472,7 +486,8 @@ func (s *Server) postProcessNativeCompletion(raw, current string, charOffset int
}
// tryProviderNativeCompletion attempts provider-native completion and returns items when successful.
-func (s *Server) tryProviderNativeCompletion(ctx context.Context, plan completionPlan, spec requestSpec, client llm.Client, sortPrefix string) ([]CompletionItem, bool) {
+func (cs *completionService) tryProviderNativeCompletion(ctx context.Context, plan completionPlan, spec requestSpec, client llm.Client, sortPrefix string) ([]CompletionItem, bool) {
+ s := cs.srv
cc, ok := client.(llm.CodeCompleter)
if !ok {
return nil, false
@@ -513,7 +528,7 @@ func (s *Server) tryProviderNativeCompletion(ctx context.Context, plan completio
logging.Logf("lsp ", "stats update error: %v", err)
}
s.logLLMStats(modelUsed)
- cleaned := s.postProcessNativeCompletion(suggestions[0], current, p.Position.Character)
+ cleaned := cs.postProcessNativeCompletion(suggestions[0], current, p.Position.Character)
if cleaned == "" {
return nil, false
}
@@ -526,15 +541,14 @@ func (s *Server) tryProviderNativeCompletion(ctx context.Context, plan completio
// 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) {
+func (cs *completionService) waitForDebounce(ctx context.Context) {
+ s := cs.srv
d := s.completionDebounce()
if d <= 0 {
return
}
for {
- s.mu.RLock()
- last := s.lastInput
- s.mu.RUnlock()
+ last := s.chatSvc().lastActivity()
if last.IsZero() {
return
}
@@ -555,7 +569,8 @@ func (s *Server) waitForDebounce(ctx context.Context) {
}
// 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 {
+func (cs *completionService) buildCompletionMessages(inlinePrompt, hasExtra bool, extraText string, inParams bool, p CompletionParams, above, current, below, funcCtx string) []llm.Message {
+ s := cs.srv
vars := map[string]string{
"file": p.TextDocument.URI,
"function": funcCtx,
@@ -587,7 +602,8 @@ func (s *Server) buildCompletionMessages(inlinePrompt, hasExtra bool, extraText
}
// postProcessCompletion normalizes and deduplicates completion text and applies indentation rules.
-func (s *Server) postProcessCompletion(text string, leftOfCursor string, currentLine string) string {
+func (cs *completionService) postProcessCompletion(text string, leftOfCursor string, currentLine string) string {
+ s := cs.srv
cleaned := stripCodeFences(text)
if cleaned != "" && strings.ContainsRune(cleaned, '`') {
if inline := stripInlineCodeSpan(cleaned); strings.TrimSpace(inline) != "" {