diff options
Diffstat (limited to 'internal/lsp/handlers_utils.go')
| -rw-r--r-- | internal/lsp/handlers_utils.go | 225 |
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) |
