diff options
| author | Paul Buetow <paul@buetow.org> | 2025-08-16 23:16:54 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-08-16 23:16:54 +0300 |
| commit | 765eda955eb811d08d867ff4d3914fc6d60c22dd (patch) | |
| tree | fdc87da6af9d86dbda2ea9ab08244e93fd167188 /internal/lsp | |
| parent | 1b01e35c34b953cbf51298f4650dc3215c382a4f (diff) | |
refactor(config): drop env-based config (except OPENAI_API_KEY)
- Switch to config-file-only; only OPENAI_API_KEY read from env.\n- llm: replace env autodetect with Config + NewFromConfig; add newOpenAI/newOllama.\n- lsp: NewServer now accepts injected llm.Client.\n- cli: remove env overrides; extend appConfig with provider-specific fields; build client from config + OPENAI_API_KEY.\n- docs: update README (config-only, defaults to OpenAI, minimal example); simplify flags table.\n- add config.json.example.\n- prompts: enforce ;text; (no spaces) and add ;;text; to remove entire line; tests added.
Diffstat (limited to 'internal/lsp')
| -rw-r--r-- | internal/lsp/handlers.go | 106 | ||||
| -rw-r--r-- | internal/lsp/handlers_test.go | 57 | ||||
| -rw-r--r-- | internal/lsp/server.go | 9 |
3 files changed, 137 insertions, 35 deletions
diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go index d8c13a1..8edfbb6 100644 --- a/internal/lsp/handlers.go +++ b/internal/lsp/handlers.go @@ -237,36 +237,86 @@ func (s *Server) tryLLMCompletion(p CompletionParams, above, current, below, fun // collectPromptRemovalEdits returns edits to remove all inline prompt markers. // Supported form (inclusive): -// - ";...;" (optional single space after trailing ';') +// - ";...;" where there is no space immediately after the first ';' +// and no space immediately before the last ';'. An optional single space +// after the trailing ';' is also removed for cleanliness. // Multiple markers per line are supported. func (s *Server) collectPromptRemovalEdits(uri string) []TextEdit { - d := s.getDocument(uri) - if d == nil || len(d.lines) == 0 { - return nil - } - var edits []TextEdit - for i, line := range d.lines { - // Scan for ;...; markers - 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 - } - endChar := j + 1 + k + 1 // include trailing ';' - if endChar < len(line) && line[endChar] == ' ' { - endChar++ - } - edits = append(edits, TextEdit{Range: Range{Start: Position{Line: i, Character: j}, End: Position{Line: i, Character: endChar}}, NewText: ""}) - startSemi = endChar - } - } - return edits + d := s.getDocument(uri) + if d == nil || len(d.lines) == 0 { + return nil + } + var edits []TextEdit + for i, line := range d.lines { + // If the line contains a double-semicolon trigger of the form + // ";;text;" (no space after the ";;" and no space before the closing ';'), + // remove the entire line. + removeWholeLine := false + { + pos := 0 + for pos < len(line) { + j := strings.Index(line[pos:], ";;") + if j < 0 { break } + j += pos + // ensure there's a non-space after the two semicolons + if j+2 >= len(line) || line[j+2] == ' ' { pos = j + 2; continue } + // find closing ';' after the content + k := strings.Index(line[j+2:], ";") + if k < 0 { break } + closeIdx := j + 2 + k + // ensure char before closing ';' is not a space + if closeIdx-1 < 0 || line[closeIdx-1] == ' ' { pos = closeIdx + 1; continue } + removeWholeLine = true + break + } + } + if removeWholeLine { + edits = append(edits, TextEdit{Range: Range{Start: Position{Line: i, Character: 0}, End: Position{Line: i, Character: len(line)}}, NewText: ""}) + continue + } + // Scan for ;...; markers that have no spaces directly inside the semicolons + 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 + } + // Require no space immediately after the first ';' + if j+1 >= len(line) || line[j+1] == ' ' { + startSemi = j + 1 + continue + } + // Ignore patterns that start with double semicolon here; handled above + if line[j+1] == ';' { + startSemi = j + 2 + continue + } + // Index of the closing ';' + closeIdx := j + 1 + k + // Require no space immediately before the closing ';' + if closeIdx-1 < 0 || line[closeIdx-1] == ' ' { + startSemi = closeIdx + 1 + continue + } + // Require at least one character between the semicolons + if closeIdx-(j+1) < 1 { + startSemi = closeIdx + 1 + continue + } + endChar := closeIdx + 1 // include trailing ';' + if endChar < len(line) && line[endChar] == ' ' { + endChar++ + } + edits = append(edits, TextEdit{Range: Range{Start: Position{Line: i, Character: j}, End: Position{Line: i, Character: endChar}}, NewText: ""}) + startSemi = endChar + } + } + return edits } func inParamList(current string, cursor int) bool { diff --git a/internal/lsp/handlers_test.go b/internal/lsp/handlers_test.go index 6ce1e5d..3ebddfb 100644 --- a/internal/lsp/handlers_test.go +++ b/internal/lsp/handlers_test.go @@ -147,3 +147,60 @@ no markers here` t.Fatalf("e0 start not at ;") } } + +func TestCollectPromptRemovalEdits_SkipSpacedMarkers(t *testing.T) { + s := newTestServer() + uri := "file:///y.go" + // Only ;ok; should be removed; "; spaced ;" must be ignored + src := `prefix ;ok; middle ; spaced ; suffix` + s.setDocument(uri, src) + edits := s.collectPromptRemovalEdits(uri) + if len(edits) != 1 { + t.Fatalf("expected 1 edit (only ;ok;), got %d", len(edits)) + } + // Ensure the removed region starts at the first ';' of ;ok; + line := s.getDocument(uri).lines[0] + wantStart := strings.Index(line, ";ok;") + if wantStart < 0 { + t.Fatalf("test setup: could not find ;ok; in %q", line) + } + if edits[0].Range.Start.Line != 0 || edits[0].Range.Start.Character != wantStart { + t.Fatalf("unexpected first edit start: got line=%d char=%d want line=0 char=%d", edits[0].Range.Start.Line, edits[0].Range.Start.Character, wantStart) + } +} + +func TestCollectPromptRemovalEdits_DoubleSemicolonRemovesWholeLine(t *testing.T) { + s := newTestServer() + uri := "file:///z.go" + line0 := "keep" + line1 := ";;todo; remove this whole line" + line2 := "keep ;ok; end" + src := strings.Join([]string{line0, line1, line2}, "\n") + s.setDocument(uri, src) + edits := s.collectPromptRemovalEdits(uri) + if len(edits) != 2 { + t.Fatalf("expected 2 edits (whole line + ;ok;), got %d", len(edits)) + } + // Find the whole-line removal for line1 + found := false + for _, e := range edits { + if e.Range.Start.Line == 1 && e.Range.Start.Character == 0 && e.Range.End.Line == 1 && e.Range.End.Character == len(line1) { + found = true + break + } + } + if !found { + t.Fatalf("did not find whole-line removal edit for line 1") + } +} + +func TestCollectPromptRemovalEdits_SkipSpacedDouble(t *testing.T) { + s := newTestServer() + uri := "file:///w.go" + src := "prefix ;; spaced ; suffix" + s.setDocument(uri, src) + edits := s.collectPromptRemovalEdits(uri) + if len(edits) != 0 { + t.Fatalf("expected 0 edits for spaced double-semicolon trigger, got %d", len(edits)) + } +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index ef51636..bfdbca2 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -35,7 +35,7 @@ type Server struct { startTime time.Time } -func NewServer(r io.Reader, w io.Writer, logger *log.Logger, logContext bool, maxTokens int, contextMode string, windowLines int, maxContextTokens int, noDiskIO bool) *Server { +func NewServer(r io.Reader, w io.Writer, logger *log.Logger, logContext bool, maxTokens int, contextMode string, windowLines int, maxContextTokens int, noDiskIO bool, client llm.Client) *Server { s := &Server{in: bufio.NewReader(r), out: w, logger: logger, docs: make(map[string]*document), logContext: logContext} if maxTokens <= 0 { maxTokens = 500 @@ -55,12 +55,7 @@ func NewServer(r io.Reader, w io.Writer, logger *log.Logger, logContext bool, ma s.maxContextTokens = maxContextTokens s.noDiskIO = noDiskIO s.startTime = time.Now() - if c, err := llm.NewDefault(); err != nil { - logging.Logf("lsp ", "llm disabled: %v", err) - } else { - s.llmClient = c - logging.Logf("lsp ", "llm enabled provider=%s model=%s", c.Name(), c.DefaultModel()) - } + s.llmClient = client return s } |
