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.go208
1 files changed, 153 insertions, 55 deletions
diff --git a/internal/lsp/handlers_utils.go b/internal/lsp/handlers_utils.go
index 2748a60..b3056b9 100644
--- a/internal/lsp/handlers_utils.go
+++ b/internal/lsp/handlers_utils.go
@@ -312,11 +312,36 @@ func (s *Server) chatWithStats(ctx context.Context, surface surfaceKind, spec re
// Inline prompt utilities
-func lineHasInlinePrompt(line string, open, close byte) bool {
- if _, _, _, ok := findStrictInlineTag(line, open, close); ok {
+func lineHasInlinePrompt(line string, openStr string, open, close byte) bool {
+ if openStr == "" {
+ openStr = string(open)
+ }
+ if _, _, _, ok := findStrictInlineTag(line, openStr, open, close); ok {
return true
}
- return hasDoubleOpenTrigger(line, open, close)
+ return hasDoubleOpenTrigger(line, openStr, open, close)
+}
+
+func doubleOpenSequences(openStr string, open, close byte) []string {
+ seen := make(map[string]struct{}, 2)
+ var seqs []string
+ if openStr != "" && close != 0 {
+ seq := openStr + string(close)
+ if _, ok := seen[seq]; !ok {
+ seen[seq] = struct{}{}
+ seqs = append(seqs, seq)
+ }
+ }
+ if openStr != "" && open != 0 {
+ seq := string(open) + openStr
+ if len(seq) > len(openStr) {
+ if _, ok := seen[seq]; !ok {
+ seen[seq] = struct{}{}
+ seqs = append(seqs, seq)
+ }
+ }
+ }
+ return seqs
}
func leadingIndent(line string) string {
@@ -353,34 +378,66 @@ func applyIndent(indent, suggestion string) string {
// --- Inline marker parsing and general string utilities ---
-// findStrictInlineTag finds >text> (configurable), with no space after the first
+// 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, open, close byte) (string, int, int, bool) {
+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)
pos := 0
for pos < len(line) {
- // find opening marker
- j := strings.IndexByte(line[pos:], open)
+ j := strings.IndexByte(line[pos:], openChar)
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] == open || line[j+1] == ' ' {
+ if !strings.HasPrefix(line[j:], openStr) {
pos = j + 1
continue
}
- // find closing marker
- k := strings.IndexByte(line[j+1:], close)
+ contentStart := j + len(openStr)
+ if contentStart >= len(line) {
+ return "", 0, 0, false
+ }
+ doubleHit := false
+ for _, seq := range doubleSeqs {
+ if strings.HasPrefix(line[j:], seq) {
+ doubleHit = true
+ contentStart += len(seq) - len(openStr)
+ if contentStart >= len(line) {
+ return "", 0, 0, false
+ }
+ break
+ }
+ }
+ next := line[contentStart]
+ if next == ' ' {
+ pos = contentStart + 1
+ continue
+ }
+ if !doubleHit && next == close {
+ pos = contentStart + 1
+ continue
+ }
+ k := strings.IndexByte(line[contentStart:], close)
if k < 0 {
return "", 0, 0, false
}
- closeIdx := j + 1 + k
- if closeIdx-1 < 0 || line[closeIdx-1] == ' ' {
+ closeIdx := contentStart + k
+ if closeIdx-1 >= contentStart && line[closeIdx-1] == ' ' {
pos = closeIdx + 1
continue
}
- inner := strings.TrimSpace(line[j+1 : closeIdx])
+ inner := strings.TrimSpace(line[contentStart:closeIdx])
if inner == "" {
pos = closeIdx + 1
continue
@@ -394,20 +451,20 @@ func findStrictInlineTag(line string, open, close byte) (string, int, int, bool)
// 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 isBareDoubleOpen(line string, open, close byte) bool {
+func isBareDoubleOpen(line string, openStr string, open, close byte) bool {
t := strings.TrimSpace(line)
- // check for double-open pattern
- dbl := string([]byte{open, open})
- if !strings.Contains(t, dbl) {
- return false
+ if openStr == "" {
+ openStr = string(open)
}
- if hasDoubleOpenTrigger(t, open, close) {
+ if openStr == "" {
return false
}
- if strings.HasPrefix(t, dbl) {
- rest := strings.TrimSpace(t[len(dbl):])
- if rest == "" || rest == ";" {
- return true
+ for _, seq := range doubleOpenSequences(openStr, open, close) {
+ if strings.HasPrefix(t, seq) {
+ rest := strings.TrimSpace(t[len(seq):])
+ if rest == "" || rest == string(close) {
+ return true
+ }
}
}
return false
@@ -562,40 +619,62 @@ func (s *Server) collectPromptRemovalEdits(uri string) []TextEdit {
return nil
}
var edits []TextEdit
- _, _, openChar, closeChar := s.inlineMarkers()
+ openStr, _, openChar, closeChar := s.inlineMarkers()
for i, line := range d.lines {
- edits = append(edits, promptRemovalEditsForLine(line, i, openChar, closeChar)...)
+ edits = append(edits, promptRemovalEditsForLine(line, i, openStr, openChar, closeChar)...)
}
return edits
}
-func promptRemovalEditsForLine(line string, lineNum int, open, close byte) []TextEdit {
- if hasDoubleOpenTrigger(line, open, close) {
+func promptRemovalEditsForLine(line string, lineNum int, openStr string, open, close byte) []TextEdit {
+ if hasDoubleOpenTrigger(line, openStr, open, close) {
return []TextEdit{{Range: Range{Start: Position{Line: lineNum, Character: 0}, End: Position{Line: lineNum, Character: len(line)}}, NewText: ""}}
}
- return collectSemicolonMarkers(line, lineNum, open, close)
+ return collectSemicolonMarkers(line, lineNum, openStr, open, close)
}
-func hasDoubleOpenTrigger(line string, open, close byte) bool {
+func hasDoubleOpenTrigger(line string, openStr string, open, close byte) bool {
+ if openStr == "" {
+ openStr = string(open)
+ }
+ if openStr == "" {
+ return false
+ }
+ seqs := doubleOpenSequences(openStr, open, close)
+ if len(seqs) == 0 {
+ return false
+ }
pos := 0
for pos < len(line) {
- // look for double-open sequence
- dbl := string([]byte{open, open})
- j := strings.Index(line[pos:], dbl)
- if j < 0 {
+ 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 {
return false
}
- j += pos
- contentStart := j + len(dbl)
+ contentStart := found + len(seq)
if contentStart >= len(line) {
return false
}
first := line[contentStart]
- if first == ' ' || first == open {
+ if first == ' ' || first == close || first == open {
pos = contentStart + 1
continue
}
- // find closing
+ if contentStart+1 >= len(line) {
+ return false
+ }
k := strings.IndexByte(line[contentStart+1:], close)
if k < 0 {
return false
@@ -610,34 +689,53 @@ func hasDoubleOpenTrigger(line string, open, close byte) bool {
return false
}
-func collectSemicolonMarkers(line string, lineNum int, open, close byte) []TextEdit {
+func collectSemicolonMarkers(line string, lineNum int, openStr string, open, close byte) []TextEdit {
+ if openStr == "" {
+ openStr = string(open)
+ }
+ if openStr == "" {
+ return nil
+ }
var edits []TextEdit
- startSemi := 0
- for startSemi < len(line) {
- j := strings.IndexByte(line[startSemi:], open)
+ start := 0
+ doubleSeqs := doubleOpenSequences(openStr, open, close)
+ for start < len(line) {
+ j := strings.Index(line[start:], openStr)
if j < 0 {
break
}
- j += startSemi
- k := strings.IndexByte(line[j+1:], close)
- if k < 0 {
+ j += start
+ contentStart := j + len(openStr)
+ if contentStart >= len(line) {
break
}
- if j+1 >= len(line) || line[j+1] == ' ' {
- startSemi = j + 1
+ next := line[contentStart]
+ if next == ' ' {
+ start = j + 1
continue
}
- if line[j+1] == open { // skip double-open start
- startSemi = j + 2
+ skipDouble := false
+ for _, seq := range doubleSeqs {
+ if strings.HasPrefix(line[j:], seq) {
+ skipDouble = true
+ break
+ }
+ }
+ if skipDouble {
+ start = j + 1
continue
}
- closeIdx := j + 1 + k
- if closeIdx-1 < 0 || line[closeIdx-1] == ' ' {
- startSemi = closeIdx + 1
+ k := strings.IndexByte(line[contentStart:], close)
+ if k < 0 {
+ break
+ }
+ closeIdx := contentStart + k
+ if closeIdx-1 < contentStart || line[closeIdx-1] == ' ' {
+ start = closeIdx + 1
continue
}
- if closeIdx-(j+1) < 1 {
- startSemi = closeIdx + 1
+ if closeIdx == contentStart {
+ start = closeIdx + 1
continue
}
endChar := closeIdx + 1
@@ -645,7 +743,7 @@ func collectSemicolonMarkers(line string, lineNum int, open, close byte) []TextE
endChar++
}
edits = append(edits, TextEdit{Range: Range{Start: Position{Line: lineNum, Character: j}, End: Position{Line: lineNum, Character: endChar}}, NewText: ""})
- startSemi = endChar
+ start = endChar
}
return edits
}