summaryrefslogtreecommitdiff
path: root/internal/lsp/handlers_utils.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/lsp/handlers_utils.go')
-rw-r--r--internal/lsp/handlers_utils.go171
1 files changed, 126 insertions, 45 deletions
diff --git a/internal/lsp/handlers_utils.go b/internal/lsp/handlers_utils.go
index bede7a0..66e2ed1 100644
--- a/internal/lsp/handlers_utils.go
+++ b/internal/lsp/handlers_utils.go
@@ -4,6 +4,7 @@ package lsp
import (
"context"
"fmt"
+ "os"
"strings"
"time"
"unicode/utf8"
@@ -60,7 +61,7 @@ func (s *Server) buildRequestSpecs(surface surfaceKind) []requestSpec {
if provider == "" {
provider = cfg.Provider
}
- provider = canonicalProvider(provider)
+ provider = llmutils.CanonicalProvider(provider)
fallbackModel := entry.Model
if fallbackModel == "" {
fallbackModel = strings.TrimSpace(llmutils.DefaultModelForProvider(cfg, provider))
@@ -87,7 +88,7 @@ func (s *Server) primaryRequestSpec(surface surfaceKind) requestSpec {
specs := s.buildRequestSpecs(surface)
if len(specs) == 0 {
cfg := s.currentConfig()
- provider := canonicalProvider(cfg.Provider)
+ provider := llmutils.CanonicalProvider(cfg.Provider)
fallback := strings.TrimSpace(llmutils.DefaultModelForProvider(cfg, provider))
return requestSpec{provider: provider, fallbackModel: fallback, options: []llm.RequestOption{llm.WithMaxTokens(s.maxTokens())}}
}
@@ -99,10 +100,6 @@ func (s *Server) buildRequestSpec(surface surfaceKind) requestSpec {
return s.primaryRequestSpec(surface)
}
-func canonicalProvider(name string) string {
- return llmutils.CanonicalProvider(name)
-}
-
func surfaceConfigsFor(cfg appconfig.App, surface surfaceKind) []appconfig.SurfaceConfig {
switch surface {
case surfaceCompletion:
@@ -173,7 +170,17 @@ func (s *Server) logLLMStats(model string) {
}
scopeReqs := snap.ScopeReqs(provider, modelName)
scopeRPM := snap.ScopeRPM(provider, modelName)
- s.emitGlobalStatus(snap.Global.Reqs, snap.RPM, snap.Global.Sent, snap.Global.Recv, provider, modelName, scopeRPM, scopeReqs, snap.Window)
+ s.emitGlobalStatus(GlobalStatus{
+ Reqs: snap.Global.Reqs,
+ RPM: snap.RPM,
+ Sent: snap.Global.Sent,
+ Recv: snap.Global.Recv,
+ Provider: provider,
+ Model: modelName,
+ ScopeRPM: scopeRPM,
+ ScopeReqs: scopeReqs,
+ Window: snap.Window,
+ })
}
}
}
@@ -342,17 +349,8 @@ func applyIndent(indent, suggestion string) string {
// opening marker and no space immediately before the closing marker. Returns the
// text between markers, the start index, the end index just after closing, and ok.
func findStrictInlineTag(line string, openStr string, open, close byte) (string, int, int, bool) {
- if openStr == "" {
- openStr = string(open)
- }
- if openStr == "" {
- return "", 0, 0, false
- }
- openChar := open
- if openChar == 0 {
- openChar = openStr[0]
- }
- doubleSeqs := doubleOpenSequences(openStr, openChar, close)
+ openChar, doubleSeqs := prepareInlineTagParsing(openStr, open, close)
+
pos := 0
for pos < len(line) {
j := strings.IndexByte(line[pos:], openChar)
@@ -364,10 +362,12 @@ func findStrictInlineTag(line string, openStr string, open, close byte) (string,
pos = j + 1
continue
}
+
contentStart := j + len(openStr)
if contentStart >= len(line) {
return "", 0, 0, false
}
+
doubleHit := false
for _, seq := range doubleSeqs {
if strings.HasPrefix(line[j:], seq) {
@@ -379,6 +379,7 @@ func findStrictInlineTag(line string, openStr string, open, close byte) (string,
break
}
}
+
next := line[contentStart]
if next == ' ' {
pos = contentStart + 1
@@ -388,26 +389,55 @@ func findStrictInlineTag(line string, openStr string, open, close byte) (string,
pos = contentStart + 1
continue
}
+
k := strings.IndexByte(line[contentStart:], close)
if k < 0 {
return "", 0, 0, false
}
closeIdx := contentStart + k
- if closeIdx-1 >= contentStart && line[closeIdx-1] == ' ' {
+
+ if closeIdx > contentStart && line[closeIdx-1] == ' ' {
pos = closeIdx + 1
continue
}
+
inner := strings.TrimSpace(line[contentStart:closeIdx])
if inner == "" {
pos = closeIdx + 1
continue
}
- end := closeIdx + 1
- return inner, j, end, true
+
+ return inner, j, closeIdx + 1, true
}
return "", 0, 0, false
}
+// prepareInlineTagParsing initializes parsing state. Returns openChar and doubleSeqs.
+func prepareInlineTagParsing(openStr string, open, close byte) (byte, []string) {
+ if openStr == "" {
+ openStr = string(open)
+ }
+ if openStr == "" {
+ return 0, nil
+ }
+ openChar := open
+ if openChar == 0 {
+ openChar = openStr[0]
+ }
+ return openChar, doubleOpenSequences(openStr, openChar, close)
+}
+
+// handleDoubleSequence checks for and handles double-open sequences.
+// Returns (doubleHit, adjustedContentStart).
+func handleDoubleSequence(line string, markerPos int, doubleSeqs []string, contentStart int, openStr string) (bool, int) {
+ for _, seq := range doubleSeqs {
+ if strings.HasPrefix(line[markerPos:], seq) {
+ return true, contentStart + len(seq) - len(openStr)
+ }
+ }
+ return false, contentStart
+}
+
// isBareDoubleSemicolon reports whether the line contains a standalone
// double-semicolon marker with no inline content (";;" possibly with only
// whitespace after it). It explicitly excludes the valid form ";;text;".
@@ -622,62 +652,86 @@ func promptRemovalEditsForLine(line string, lineNum int, openStr string, open, c
return collectSemicolonMarkers(line, lineNum, openStr, open, close)
}
+// hasDoubleOpenTrigger reports whether line contains a valid double-open trigger.
func hasDoubleOpenTrigger(line string, openStr string, open, close byte) bool {
- if openStr == "" {
- openStr = string(open)
- }
- if openStr == "" {
- return false
- }
- seqs := doubleOpenSequences(openStr, open, close)
+ seqs := validDoubleOpenSequences(openStr, open, close)
if len(seqs) == 0 {
return false
}
+
pos := 0
for pos < len(line) {
- found := -1
- var seq string
- for _, cand := range seqs {
- if cand == "" {
- continue
- }
- if idx := strings.Index(line[pos:], cand); idx >= 0 {
- abs := pos + idx
- if found < 0 || abs < found {
- found = abs
- seq = cand
- }
- }
- }
- if found < 0 {
+ foundAt, seq := findEarliestSequence(line, pos, seqs)
+ if foundAt < 0 {
return false
}
- contentStart := found + len(seq)
+
+ contentStart := foundAt + len(seq)
if contentStart >= len(line) {
return false
}
+
first := line[contentStart]
if first == ' ' || first == close || first == open {
pos = contentStart + 1
continue
}
+
if contentStart+1 >= len(line) {
return false
}
+
k := strings.IndexByte(line[contentStart+1:], close)
if k < 0 {
return false
}
+
closeIdx := contentStart + 1 + k
- if closeIdx-1 >= 0 && line[closeIdx-1] == ' ' {
+ if closeIdx > 0 && line[closeIdx-1] == ' ' {
pos = closeIdx + 1
continue
}
+
return true
}
+
return false
}
+// validDoubleOpenSequences returns non-empty double-open sequences.
+func validDoubleOpenSequences(openStr string, open, close byte) []string {
+ seqs := doubleOpenSequences(openStr, open, close)
+ var result []string
+ for _, s := range seqs {
+ if s != "" {
+ result = append(result, s)
+ }
+ }
+ return result
+}
+
+// findEarliestSequence finds the earliest sequence in line starting at pos.
+// Returns (position, sequence) or (-1, "") if none found.
+func findEarliestSequence(line string, pos int, seqs []string) (int, string) {
+ foundAt := -1
+ var foundSeq string
+
+ for _, cand := range seqs {
+ if idx := strings.Index(line[pos:], cand); idx >= 0 {
+ abs := pos + idx
+ if foundAt < 0 || abs < foundAt {
+ foundAt = abs
+ foundSeq = cand
+ }
+ }
+ }
+
+ if foundAt < 0 {
+ return -1, ""
+ }
+ return foundAt, foundSeq
+}
+
func collectSemicolonMarkers(line string, lineNum int, openStr string, open, close byte) []TextEdit {
if openStr == "" {
openStr = string(open)
@@ -755,3 +809,30 @@ func utf16OffsetToByteOffset(s string, utf16Offset int) int {
}
return byteIdx
}
+
+// --- Error handling helpers ---
+
+// fileOpenError formats an error for file opening failures.
+// Wraps the original error with path context.
+func fileOpenError(path string, err error) error {
+ return fmt.Errorf("cannot open %s: %w", path, err)
+}
+
+// ensureDirectory creates a directory if it doesn't exist.
+// Returns an error if directory creation fails.
+func ensureDirectory(path string) error {
+ return os.MkdirAll(path, 0o755)
+}
+
+// directoryCreateError formats an error for directory creation failures.
+func directoryCreateError(path string, err error) error {
+ return fmt.Errorf("cannot create %s: %w", path, err)
+}
+
+// requireLLMClient checks if LLM client is available, returning an error if not.
+func requireLLMClient(client llm.Client) error {
+ if client == nil {
+ return fmt.Errorf("llm client unavailable")
+ }
+ return nil
+}