summaryrefslogtreecommitdiff
path: root/internal/lsp/handlers.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-08-29 00:24:59 +0300
committerPaul Buetow <paul@buetow.org>2025-08-29 00:24:59 +0300
commit99db2d66c8baa72a0a6dd6e0fbaad9b20826483d (patch)
treec99908a62c94ce8f0c68be3496ca524a77637797 /internal/lsp/handlers.go
parent0c2994f0065090a4884b28dc27eb760db2dfaab3 (diff)
lsp: extract generic helpers to handlers_utils.go; tidy imports
Diffstat (limited to 'internal/lsp/handlers.go')
-rw-r--r--internal/lsp/handlers.go245
1 files changed, 11 insertions, 234 deletions
diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go
index 774a94a..a7b0ac4 100644
--- a/internal/lsp/handlers.go
+++ b/internal/lsp/handlers.go
@@ -5,10 +5,7 @@ package lsp
import (
"encoding/json"
"fmt"
- "hexai/internal/llm"
- "hexai/internal/logging"
"strings"
- "time"
)
func (s *Server) handle(req Request) {
@@ -23,13 +20,7 @@ func (s *Server) handle(req Request) {
// handleInitialize moved to handlers_init.go
-func (s *Server) llmRequestOpts() []llm.RequestOption {
- opts := []llm.RequestOption{llm.WithMaxTokens(s.maxTokens)}
- if s.codingTemperature != nil {
- opts = append(opts, llm.WithTemperature(*s.codingTemperature))
- }
- return opts
-}
+// llmRequestOpts moved to handlers_utils.go
// instructionFromSelection extracts the first instruction from selection text.
// Preference order on each line: strict ;text; marker (no inner spaces), then
@@ -482,41 +473,7 @@ func (s *Server) makeCompletionItems(cleaned string, inParams bool, current stri
}
// small helpers to keep tryLLMCompletion short
-func (s *Server) incSentCounters(n int) {
- s.mu.Lock()
- s.llmReqTotal++
- s.llmSentBytesTotal += int64(n)
- s.mu.Unlock()
-}
-
-func (s *Server) incRecvCounters(n int) {
- s.mu.Lock()
- s.llmRespTotal++
- s.llmRespBytesTotal += int64(n)
- s.mu.Unlock()
-}
-
-func (s *Server) logLLMStats() {
- s.mu.RLock()
- avgSent := int64(0)
- if s.llmReqTotal > 0 {
- avgSent = s.llmSentBytesTotal / s.llmReqTotal
- }
- avgRecv := int64(0)
- if s.llmRespTotal > 0 {
- avgRecv = s.llmRespBytesTotal / s.llmRespTotal
- }
- reqs, sentTot, recvTot := s.llmReqTotal, s.llmSentBytesTotal, s.llmRespBytesTotal
- s.mu.RUnlock()
- mins := time.Since(s.startTime).Minutes()
- if mins <= 0 {
- mins = 0.001
- }
- rpm := float64(reqs) / mins
- sentPerMin := float64(sentTot) / mins
- recvPerMin := float64(recvTot) / mins
- logging.Logf("lsp ", "llm stats reqs=%d avg_sent=%d avg_recv=%d sent_total=%d recv_total=%d rpm=%.2f sent_per_min=%.0f recv_per_min=%.0f", reqs, avgSent, avgRecv, sentTot, recvTot, rpm, sentPerMin, recvPerMin)
-}
+// LLM stats helpers moved to handlers_utils.go
// collectPromptRemovalEdits returns edits to remove all inline prompt markers.
// Supported form (inclusive):
@@ -525,209 +482,29 @@ func (s *Server) logLLMStats() {
// after the trailing ';' is also removed for cleanliness.
//
// Multiple markers per line are supported.
-func (s *Server) collectPromptRemovalEdits(uri string) []TextEdit {
- d := s.getDocument(uri)
- if d == nil || len(d.lines) == 0 {
- return nil
- }
- var edits []TextEdit
- for i, line := range d.lines {
- edits = append(edits, promptRemovalEditsForLine(line, i)...)
- }
- return edits
-}
-
-func promptRemovalEditsForLine(line string, lineNum int) []TextEdit {
- if hasDoubleSemicolonTrigger(line) {
- return []TextEdit{{Range: Range{Start: Position{Line: lineNum, Character: 0}, End: Position{Line: lineNum, Character: len(line)}}, NewText: ""}}
- }
- return collectSemicolonMarkers(line, lineNum)
-}
-
-func hasDoubleSemicolonTrigger(line string) bool {
- pos := 0
- for pos < len(line) {
- j := strings.Index(line[pos:], ";;")
- if j < 0 {
- return false
- }
- j += pos
- contentStart := j + 2
- if contentStart >= len(line) {
- return false // nothing after ';;'
- }
- // First content char cannot be space or another ';'
- first := line[contentStart]
- if first == ' ' || first == ';' {
- pos = contentStart + 1
- continue
- }
- // Require at least one content char before a closing ';'
- k := strings.Index(line[contentStart+1:], ";")
- if k < 0 {
- return false
- }
- closeIdx := contentStart + 1 + k
- // Disallow trailing space before closing ';'
- if closeIdx-1 >= 0 && line[closeIdx-1] == ' ' {
- pos = closeIdx + 1
- continue
- }
- return true
- }
- return false
-}
-
-func collectSemicolonMarkers(line string, lineNum int) []TextEdit {
- var edits []TextEdit
- startSemi := 0
- for startSemi < len(line) {
- j := strings.Index(line[startSemi:], ";")
- if j < 0 {
- break
- }
- j += startSemi
- k := strings.Index(line[j+1:], ";")
- if k < 0 {
- break
- }
- if j+1 >= len(line) || line[j+1] == ' ' {
- startSemi = j + 1
- continue
- }
- if line[j+1] == ';' {
- startSemi = j + 2
- continue
- }
- closeIdx := j + 1 + k
- if closeIdx-1 < 0 || line[closeIdx-1] == ' ' {
- startSemi = closeIdx + 1
- continue
- }
- if closeIdx-(j+1) < 1 {
- startSemi = closeIdx + 1
- continue
- }
- endChar := closeIdx + 1
- if endChar < len(line) && line[endChar] == ' ' {
- endChar++
- }
- edits = append(edits, TextEdit{Range: Range{Start: Position{Line: lineNum, Character: j}, End: Position{Line: lineNum, Character: endChar}}, NewText: ""})
- startSemi = endChar
- }
- return edits
-}
+// Inline prompt removal helpers moved to handlers_utils.go
-func inParamList(current string, cursor int) bool {
- if !strings.Contains(current, "func ") {
- return false
- }
- open := strings.Index(current, "(")
- close := strings.Index(current, ")")
- return open >= 0 && cursor > open && (close == -1 || cursor <= close)
-}
+// inParamList moved to handlers_utils.go
-func buildPrompts(inParams bool, p CompletionParams, above, current, below, funcCtx string) (string, string) {
- if inParams {
- sys := "You are a code completion engine for function signatures. Return only the parameter list contents (without parentheses), no braces, no prose. Prefer idiomatic names and types."
- user := fmt.Sprintf("Cursor is inside the function parameter list. Suggest only the parameter list (no parentheses).\nFunction line: %s\nCurrent line (cursor at %d): %s", funcCtx, p.Position.Character, current)
- return sys, user
- }
- sys := "You are a terse code completion engine. Return only the code to insert, no surrounding prose or backticks. Only continue from the cursor; never repeat characters already present to the left of the cursor on the current line (e.g., if 'name :=' is already typed, only return the right-hand side expression)."
- user := fmt.Sprintf("Provide the next likely code to insert at the cursor.\nFile: %s\nFunction/context: %s\nAbove line: %s\nCurrent line (cursor at character %d): %s\nBelow line: %s\nOnly return the completion snippet.", p.TextDocument.URI, funcCtx, above, p.Position.Character, current, below)
- return sys, user
-}
+// buildPrompts moved to handlers_utils.go
-func computeTextEditAndFilter(cleaned string, inParams bool, current string, p CompletionParams) (*TextEdit, string) {
- if inParams {
- open := strings.Index(current, "(")
- close := strings.Index(current, ")")
- if open >= 0 {
- left := open + 1
- right := len(current)
- if close >= 0 && close >= left {
- right = close
- }
- if p.Position.Character < right {
- right = p.Position.Character
- }
- te := &TextEdit{Range: Range{Start: Position{Line: p.Position.Line, Character: left}, End: Position{Line: p.Position.Line, Character: right}}, NewText: cleaned}
- var filter string
- if left >= 0 && right >= left && right <= len(current) {
- filter = strings.TrimLeft(current[left:right], " \t")
- }
- return te, filter
- }
- }
- startChar := computeWordStart(current, p.Position.Character)
- te := &TextEdit{Range: Range{Start: Position{Line: p.Position.Line, Character: startChar}, End: Position{Line: p.Position.Line, Character: p.Position.Character}}, NewText: cleaned}
- filter := strings.TrimLeft(current[startChar:p.Position.Character], " \t")
- return te, filter
-}
+// computeTextEditAndFilter moved to handlers_utils.go
-func computeWordStart(current string, at int) int {
- if at > len(current) {
- at = len(current)
- }
- for at > 0 {
- ch := current[at-1]
- if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' {
- at--
- continue
- }
- break
- }
- return at
-}
+// computeWordStart moved to handlers_utils.go
-func isIdentChar(ch byte) bool {
- return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_'
-}
+// isIdentChar moved to handlers_utils.go
// lineHasInlinePrompt returns true if the line contains an inline strict
// semicolon marker ;text; (no spaces at boundaries) or a double-semicolon
// pattern recognized by hasDoubleSemicolonTrigger.
-func lineHasInlinePrompt(line string) bool {
- if _, _, _, ok := findStrictSemicolonTag(line); ok {
- return true
- }
- return hasDoubleSemicolonTrigger(line)
-}
+// lineHasInlinePrompt moved to handlers_utils.go
// leadingIndent returns the run of leading spaces/tabs from the provided line.
-func leadingIndent(line string) string {
- i := 0
- for i < len(line) {
- if line[i] == ' ' || line[i] == '\t' {
- i++
- continue
- }
- break
- }
- if i == 0 {
- return ""
- }
- return line[:i]
-}
+// leadingIndent moved to handlers_utils.go
// applyIndent prefixes each non-empty line of suggestion with the given indent
// unless it already starts with that indent.
-func applyIndent(indent, suggestion string) string {
- if indent == "" || suggestion == "" {
- return suggestion
- }
- lines := splitLines(suggestion)
- for i, ln := range lines {
- if strings.TrimSpace(ln) == "" {
- continue
- }
- if strings.HasPrefix(ln, indent) {
- continue
- }
- lines[i] = indent + ln
- }
- return strings.Join(lines, "\n")
-}
+// applyIndent moved to handlers_utils.go
// isBareDoubleSemicolon reports whether the line contains a standalone
// double-semicolon marker with no inline content (";;" possibly with only