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.go225
1 files changed, 225 insertions, 0 deletions
diff --git a/internal/lsp/handlers_utils.go b/internal/lsp/handlers_utils.go
index 26a0780..d2cf52d 100644
--- a/internal/lsp/handlers_utils.go
+++ b/internal/lsp/handlers_utils.go
@@ -162,6 +162,231 @@ func applyIndent(indent, suggestion string) string {
return strings.Join(lines, "\n")
}
+// --- 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
+}
+
+// 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
+}
+
+// stripDuplicateAssignmentPrefix removes a duplicated assignment prefix from the suggestion.
+func stripDuplicateAssignmentPrefix(prefixBeforeCursor, suggestion string) string {
+ s2 := strings.TrimLeft(suggestion, " \t")
+ // Prefer := if present at end of prefix
+ if idx := strings.LastIndex(prefixBeforeCursor, ":="); idx >= 0 && idx+2 <= len(prefixBeforeCursor) {
+ tail := prefixBeforeCursor[idx+2:]
+ if strings.TrimSpace(tail) == "" {
+ start := idx - 1
+ for start >= 0 && (isIdentChar(prefixBeforeCursor[start]) || prefixBeforeCursor[start] == ' ' || prefixBeforeCursor[start] == '\t') {
+ start--
+ }
+ start++
+ seg := strings.TrimRight(prefixBeforeCursor[start:idx+2], " \t")
+ if strings.HasPrefix(s2, seg) {
+ return strings.TrimLeft(s2[len(seg):], " \t")
+ }
+ }
+ }
+ // Fallback to plain '=' if present
+ if idx := strings.LastIndex(prefixBeforeCursor, "="); idx >= 0 {
+ if !(idx > 0 && prefixBeforeCursor[idx-1] == ':') { // not :=
+ tail := prefixBeforeCursor[idx+1:]
+ if strings.TrimSpace(tail) == "" {
+ start := idx - 1
+ for start >= 0 && (isIdentChar(prefixBeforeCursor[start]) || prefixBeforeCursor[start] == ' ' || prefixBeforeCursor[start] == '\t') {
+ start--
+ }
+ start++
+ seg := strings.TrimRight(prefixBeforeCursor[start:idx+1], " \t")
+ if strings.HasPrefix(s2, seg) {
+ return strings.TrimLeft(s2[len(seg):], " \t")
+ }
+ }
+ }
+ }
+ return suggestion
+}
+
+// stripDuplicateGeneralPrefix removes any already-typed prefix that the model repeated.
+func stripDuplicateGeneralPrefix(prefixBeforeCursor, suggestion string) string {
+ if suggestion == "" {
+ return suggestion
+ }
+ s := strings.TrimLeft(suggestion, " \t")
+ p := strings.TrimRight(prefixBeforeCursor, " \t")
+ if p != "" && strings.HasPrefix(s, p) {
+ return strings.TrimLeft(s[len(p):], " \t")
+ }
+ for k := len(p) - 1; k > 0; k-- {
+ if !isIdentBoundary(p[k-1]) {
+ continue
+ }
+ suf := strings.TrimLeft(p[k:], " \t")
+ if suf == "" {
+ continue
+ }
+ if strings.HasPrefix(s, suf) {
+ return strings.TrimLeft(s[len(suf):], " \t")
+ }
+ }
+ return suggestion
+}
+
+func isIdentBoundary(ch byte) bool {
+ return !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_')
+}
+
+// stripCodeFences removes surrounding Markdown code fences from a model response.
+func stripCodeFences(s string) string {
+ t := strings.TrimSpace(s)
+ if t == "" {
+ return t
+ }
+ lines := splitLines(t)
+ start := 0
+ for start < len(lines) && strings.TrimSpace(lines[start]) == "" {
+ start++
+ }
+ end := len(lines) - 1
+ for end >= 0 && strings.TrimSpace(lines[end]) == "" {
+ end--
+ }
+ if start >= len(lines) || end < 0 || start > end {
+ return t
+ }
+ first := strings.TrimSpace(lines[start])
+ last := strings.TrimSpace(lines[end])
+ if strings.HasPrefix(first, "```") && last == "```" && end > start {
+ inner := strings.Join(lines[start+1:end], "\n")
+ return inner
+ }
+ return t
+}
+
+// stripInlineCodeSpan returns the contents of the first inline backtick code span if present.
+func stripInlineCodeSpan(s string) string {
+ t := strings.TrimSpace(s)
+ if t == "" {
+ return t
+ }
+ i := strings.IndexByte(t, '`')
+ if i < 0 {
+ return t
+ }
+ jrel := strings.IndexByte(t[i+1:], '`')
+ if jrel < 0 {
+ return t
+ }
+ j := i + 1 + jrel
+ return t[i+1 : j]
+}
+
+// labelForCompletion picks a short, readable label for the completion list.
+func labelForCompletion(cleaned, filter string) string {
+ label := trimLen(firstLine(cleaned))
+ if filter != "" && !strings.HasPrefix(strings.ToLower(label), strings.ToLower(filter)) {
+ return filter
+ }
+ return label
+}
+
+// extractRangeText returns the exact text within the given document range.
+func extractRangeText(d *document, r Range) string {
+ if r.Start.Line == r.End.Line {
+ line := d.lines[r.Start.Line]
+ if r.Start.Character < 0 {
+ r.Start.Character = 0
+ }
+ if r.End.Character > len(line) {
+ r.End.Character = len(line)
+ }
+ if r.Start.Character > r.End.Character {
+ return ""
+ }
+ return line[r.Start.Character:r.End.Character]
+ }
+ var b strings.Builder
+ // first line
+ first := d.lines[r.Start.Line]
+ if r.Start.Character < 0 {
+ r.Start.Character = 0
+ }
+ if r.Start.Character > len(first) {
+ r.Start.Character = len(first)
+ }
+ b.WriteString(first[r.Start.Character:])
+ b.WriteString("\n")
+ // middle lines
+ for i := r.Start.Line + 1; i < r.End.Line; i++ {
+ b.WriteString(d.lines[i])
+ if i+1 <= r.End.Line {
+ b.WriteString("\n")
+ }
+ }
+ // last line
+ last := d.lines[r.End.Line]
+ if r.End.Character < 0 {
+ r.End.Character = 0
+ }
+ if r.End.Character > len(last) {
+ r.End.Character = len(last)
+ }
+ b.WriteString(last[:r.End.Character])
+ return b.String()
+}
+
// collectPromptRemovalEdits returns edits to remove all inline prompt markers.
func (s *Server) collectPromptRemovalEdits(uri string) []TextEdit {
d := s.getDocument(uri)