summaryrefslogtreecommitdiff
path: root/internal/lsp
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-26 08:19:26 +0300
committerPaul Buetow <paul@buetow.org>2025-09-26 08:19:26 +0300
commit9bcccbd80d36ae678d58cd8f83c4d0c790c16b48 (patch)
treeccbfdec5119daf443332db020824bc5845bbcf78 /internal/lsp
parent439ebb14fa6fb43bfda2e0ee6811c37f96b15ecc (diff)
Auto apply inline prompt completions
Diffstat (limited to 'internal/lsp')
-rw-r--r--internal/lsp/handlers_document.go45
-rw-r--r--internal/lsp/inline_prompt_completion_test.go67
2 files changed, 112 insertions, 0 deletions
diff --git a/internal/lsp/handlers_document.go b/internal/lsp/handlers_document.go
index 282ef26..0340866 100644
--- a/internal/lsp/handlers_document.go
+++ b/internal/lsp/handlers_document.go
@@ -94,6 +94,10 @@ func (s *Server) detectAndHandleChat(uri string) {
_, _, openChar, closeChar := s.inlineMarkers()
for i, raw := range d.lines {
if lineHasInlinePrompt(raw, openChar, closeChar) {
+ if s.currentLLMClient() != nil {
+ pos := Position{Line: i, Character: len(raw)}
+ go s.runInlinePrompt(uri, pos)
+ }
continue
}
// Find last non-space character index
@@ -210,6 +214,47 @@ func (s *Server) applyChatEdits(uri string, lineIdx int, lastNonSpace int, remov
s.clientApplyEdit("Hexai: insert chat response", we)
}
+func (s *Server) runInlinePrompt(uri string, pos Position) {
+ if s.currentLLMClient() == nil {
+ return
+ }
+ d := s.getDocument(uri)
+ if d == nil || pos.Line < 0 || pos.Line >= len(d.lines) {
+ return
+ }
+ line := d.lines[pos.Line]
+ _, _, openChar, closeChar := s.inlineMarkers()
+ if !lineHasInlinePrompt(line, openChar, closeChar) {
+ return
+ }
+ p := CompletionParams{TextDocument: TextDocumentIdentifier{URI: uri}, Position: Position{Line: pos.Line, Character: len(line)}}
+ p.Context = map[string]int{"triggerKind": 1}
+ above, current, below, funcCtx := s.lineContext(uri, p.Position)
+ docStr := s.buildDocString(p, above, current, below, funcCtx)
+ newFunc := s.isDefiningNewFunction(uri, p.Position)
+ extra, hasExtra := s.buildAdditionalContext(newFunc, uri, p.Position)
+ items, ok := s.tryLLMCompletion(p, above, current, below, funcCtx, docStr, hasExtra, extra)
+ if !ok || len(items) == 0 {
+ return
+ }
+ s.applyInlineCompletion(uri, items[0])
+}
+
+func (s *Server) applyInlineCompletion(uri string, item CompletionItem) {
+ var edits []TextEdit
+ if len(item.AdditionalTextEdits) > 0 {
+ edits = append(edits, item.AdditionalTextEdits...)
+ }
+ if item.TextEdit != nil {
+ edits = append(edits, *item.TextEdit)
+ }
+ if len(edits) == 0 {
+ return
+ }
+ we := WorkspaceEdit{Changes: map[string][]TextEdit{uri: edits}}
+ s.clientApplyEdit("Hexai: inline prompt", we)
+}
+
// buildChatHistory walks upwards from the current line to collect the most recent
// Q/A pairs in the in-editor transcript. Returns messages ending with current prompt.
func (s *Server) buildChatHistory(uri string, lineIdx int, currentPrompt string) []llm.Message {
diff --git a/internal/lsp/inline_prompt_completion_test.go b/internal/lsp/inline_prompt_completion_test.go
new file mode 100644
index 0000000..0b71d13
--- /dev/null
+++ b/internal/lsp/inline_prompt_completion_test.go
@@ -0,0 +1,67 @@
+package lsp
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "io"
+ "log"
+ "strings"
+ "testing"
+
+ "codeberg.org/snonux/hexai/internal/llm"
+)
+
+// fakeLLMInline returns a canned suggestion to help validate inline prompt handling.
+type fakeLLMInline struct{}
+
+func (fakeLLMInline) Chat(_ context.Context, _ []llm.Message, _ ...llm.RequestOption) (string, error) {
+ return "Здравей свят", nil
+}
+func (fakeLLMInline) Name() string { return "fake" }
+func (fakeLLMInline) DefaultModel() string { return "inline" }
+
+func TestHandleCompletionInlinePromptDoubleArrow(t *testing.T) {
+ var out bytes.Buffer
+ s := NewServer(bytes.NewReader(nil), &out, log.New(io.Discard, "", 0), ServerOptions{})
+ initServerDefaults(s)
+ s.llmClient = fakeLLMInline{}
+ uri := "file:///inline.go"
+ line := "hello world >>translate this into bulgarian>"
+ s.setDocument(uri, line)
+ p := CompletionParams{TextDocument: TextDocumentIdentifier{URI: uri}, Position: Position{Line: 0, Character: len(line)}}
+ ctx := struct {
+ TriggerKind int `json:"triggerKind"`
+ TriggerCharacter string `json:"triggerCharacter"`
+ }{TriggerKind: 1}
+ bctx, _ := json.Marshal(ctx)
+ p.Context = json.RawMessage(bctx)
+
+ s.handleCompletion(Request{JSONRPC: "2.0", ID: json.RawMessage("1"), Method: "textDocument/completion", Params: mustJSON(p)})
+ resp := captureResponse(t, &out)
+ var list CompletionList
+ b, _ := json.Marshal(resp.Result)
+ if err := json.Unmarshal(b, &list); err != nil {
+ t.Fatalf("decode completion list: %v", err)
+ }
+ if len(list.Items) == 0 {
+ t.Fatalf("expected completion items")
+ }
+ item := list.Items[0]
+ if got := strings.TrimSpace(item.Label); got == "" {
+ t.Fatalf("expected label for inline completion")
+ }
+ if len(item.AdditionalTextEdits) == 0 {
+ t.Fatalf("expected removal edits for inline prompt")
+ }
+ found := false
+ for _, edit := range item.AdditionalTextEdits {
+ if edit.Range.Start.Line == 0 && edit.Range.End.Line == 0 && edit.Range.Start.Character == 0 && edit.Range.End.Character == len(line) {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Fatalf("inline prompt removal edit missing: %+v", item.AdditionalTextEdits)
+ }
+}