summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-28 21:56:32 +0300
committerPaul Buetow <paul@buetow.org>2025-09-28 21:56:32 +0300
commitf14eb9199f4e1aee49594e590c08996244bb77b3 (patch)
tree6ecc23fda81ddc562bc6431b4e32bf69fd64fceb /internal
parent6103208e0fd382fb5f8c3e317fa28d888d42cb2b (diff)
Add slash toggle for completionsv0.14.0
Diffstat (limited to 'internal')
-rw-r--r--internal/lsp/chat_commands.go22
-rw-r--r--internal/lsp/chat_commands_test.go41
-rw-r--r--internal/lsp/completion_toggle_test.go46
-rw-r--r--internal/lsp/handlers_completion.go4
-rw-r--r--internal/lsp/server.go16
-rw-r--r--internal/version.go2
6 files changed, 130 insertions, 1 deletions
diff --git a/internal/lsp/chat_commands.go b/internal/lsp/chat_commands.go
index b2da7d4..11e5a28 100644
--- a/internal/lsp/chat_commands.go
+++ b/internal/lsp/chat_commands.go
@@ -22,6 +22,10 @@ func (s *Server) chatCommandResponse(uri string, lineIdx int, prompt string) (ch
return s.handleReloadCommand(), true
case strings.HasPrefix(trimmed, "/help"):
return s.handleHelpCommand(), true
+ case strings.HasPrefix(trimmed, "/disable"):
+ return s.handleDisableCompletionCommand(), true
+ case strings.HasPrefix(trimmed, "/enable"):
+ return s.handleEnableCompletionCommand(), true
default:
return chatCommandResult{message: fmt.Sprintf("Unknown command %q. Try /help?>", trimmed)}, true
}
@@ -31,6 +35,8 @@ func (s *Server) handleHelpCommand() chatCommandResult {
lines := []string{
"Available slash commands:",
"- /reload?> reload configuration from file (ignores env overrides)",
+ "- /disable?> disable auto-completions for this session",
+ "- /enable?> re-enable auto-completions",
}
return chatCommandResult{message: strings.Join(lines, "\n")}
}
@@ -50,3 +56,19 @@ func (s *Server) handleReloadCommand() chatCommandResult {
s.logger.Print(summary)
return chatCommandResult{message: summary}
}
+
+func (s *Server) handleDisableCompletionCommand() chatCommandResult {
+ prev := s.setCompletionsDisabled(true)
+ if prev {
+ return chatCommandResult{message: "Auto-completions were already disabled."}
+ }
+ return chatCommandResult{message: "Auto-completions disabled. Use /enable?> to restore."}
+}
+
+func (s *Server) handleEnableCompletionCommand() chatCommandResult {
+ prev := s.setCompletionsDisabled(false)
+ if !prev {
+ return chatCommandResult{message: "Auto-completions are already enabled."}
+ }
+ return chatCommandResult{message: "Auto-completions enabled."}
+}
diff --git a/internal/lsp/chat_commands_test.go b/internal/lsp/chat_commands_test.go
index 87cc1b4..0e31c7b 100644
--- a/internal/lsp/chat_commands_test.go
+++ b/internal/lsp/chat_commands_test.go
@@ -32,6 +32,9 @@ func TestHandleHelpCommandListsReload(t *testing.T) {
if !strings.Contains(res.message, "/reload?>") {
t.Fatalf("expected reload command in help output: %q", res.message)
}
+ if !strings.Contains(res.message, "/disable?>") || !strings.Contains(res.message, "/enable?>") {
+ t.Fatalf("expected completion toggle commands in help output: %q", res.message)
+ }
}
func TestHandleReloadCommandReloadsStore(t *testing.T) {
@@ -105,6 +108,7 @@ func TestDetectAndHandleChatExecutesSlashCommand(t *testing.T) {
s.configStore = store
var out bytes.Buffer
s.out = &out
+ s.setCompletionsDisabled(true) // chat commands should remain available when completions are disabled
uri := "file:///cmd.go"
s.setDocument(uri, "/reload>\n")
@@ -119,3 +123,40 @@ func TestDetectAndHandleChatExecutesSlashCommand(t *testing.T) {
t.Fatalf("expected reload summary logged, got %q", logBuf.String())
}
}
+
+func TestDisableEnableCommandsToggleCompletions(t *testing.T) {
+ s := newTestServer()
+ if s.completionDisabled() {
+ t.Fatalf("expected completions enabled initially")
+ }
+
+ if res, ok := s.chatCommandResponse("file:///x", 0, "/disable>"); !ok {
+ t.Fatalf("expected disable command to be handled")
+ } else if !strings.Contains(res.message, "disabled") {
+ t.Fatalf("unexpected disable message: %q", res.message)
+ }
+ if !s.completionDisabled() {
+ t.Fatalf("expected completions disabled after command")
+ }
+
+ if res, ok := s.chatCommandResponse("file:///x", 0, "/disable>"); !ok {
+ t.Fatalf("expected repeated disable command to be handled")
+ } else if !strings.Contains(res.message, "already disabled") {
+ t.Fatalf("expected already-disabled message, got %q", res.message)
+ }
+
+ if res, ok := s.chatCommandResponse("file:///x", 0, "/enable>"); !ok {
+ t.Fatalf("expected enable command to be handled")
+ } else if !strings.Contains(res.message, "enabled") {
+ t.Fatalf("unexpected enable message: %q", res.message)
+ }
+ if s.completionDisabled() {
+ t.Fatalf("expected completions enabled after command")
+ }
+
+ if res, ok := s.chatCommandResponse("file:///x", 0, "/enable>"); !ok {
+ t.Fatalf("expected repeated enable command to be handled")
+ } else if !strings.Contains(res.message, "already enabled") {
+ t.Fatalf("expected already-enabled message, got %q", res.message)
+ }
+}
diff --git a/internal/lsp/completion_toggle_test.go b/internal/lsp/completion_toggle_test.go
new file mode 100644
index 0000000..57ee1fd
--- /dev/null
+++ b/internal/lsp/completion_toggle_test.go
@@ -0,0 +1,46 @@
+package lsp
+
+import (
+ "bytes"
+ "encoding/json"
+ "strings"
+ "testing"
+)
+
+func TestHandleCompletionRespectsDisableCommand(t *testing.T) {
+ s := newTestServer()
+ var buf bytes.Buffer
+ s.out = &buf
+
+ // Disable completions and trigger handler
+ s.setCompletionsDisabled(true)
+
+ params := CompletionParams{TextDocument: TextDocumentIdentifier{URI: "file:///test.go"}, Position: Position{Line: 0, Character: 0}}
+ req := Request{JSONRPC: "2.0", ID: json.RawMessage("1"), Method: "textDocument/completion", Params: mustJSON(params)}
+
+ s.handleCompletion(req)
+
+ payload := buf.String()
+ parts := strings.SplitN(payload, "\r\n\r\n", 2)
+ if len(parts) != 2 {
+ t.Fatalf("unexpected response framing: %q", payload)
+ }
+ var resp Response
+ if err := json.Unmarshal([]byte(parts[1]), &resp); err != nil {
+ t.Fatalf("unmarshal response: %v", err)
+ }
+ resultMap, ok := resp.Result.(map[string]any)
+ if !ok {
+ t.Fatalf("expected map result, got %T", resp.Result)
+ }
+ switch v := resultMap["items"].(type) {
+ case []any:
+ if len(v) != 0 {
+ t.Fatalf("expected no completion items when disabled, got %d", len(v))
+ }
+ case nil:
+ // ok: encoder emitted null for slice
+ default:
+ t.Fatalf("expected items slice or null, got %T", v)
+ }
+}
diff --git a/internal/lsp/handlers_completion.go b/internal/lsp/handlers_completion.go
index 78e685a..db6866b 100644
--- a/internal/lsp/handlers_completion.go
+++ b/internal/lsp/handlers_completion.go
@@ -30,6 +30,10 @@ type completionPlan struct {
}
func (s *Server) handleCompletion(req Request) {
+ if s.completionDisabled() {
+ s.reply(req.ID, CompletionList{IsIncomplete: false, Items: nil}, nil)
+ return
+ }
var p CompletionParams
var docStr string
if err := json.Unmarshal(req.Params, &p); err == nil {
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 974b926..8e210b4 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -48,6 +48,8 @@ type Server struct {
nextID int64
lastLLMCall time.Time
+ completionsDisabled bool
+
// Dispatch table for JSON-RPC methods → handler functions
handlers map[string]func(Request)
}
@@ -334,6 +336,20 @@ func (s *Server) storePendingCompletion(key string, items []CompletionItem) {
s.mu.Unlock()
}
+func (s *Server) setCompletionsDisabled(disabled bool) bool {
+ s.mu.Lock()
+ prev := s.completionsDisabled
+ s.completionsDisabled = disabled
+ s.mu.Unlock()
+ return prev
+}
+
+func (s *Server) completionDisabled() bool {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ return s.completionsDisabled
+}
+
func (s *Server) takePendingCompletion(key string) []CompletionItem {
s.mu.Lock()
defer s.mu.Unlock()
diff --git a/internal/version.go b/internal/version.go
index cda626b..f781c7a 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -1,4 +1,4 @@
// Summary: Hexai semantic version identifier used by CLI and LSP binaries.
package internal
-const Version = "0.13.0"
+const Version = "0.14.0"