summaryrefslogtreecommitdiff
path: root/internal/lsp/handlers_utils.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-06 10:25:36 +0300
committerPaul Buetow <paul@buetow.org>2025-09-06 10:25:36 +0300
commit5be9532cfa630f4aacd8d879c3e4f5cc316da0fa (patch)
tree0a901680fccd1e2703ffdbd9284ccff932be1d67 /internal/lsp/handlers_utils.go
parent70f1d0e78c57dfa5beae779b3d392b6e6fa44c14 (diff)
feat(lsp): configurable inline/chat triggers; switch inline markers to >text>/>>text>; update docs and example config; tests updated to new triggers and raise LSP coverage to >=85%; chore: remove semicolon legacy; chore(mage): auto-refresh coverage daily if docs/coverage.out is older than 24h
Diffstat (limited to 'internal/lsp/handlers_utils.go')
-rw-r--r--internal/lsp/handlers_utils.go259
1 files changed, 135 insertions, 124 deletions
diff --git a/internal/lsp/handlers_utils.go b/internal/lsp/handlers_utils.go
index 42b35a5..e2c35e3 100644
--- a/internal/lsp/handlers_utils.go
+++ b/internal/lsp/handlers_utils.go
@@ -9,6 +9,11 @@ import (
"time"
)
+// Configurable inline trigger characters (default to '>') used by free helpers below.
+// NewServer assigns these based on ServerOptions.
+var inlineOpenChar byte = '>'
+var inlineCloseChar byte = '>'
+
// llmRequestOpts builds request options from server settings.
func (s *Server) llmRequestOpts() []llm.RequestOption {
opts := []llm.RequestOption{llm.WithMaxTokens(s.maxTokens)}
@@ -124,10 +129,10 @@ func isIdentChar(ch byte) bool {
// Inline prompt utilities
func lineHasInlinePrompt(line string) bool {
- if _, _, _, ok := findStrictSemicolonTag(line); ok {
- return true
- }
- return hasDoubleSemicolonTrigger(line)
+ if _, _, _, ok := findStrictInlineTag(line); ok {
+ return true
+ }
+ return hasDoubleOpenTrigger(line)
}
func leadingIndent(line string) string {
@@ -164,61 +169,64 @@ func applyIndent(indent, suggestion string) string {
// --- Inline marker parsing and general string utilities ---
-// findStrictSemicolonTag finds ;text; with no space after first ';' and no space
-// before the last ';' on the given line. Returns the text between semicolons,
-// the start index of the opening ';', the end index just after the closing ';',
-// and whether it was found.
-func findStrictSemicolonTag(line string) (string, int, int, bool) {
- pos := 0
- for pos < len(line) {
- j := strings.Index(line[pos:], ";")
- if j < 0 {
- return "", 0, 0, false
- }
- j += pos
- // ensure single ';' (not ';;') and non-space after
- if j+1 >= len(line) || line[j+1] == ';' || line[j+1] == ' ' {
- pos = j + 1
- continue
- }
- k := strings.Index(line[j+1:], ";")
- if k < 0 {
- return "", 0, 0, false
- }
- closeIdx := j + 1 + k
- if closeIdx-1 < 0 || line[closeIdx-1] == ' ' {
- pos = closeIdx + 1
- continue
- }
- inner := strings.TrimSpace(line[j+1 : closeIdx])
- if inner == "" {
- pos = closeIdx + 1
- continue
- }
- end := closeIdx + 1
- return inner, j, end, true
- }
- return "", 0, 0, false
+// findStrictInlineTag finds >text> (configurable), with no space after the first
+// 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) (string, int, int, bool) {
+ pos := 0
+ for pos < len(line) {
+ // find opening marker
+ j := strings.IndexByte(line[pos:], inlineOpenChar)
+ if j < 0 {
+ return "", 0, 0, false
+ }
+ j += pos
+ // ensure single open (not double) and non-space after
+ if j+1 >= len(line) || line[j+1] == inlineOpenChar || line[j+1] == ' ' {
+ pos = j + 1
+ continue
+ }
+ // find closing marker
+ k := strings.IndexByte(line[j+1:], inlineCloseChar)
+ if k < 0 {
+ return "", 0, 0, false
+ }
+ closeIdx := j + 1 + k
+ if closeIdx-1 < 0 || line[closeIdx-1] == ' ' {
+ pos = closeIdx + 1
+ continue
+ }
+ inner := strings.TrimSpace(line[j+1 : closeIdx])
+ if inner == "" {
+ pos = closeIdx + 1
+ continue
+ }
+ end := closeIdx + 1
+ return inner, j, end, true
+ }
+ return "", 0, 0, false
}
// 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;".
-func isBareDoubleSemicolon(line string) bool {
- t := strings.TrimSpace(line)
- if !strings.Contains(t, ";;") {
- return false
- }
- if hasDoubleSemicolonTrigger(t) {
- return false
- }
- if strings.HasPrefix(t, ";;") {
- rest := strings.TrimSpace(t[2:])
- if rest == "" || rest == ";" {
- return true
- }
- }
- return false
+func isBareDoubleOpen(line string) bool {
+ t := strings.TrimSpace(line)
+ // check for double-open pattern
+ dbl := string([]byte{inlineOpenChar, inlineOpenChar})
+ if !strings.Contains(t, dbl) {
+ return false
+ }
+ if hasDoubleOpenTrigger(t) {
+ return false
+ }
+ if strings.HasPrefix(t, dbl) {
+ rest := strings.TrimSpace(t[len(dbl):])
+ if rest == "" || rest == ";" {
+ return true
+ }
+ }
+ return false
}
// stripDuplicateAssignmentPrefix removes a duplicated assignment prefix from the suggestion.
@@ -401,79 +409,82 @@ func (s *Server) collectPromptRemovalEdits(uri string) []TextEdit {
}
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)
+ if hasDoubleOpenTrigger(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
- }
- first := line[contentStart]
- if first == ' ' || first == ';' {
- pos = contentStart + 1
- continue
- }
- k := strings.Index(line[contentStart+1:], ";")
- if k < 0 {
- return false
- }
- closeIdx := contentStart + 1 + k
- if closeIdx-1 >= 0 && line[closeIdx-1] == ' ' {
- pos = closeIdx + 1
- continue
- }
- return true
- }
- return false
+func hasDoubleOpenTrigger(line string) bool {
+ pos := 0
+ for pos < len(line) {
+ // look for double-open sequence
+ dbl := string([]byte{inlineOpenChar, inlineOpenChar})
+ j := strings.Index(line[pos:], dbl)
+ if j < 0 {
+ return false
+ }
+ j += pos
+ contentStart := j + len(dbl)
+ if contentStart >= len(line) {
+ return false
+ }
+ first := line[contentStart]
+ if first == ' ' || first == inlineOpenChar {
+ pos = contentStart + 1
+ continue
+ }
+ // find closing
+ k := strings.IndexByte(line[contentStart+1:], inlineCloseChar)
+ if k < 0 {
+ return false
+ }
+ closeIdx := contentStart + 1 + k
+ 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
+ var edits []TextEdit
+ startSemi := 0
+ for startSemi < len(line) {
+ j := strings.IndexByte(line[startSemi:], inlineOpenChar)
+ if j < 0 {
+ break
+ }
+ j += startSemi
+ k := strings.IndexByte(line[j+1:], inlineCloseChar)
+ if k < 0 {
+ break
+ }
+ if j+1 >= len(line) || line[j+1] == ' ' {
+ startSemi = j + 1
+ continue
+ }
+ if line[j+1] == inlineOpenChar { // skip double-open start
+ 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
}