diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-06 10:25:36 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-06 10:25:36 +0300 |
| commit | 5be9532cfa630f4aacd8d879c3e4f5cc316da0fa (patch) | |
| tree | 0a901680fccd1e2703ffdbd9284ccff932be1d67 /internal/lsp/handlers_utils.go | |
| parent | 70f1d0e78c57dfa5beae779b3d392b6e6fa44c14 (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.go | 259 |
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 } |
