summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-08-22 19:49:44 +0300
committerPaul Buetow <paul@buetow.org>2025-08-22 19:49:44 +0300
commit0478bd470c523c8a6e07d4fa4f11ca987c38cea9 (patch)
treed31b8c40d6eb44bc4acd7149bc21ba18de50b204
parentdcb0eaee01ddcb1fa931970df246764f09383c0d (diff)
chat: remove ';;' as in-editor chat trigger to avoid conflict with inline ';;text;' completion; update docs
-rw-r--r--docs/usage-examples.md3
-rw-r--r--internal/lsp/handlers.go41
2 files changed, 37 insertions, 7 deletions
diff --git a/docs/usage-examples.md b/docs/usage-examples.md
index 8d8014e..8d628d8 100644
--- a/docs/usage-examples.md
+++ b/docs/usage-examples.md
@@ -32,7 +32,7 @@ Note: additional LSPs (`gopls`, `golangci-lint-lsp`) are optional; Hexai works w
Ask a question at the end of a line and receive the answer inline.
-- End your question line with a trigger: `..`, `??`, `!!`, `::`, or `;;`.
+- End your question line with a trigger: `..`, `??`, `!!`, or `::`.
- Hexai removes the trailing marker (last char for `..`/`??`/`!!`/`::`, both for `;;`).
- It inserts a blank line, then a reply line prefixed with `> `, then one extra newline so most
editors place the cursor on a fresh blank line after the answer.
@@ -55,6 +55,7 @@ Context: Hexai includes up to the three most recent Q/A pairs above the question
Hexai supports inline prompt tags you can type in code to request an action from the LLM and then auto-clean the tag. The strict semicolon form is supported:
- `;do something;` — Hexai uses the text between semicolons as the instruction and removes only the prompt. Strict form requires no space after the first `;` and no space before the closing `;`.
+- `;;do someting;` - Same as above, but replace the current line with the completion
Spaced variants (e.g., `; spaced ;`) are ignored.
diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go
index 987bcbe..8c4fd51 100644
--- a/internal/lsp/handlers.go
+++ b/internal/lsp/handlers.go
@@ -527,7 +527,7 @@ func (s *Server) detectAndHandleChat(uri string) {
continue
}
pair := raw[j-1 : j+1]
- isTrigger := pair == ".." || pair == "??" || pair == "!!" || pair == "::" || pair == ";;"
+ isTrigger := pair == ".." || pair == "??" || pair == "!!" || pair == "::"
if !isTrigger {
continue
}
@@ -537,9 +537,8 @@ func (s *Server) detectAndHandleChat(uri string) {
if k < len(d.lines) && strings.HasPrefix(strings.TrimSpace(d.lines[k]), ">") {
continue
}
- // Derive prompt by removing 1 trailing char for punctuation pairs; remove both for ';;'
+ // Derive prompt by removing 1 trailing char for punctuation pairs
removeCount := 1
- if pair == ";;" { removeCount = 2 }
base := raw[:j+1-removeCount]
prompt := strings.TrimSpace(base)
if prompt == "" {
@@ -742,8 +741,9 @@ func (s *Server) tryLLMCompletion(p CompletionParams, above, current, below, fun
logging.AnsiGreen, logging.PreviewForLog(cleaned), logging.AnsiBase)
return s.makeCompletionItems(cleaned, inParams, current, p, docStr), true, false
}
- // If there is a bare ';;' (no valid ';;text;'), do not auto-trigger unless it was a manual invoke.
- if strings.Contains(current, ";;") && !hasDoubleSemicolonTrigger(current) && !manualInvoke {
+ // If there is a bare ';;' on the current or next line (no valid ';;text;'),
+ // do not auto-trigger unless it was a manual invoke.
+ if (isBareDoubleSemicolon(current) || isBareDoubleSemicolon(below)) && !manualInvoke {
logging.Logf("lsp ", "%scompletion skip=empty-double-semicolon line=%d char=%d current=%q%s", logging.AnsiYellow, p.Position.Line, p.Position.Character, trimLen(current), logging.AnsiBase)
return []CompletionItem{}, true, false
}
@@ -950,7 +950,11 @@ func (s *Server) isTriggerEvent(p CompletionParams, current string) bool {
b, _ := json.Marshal(p.Context)
_ = json.Unmarshal(b, &ctx)
}
- // TriggerKind 1 = Invoked (manual) — always allow
+ // If the line contains a bare ';;' (no ';;text;'), do not treat as a trigger source.
+ if strings.Contains(current, ";;") && !hasDoubleSemicolonTrigger(current) {
+ return false
+ }
+ // TriggerKind 1 = Invoked (manual) — always allow (unless bare ';;' above)
if ctx.TriggerKind == 1 {
return true
}
@@ -974,6 +978,10 @@ func (s *Server) isTriggerEvent(p CompletionParams, current string) bool {
if idx <= 0 || idx > len(current) {
return false
}
+ // Bare ';;' should not trigger via fallback char either
+ if strings.Contains(current, ";;") && !hasDoubleSemicolonTrigger(current) {
+ return false
+ }
ch := string(current[idx-1])
for _, c := range s.triggerChars {
if c == ch {
@@ -1217,6 +1225,27 @@ func lineHasInlinePrompt(line string) bool {
return hasDoubleSemicolonTrigger(line)
}
+// 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:])
+ // Bare if nothing follows or only semicolons/spaces remain without closing pattern
+ if rest == "" || rest == ";" {
+ return true
+ }
+ }
+ return false
+}
+
// stripDuplicateAssignmentPrefix removes a duplicated assignment prefix (e.g.,
// "name :=") from the beginning of the model suggestion when that same prefix
// already appears immediately to the left of the cursor on the current line.