summaryrefslogtreecommitdiff
path: root/internal/lsp
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-26 07:46:14 +0300
committerPaul Buetow <paul@buetow.org>2025-09-26 07:46:14 +0300
commitd0330d02ff040326216ab940a767490cb2de09ce (patch)
tree36c73d4562630c7f56f6f3c8ae6f8f3d7e36f102 /internal/lsp
parentcff326a7e562eb1acc8027f129b3fd0035eb7d22 (diff)
Allow slash commands without prefix
Diffstat (limited to 'internal/lsp')
-rw-r--r--internal/lsp/chat_commands_test.go39
-rw-r--r--internal/lsp/handlers_document.go42
2 files changed, 61 insertions, 20 deletions
diff --git a/internal/lsp/chat_commands_test.go b/internal/lsp/chat_commands_test.go
index bedfaed..f9bd6a0 100644
--- a/internal/lsp/chat_commands_test.go
+++ b/internal/lsp/chat_commands_test.go
@@ -80,3 +80,42 @@ func TestHandleReloadCommandReloadsStore(t *testing.T) {
t.Fatalf("expected summary logged, got %q", logBuf.String())
}
}
+
+func TestDetectAndHandleChatExecutesSlashCommand(t *testing.T) {
+ tmp := t.TempDir()
+ configDir := filepath.Join(tmp, "hexai")
+ if err := os.MkdirAll(configDir, 0o755); err != nil {
+ t.Fatalf("mkdir: %v", err)
+ }
+ configPath := filepath.Join(configDir, "config.toml")
+ if err := os.WriteFile(configPath, []byte("[general]\nmax_tokens = 128\n"), 0o644); err != nil {
+ t.Fatalf("write config: %v", err)
+ }
+ t.Setenv("XDG_CONFIG_HOME", tmp)
+ t.Setenv("HEXAI_MAX_TOKENS", "")
+
+ var logBuf bytes.Buffer
+ logger := log.New(&logBuf, "", 0)
+
+ initial := appconfig.Load(logger)
+ store := runtimeconfig.New(initial)
+
+ s := newTestServer()
+ s.logger = logger
+ s.configStore = store
+ var out bytes.Buffer
+ s.out = &out
+
+ uri := "file:///cmd.go"
+ s.setDocument(uri, "/reload>\n")
+
+ s.detectAndHandleChat(uri)
+
+ outStr := out.String()
+ if !strings.Contains(outStr, "Reloaded config") {
+ t.Fatalf("expected reload summary in applyEdit payload, got %q", outStr)
+ }
+ if !strings.Contains(logBuf.String(), "Reloaded config") {
+ t.Fatalf("expected reload summary logged, got %q", logBuf.String())
+ }
+}
diff --git a/internal/lsp/handlers_document.go b/internal/lsp/handlers_document.go
index e82e683..69f482f 100644
--- a/internal/lsp/handlers_document.go
+++ b/internal/lsp/handlers_document.go
@@ -104,28 +104,37 @@ func (s *Server) detectAndHandleChat(uri string) {
if j < 0 {
continue
}
- // Check suffix/prefix according to configuration
+ // Check suffix and derive the prompt text before validating prefixes
if suffix == "" {
continue
}
- // Last non-space must equal suffix
if string(raw[j]) != suffix {
continue
}
- // Require at least one char before suffix and that char must be in chatPrefixes
- if j < 1 {
+ removeCount := len(suffix)
+ base := raw[:j+1-removeCount]
+ prompt := strings.TrimSpace(base)
+ if prompt == "" {
continue
}
- prev := string(raw[j-1])
- isTrigger := false
- for _, pfx := range prefixes {
- if prev == pfx {
- isTrigger = true
- break
+ // Slash commands (`/foo>`) do not require a prefix trigger.
+ isCommand := strings.HasPrefix(prompt, "/")
+ if !isCommand {
+ // Require at least one char before suffix and that char must be in chatPrefixes
+ if j < 1 {
+ continue
+ }
+ prev := string(raw[j-1])
+ match := false
+ for _, pfx := range prefixes {
+ if prev == pfx {
+ match = true
+ break
+ }
+ }
+ if !match {
+ continue
}
- }
- if !isTrigger {
- continue
}
// Avoid double-answering: if the next non-empty line starts with '>' we skip.
k := i + 1
@@ -135,13 +144,6 @@ func (s *Server) detectAndHandleChat(uri string) {
if k < len(d.lines) && strings.HasPrefix(strings.TrimSpace(d.lines[k]), ">") {
continue
}
- // Derive prompt by removing only the trailing '>'
- removeCount := len(suffix)
- base := raw[:j+1-removeCount]
- prompt := strings.TrimSpace(base)
- if prompt == "" {
- continue
- }
lineIdx := i
lastIdx := j
if resp, ok := s.chatCommandResponse(uri, lineIdx, prompt); ok {