summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-10-03 23:50:49 +0300
committerPaul Buetow <paul@buetow.org>2025-10-03 23:50:49 +0300
commit420b5aebf888c638ac096e1476c06eac979ac257 (patch)
tree59434cbc37837399d7b5bc7920ffd7be62f1fc7d
parente36a5446bc62842ae3b3e165f66fecb7285a8c6a (diff)
Switch inline prompt markers to >! prefixv0.15.1
-rw-r--r--config.toml.example2
-rw-r--r--docs/usage.md8
-rw-r--r--internal/appconfig/config.go4
-rw-r--r--internal/appconfig/config_alias_test.go39
-rw-r--r--internal/appconfig/config_test.go2
-rw-r--r--internal/hexaicli/run_test.go12
-rw-r--r--internal/hexaicli/testhelpers_test.go10
-rw-r--r--internal/lsp/chat_commands_test.go2
-rw-r--r--internal/lsp/codeaction_custom_test.go2
-rw-r--r--internal/lsp/codeaction_test.go2
-rw-r--r--internal/lsp/completion_prefix_strip_test.go20
-rw-r--r--internal/lsp/coverage_add_test.go6
-rw-r--r--internal/lsp/document_test.go4
-rw-r--r--internal/lsp/handlers.go23
-rw-r--r--internal/lsp/handlers_completion.go14
-rw-r--r--internal/lsp/handlers_document.go8
-rw-r--r--internal/lsp/handlers_helpers_test.go20
-rw-r--r--internal/lsp/handlers_test.go28
-rw-r--r--internal/lsp/handlers_utils.go208
-rw-r--r--internal/lsp/helpers_inline_prompt_test.go8
-rw-r--r--internal/lsp/helpers_more_test.go14
-rw-r--r--internal/lsp/init_and_trigger_test.go4
-rw-r--r--internal/lsp/inline_prompt_completion_test.go2
-rw-r--r--internal/lsp/instruction_table_test.go2
-rw-r--r--internal/lsp/postprocess_indent_test.go2
-rw-r--r--internal/lsp/provider_native_success_test.go2
-rw-r--r--internal/lsp/server.go2
-rw-r--r--internal/lsp/triggers_config_test.go4
-rw-r--r--internal/runtimeconfig/store_test.go2
-rw-r--r--internal/version.go2
30 files changed, 295 insertions, 163 deletions
diff --git a/config.toml.example b/config.toml.example
index 9aa217f..cd10e73 100644
--- a/config.toml.example
+++ b/config.toml.example
@@ -20,7 +20,7 @@ manual_invoke_min_prefix = 0 # required identifier chars for manual invo
trigger_characters = [".", ":", "/", "_", " "]
[inline]
-inline_open = ">" # single-character
+inline_open = ">!" # marker prefix for inline prompts
inline_close = ">" # single-character
[chat]
diff --git a/docs/usage.md b/docs/usage.md
index 1dadb1c..49ed4e6 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -33,7 +33,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 `:>`.
-- Hexai removes only the trailing `>` from the question line (and keeps your trailing punctuation). Inline code-completion triggers now use `>text>` (inline) or `>>text>` (line-replace).
+- Hexai removes only the trailing `>` from the question line (and keeps your trailing punctuation). Inline code-completion triggers now use `>!text>` (inline) or `>>!text>` (line-replace).
- 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.
- If a `>` reply already exists below the question, Hexai won’t answer again.
@@ -52,10 +52,10 @@ Context: Hexai includes up to the three most recent Q/A pairs above the question
## Inline triggers
-Hexai supports inline prompt tags you can type in code to request an action from the LLM and then auto-clean the tag. The new `>`-based forms are:
+Hexai supports inline prompt tags you can type in code to request an action from the LLM and then auto-clean the tag. The new `>!`-based forms are:
-- `>do something>` — uses the text between `>` markers as the instruction and removes only the prompt. Strict form requires no space after the first `>` and no space before the closing `>`.
-- `>>do something>` — same as above, but replaces the entire current line with the completion.
+- `>!do something>` — uses the text between markers as the instruction and removes only the prompt. Strict form requires no space after `>!` and no space before the closing `>`.
+- `>>!do something>` — same as above, but replaces the entire current line with the completion.
Spaced variants (e.g., `> spaced >`) are ignored.
diff --git a/internal/appconfig/config.go b/internal/appconfig/config.go
index e5a8d5f..59ffd89 100644
--- a/internal/appconfig/config.go
+++ b/internal/appconfig/config.go
@@ -44,7 +44,7 @@ type App struct {
TriggerCharacters []string `json:"trigger_characters" toml:"trigger_characters"`
Provider string `json:"provider" toml:"provider"`
- // Inline prompt trigger characters (default: >text> and >>text>)
+ // Inline prompt trigger characters (default: >!text> and >>!text>)
InlineOpen string `json:"inline_open" toml:"inline_open"`
InlineClose string `json:"inline_close" toml:"inline_close"`
// In-editor chat triggers (default: suffix ">" after one of [?, !, :, ;])
@@ -141,7 +141,7 @@ func newDefaultConfig() App {
CompletionDebounceMs: 800,
CompletionThrottleMs: 0,
// Inline/chat trigger defaults
- InlineOpen: ">",
+ InlineOpen: ">!",
InlineClose: ">",
ChatSuffix: ">",
ChatPrefixes: []string{"?", "!", ":", ";"},
diff --git a/internal/appconfig/config_alias_test.go b/internal/appconfig/config_alias_test.go
index 6cc5bda..da7909e 100644
--- a/internal/appconfig/config_alias_test.go
+++ b/internal/appconfig/config_alias_test.go
@@ -1,20 +1,20 @@
package appconfig
import (
- "log"
- "os"
- "path/filepath"
- "testing"
+ "log"
+ "os"
+ "path/filepath"
+ "testing"
)
func TestOpenAIPresets_AliasResolution(t *testing.T) {
- dir := t.TempDir()
- t.Setenv("XDG_CONFIG_HOME", dir)
- cfgDir := filepath.Join(dir, "hexai")
- if err := os.MkdirAll(cfgDir, 0o755); err != nil {
- t.Fatalf("mkdir: %v", err)
- }
- toml := `
+ dir := t.TempDir()
+ t.Setenv("XDG_CONFIG_HOME", dir)
+ cfgDir := filepath.Join(dir, "hexai")
+ if err := os.MkdirAll(cfgDir, 0o755); err != nil {
+ t.Fatalf("mkdir: %v", err)
+ }
+ toml := `
[provider]
name = "openai"
@@ -24,13 +24,12 @@ model = "codex"
[openai.presets]
codex = "gpt-5-codex"
`
- path := filepath.Join(cfgDir, "config.toml")
- if err := os.WriteFile(path, []byte(toml), 0o644); err != nil {
- t.Fatalf("write: %v", err)
- }
- cfg := Load(log.New(os.Stderr, "test ", 0))
- if cfg.OpenAIModel != "gpt-5-codex" {
- t.Fatalf("expected alias to resolve to gpt-5-codex, got %q", cfg.OpenAIModel)
- }
+ path := filepath.Join(cfgDir, "config.toml")
+ if err := os.WriteFile(path, []byte(toml), 0o644); err != nil {
+ t.Fatalf("write: %v", err)
+ }
+ cfg := Load(log.New(os.Stderr, "test ", 0))
+ if cfg.OpenAIModel != "gpt-5-codex" {
+ t.Fatalf("expected alias to resolve to gpt-5-codex, got %q", cfg.OpenAIModel)
+ }
}
-
diff --git a/internal/appconfig/config_test.go b/internal/appconfig/config_test.go
index 4ae04d8..2c00f68 100644
--- a/internal/appconfig/config_test.go
+++ b/internal/appconfig/config_test.go
@@ -365,7 +365,7 @@ manual_invoke_min_prefix = 3
trigger_characters = [".", ":"]
[inline]
-inline_open = ">"
+inline_open = ">!"
inline_close = ">"
[chat]
diff --git a/internal/hexaicli/run_test.go b/internal/hexaicli/run_test.go
index dfde068..991965e 100644
--- a/internal/hexaicli/run_test.go
+++ b/internal/hexaicli/run_test.go
@@ -125,12 +125,20 @@ func TestRunWithClient_ErrorPrint(t *testing.T) {
func TestRun_OpenAI_NoKey_ShowsError(t *testing.T) {
dir := testingTempDir(t)
- // write config with provider=openai
- writeTOML(t, filepath.Join(dir, "hexai", "config.toml"), map[string]string{"provider": "openai", "openai_model": "gpt-x"})
+ // write config with provider=openai using sectioned tables
+ configPath := filepath.Join(dir, "hexai", "config.toml")
+ writeConfigString(t, configPath, `
+[provider]
+name = "openai"
+
+[openai]
+model = "gpt-x"
+`)
t.Setenv("XDG_CONFIG_HOME", dir)
// Ensure no OpenAI API key is present in environment
t.Setenv("HEXAI_OPENAI_API_KEY", "")
t.Setenv("OPENAI_API_KEY", "")
+ t.Setenv("HEXAI_PROVIDER", "")
var out, errb bytes.Buffer
// Run expects parsed flags; here args irrelevant
err := Run(context.Background(), []string{"hello"}, strings.NewReader(""), &out, &errb)
diff --git a/internal/hexaicli/testhelpers_test.go b/internal/hexaicli/testhelpers_test.go
index 93f1e3d..4cc04f7 100644
--- a/internal/hexaicli/testhelpers_test.go
+++ b/internal/hexaicli/testhelpers_test.go
@@ -79,4 +79,14 @@ func writeTOML(t *testing.T, path string, m map[string]string) {
}
}
+func writeConfigString(t *testing.T, path string, contents string) {
+ t.Helper()
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ t.Fatalf("mkdir: %v", err)
+ }
+ if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
+ t.Fatalf("write: %v", err)
+ }
+}
+
func testingTempDir(t *testing.T) string { t.Helper(); return t.TempDir() }
diff --git a/internal/lsp/chat_commands_test.go b/internal/lsp/chat_commands_test.go
index 0e31c7b..ffe31dd 100644
--- a/internal/lsp/chat_commands_test.go
+++ b/internal/lsp/chat_commands_test.go
@@ -50,6 +50,7 @@ func TestHandleReloadCommandReloadsStore(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", tmp)
t.Setenv("HEXAI_MAX_TOKENS", "321")
+ t.Setenv("HEXAI_PROVIDER", "")
var logBuf bytes.Buffer
logger := log.New(&logBuf, "", 0)
@@ -96,6 +97,7 @@ func TestDetectAndHandleChatExecutesSlashCommand(t *testing.T) {
}
t.Setenv("XDG_CONFIG_HOME", tmp)
t.Setenv("HEXAI_MAX_TOKENS", "")
+ t.Setenv("HEXAI_PROVIDER", "")
var logBuf bytes.Buffer
logger := log.New(&logBuf, "", 0)
diff --git a/internal/lsp/codeaction_custom_test.go b/internal/lsp/codeaction_custom_test.go
index ea8ae82..36f99d4 100644
--- a/internal/lsp/codeaction_custom_test.go
+++ b/internal/lsp/codeaction_custom_test.go
@@ -30,7 +30,7 @@ func capResp(t *testing.T, buf *bytes.Buffer) Response {
func TestHandleCodeAction_ListsCustomActions(t *testing.T) {
var out bytes.Buffer
cfg := appconfig.App{
- InlineOpen: ">",
+ InlineOpen: ">!",
InlineClose: ">",
ChatSuffix: ">",
ChatPrefixes: []string{"?", "!", ":", ";"},
diff --git a/internal/lsp/codeaction_test.go b/internal/lsp/codeaction_test.go
index 29cb416..af08fe1 100644
--- a/internal/lsp/codeaction_test.go
+++ b/internal/lsp/codeaction_test.go
@@ -23,7 +23,7 @@ func TestBuildRewriteCodeAction_LazyAndResolves(t *testing.T) {
s := newTestServer()
s.llmClient = fakeLLM{resp: "REWRITTEN"}
p := CodeActionParams{TextDocument: TextDocumentIdentifier{URI: "file:///t.go"}, Range: Range{Start: Position{Line: 1, Character: 2}, End: Position{Line: 3, Character: 4}}}
- sel := ">rewrite>\nold code"
+ sel := ">!rewrite>\nold code"
ca := s.buildRewriteCodeAction(p, sel)
if ca == nil {
t.Fatalf("expected code action")
diff --git a/internal/lsp/completion_prefix_strip_test.go b/internal/lsp/completion_prefix_strip_test.go
index e0c655c..c8e2bd7 100644
--- a/internal/lsp/completion_prefix_strip_test.go
+++ b/internal/lsp/completion_prefix_strip_test.go
@@ -69,12 +69,12 @@ func TestTryLLMCompletion_InlinePromptAlwaysTriggers(t *testing.T) {
cfg.TriggerCharacters = []string{".", ":", "/", "_"}
s.cfg = cfg
s.llmClient = fakeLLM{resp: "replacement"}
- line := "prefix >do something> suffix"
+ line := "prefix >!do something> suffix"
// No trigger char immediately before cursor; place cursor at end
p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://inline.go"}}
items, ok, _ := s.tryLLMCompletion(p, "", line, "", "", "", false, "")
if !ok || len(items) == 0 {
- t.Fatalf("expected completion to trigger on inline >text> prompt")
+ t.Fatalf("expected completion to trigger on inline >!text> prompt")
}
}
@@ -87,7 +87,7 @@ func TestTryLLMCompletion_DoubleOpenEmpty_DoesNotAutoTrigger(t *testing.T) {
s.cfg = cfg
fake := &countingLLM{}
s.llmClient = fake
- line := ">> " // empty content after double-open should not force-trigger
+ line := ">>! " // empty content after double-open should not force-trigger
p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://empty-inline.go"}}
items, ok, _ := s.tryLLMCompletion(p, "", line, "", "", "", false, "")
if !ok {
@@ -102,16 +102,16 @@ func TestTryLLMCompletion_DoubleOpenEmpty_DoesNotAutoTrigger(t *testing.T) {
}
func TestHasDoubleSemicolonTrigger_Variants(t *testing.T) {
- if hasDoubleOpenTrigger(">>", '>', '>') {
+ if hasDoubleOpenTrigger(">>!", ">!", '>', '>') {
t.Fatalf("bare double-open should not trigger")
}
- if hasDoubleOpenTrigger(">> ", '>', '>') {
+ if hasDoubleOpenTrigger(">>! ", ">!", '>', '>') {
t.Fatalf("double-open followed by space should not trigger")
}
- if hasDoubleOpenTrigger(">>>", '>', '>') {
+ if hasDoubleOpenTrigger(">>!>", ">!", '>', '>') {
t.Fatalf("';;;' should not trigger (no content)")
}
- if !hasDoubleOpenTrigger(">>x>", '>', '>') {
+ if !hasDoubleOpenTrigger(">>!x>", ">!", '>', '>') {
t.Fatalf("expected trigger for ';;x;' pattern")
}
}
@@ -126,7 +126,7 @@ func TestBareDoubleOpenPreventsAutoTriggerEvenWithOtherTriggers(t *testing.T) {
fake := &countingLLM{}
s.llmClient = fake
// Place a '.' earlier but also include bare double-open at end; should not auto-trigger
- line := "obj. call >>"
+ line := "obj. call >>!"
p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://bare-ds.go"}}
items, ok, _ := s.tryLLMCompletion(p, "", line, "", "", "", false, "")
if !ok {
@@ -150,7 +150,7 @@ func TestBareDoubleOpenOnNextLine_PreventsAutoTrigger(t *testing.T) {
fake := &countingLLM{}
s.llmClient = fake
current := "expression := flag.String(\"expression\", \"\", \"Expression to evaluate\")"
- below := ">>"
+ below := ">>!"
p := CompletionParams{Position: Position{Line: 0, Character: len(current)}, TextDocument: TextDocumentIdentifier{URI: "file://nextline.go"}}
items, ok, _ := s.tryLLMCompletion(p, "", current, below, "", "", false, "")
if !ok {
@@ -173,7 +173,7 @@ func TestBareDoubleOpenPreventsManualInvoke(t *testing.T) {
s.cfg = cfg
fake := &countingLLM{}
s.llmClient = fake
- line := ">>"
+ line := ">>!"
p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://bare-ds-manual.go"}}
// Simulate manual invoke
p.Context = json.RawMessage([]byte(`{"triggerKind":1}`))
diff --git a/internal/lsp/coverage_add_test.go b/internal/lsp/coverage_add_test.go
index b3b7322..2967fb5 100644
--- a/internal/lsp/coverage_add_test.go
+++ b/internal/lsp/coverage_add_test.go
@@ -56,7 +56,7 @@ func TestFindGoFunctionAtLine_NoBody(t *testing.T) {
}
func TestLineHasInlinePrompt(t *testing.T) {
- if !lineHasInlinePrompt(">do>", '>', '>') {
+ if !lineHasInlinePrompt(">!do>", ">!", '>', '>') {
t.Fatalf("expected inline prompt")
}
}
@@ -89,12 +89,12 @@ func TestIndentHelpersAndPromptRemoval(t *testing.T) {
t.Fatalf("applyIndent: %q", out)
}
// double-open trigger removes whole line
- edits := promptRemovalEditsForLine(">>ask>", 3, '>', '>')
+ edits := promptRemovalEditsForLine(">>!ask>", 3, ">!", '>', '>')
if len(edits) != 1 || edits[0].Range.Start.Line != 3 {
t.Fatalf("unexpected edits: %#v", edits)
}
// semicolon tags collect correctly when provided explicitly
- edits2 := collectSemicolonMarkers("pre;do;post", 1, ';', ';')
+ edits2 := collectSemicolonMarkers("pre;do;post", 1, ";", ';', ';')
if len(edits2) != 1 {
t.Fatalf("expected one semicolon edit, got %#v", edits2)
}
diff --git a/internal/lsp/document_test.go b/internal/lsp/document_test.go
index fd13e5d..95f0157 100644
--- a/internal/lsp/document_test.go
+++ b/internal/lsp/document_test.go
@@ -13,7 +13,7 @@ import (
func newTestServer() *Server {
cfg := appconfig.App{
- InlineOpen: ">",
+ InlineOpen: ">!",
InlineClose: ">",
ChatSuffix: ">",
ChatPrefixes: []string{"?", "!", ":", ";"},
@@ -47,7 +47,7 @@ func newTestServer() *Server {
func initServerDefaults(s *Server) {
cfg := s.cfg
if strings.TrimSpace(cfg.InlineOpen) == "" {
- cfg.InlineOpen = ">"
+ cfg.InlineOpen = ">!"
}
if strings.TrimSpace(cfg.InlineClose) == "" {
cfg.InlineClose = ">"
diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go
index 94b6348..7b61970 100644
--- a/internal/lsp/handlers.go
+++ b/internal/lsp/handlers.go
@@ -51,8 +51,8 @@ func (s *Server) findFirstInstructionInLine(line string) (instr string, cleaned
text string
}
cands := []cand{}
- _, _, openChar, closeChar := s.inlineMarkers()
- if t, l, r, ok := findStrictInlineTag(line, openChar, closeChar); ok {
+ openStr, _, openChar, closeChar := s.inlineMarkers()
+ if t, l, r, ok := findStrictInlineTag(line, openStr, openChar, closeChar); ok {
cands = append(cands, cand{start: l, end: r, text: t})
}
if i := strings.Index(line, "/*"); i >= 0 {
@@ -288,6 +288,7 @@ func (s *Server) compCacheTouchLocked(key string) {
// immediately to the left of the cursor.
func (s *Server) isTriggerEvent(p CompletionParams, current string) bool {
open, _, openChar, closeChar := s.inlineMarkers()
+ doubleSeqs := doubleOpenSequences(open, openChar, closeChar)
triggerChars := s.triggerCharacters()
// 1) Inspect LSP completion context if present
if p.Context != nil {
@@ -301,9 +302,9 @@ func (s *Server) isTriggerEvent(p CompletionParams, current string) bool {
b, _ := json.Marshal(p.Context)
_ = json.Unmarshal(b, &ctx)
}
- // If configured and the line contains a bare double-open marker (e.g., '>>' with no '>>text>'),
+ // If configured and the line contains a bare double-open marker (e.g., '>>!' with no '>>!text>'),
// do not treat as a trigger source.
- if open != "" && strings.Contains(current, open+open) && !hasDoubleOpenTrigger(current, openChar, closeChar) {
+ if containsAny(current, doubleSeqs) && !hasDoubleOpenTrigger(current, open, openChar, closeChar) {
return false
}
// TriggerKind 1 = Invoked (manual). Always allow manual invoke.
@@ -331,7 +332,7 @@ func (s *Server) isTriggerEvent(p CompletionParams, current string) bool {
return false
}
// Bare double-open should not trigger via fallback char either (only when configured)
- if open != "" && strings.Contains(current, open+open) && !hasDoubleOpenTrigger(current, openChar, closeChar) {
+ if containsAny(current, doubleSeqs) && !hasDoubleOpenTrigger(current, open, openChar, closeChar) {
return false
}
ch := string(current[idx-1])
@@ -366,6 +367,18 @@ func (s *Server) makeCompletionItems(cleaned string, inParams bool, current stri
}}
}
+func containsAny(haystack string, seqs []string) bool {
+ for _, seq := range seqs {
+ if seq == "" {
+ continue
+ }
+ if strings.Contains(haystack, seq) {
+ return true
+ }
+ }
+ return false
+}
+
// small helpers to keep tryLLMCompletion short
// LLM stats helpers moved to handlers_utils.go
diff --git a/internal/lsp/handlers_completion.go b/internal/lsp/handlers_completion.go
index db6866b..2fac1f3 100644
--- a/internal/lsp/handlers_completion.go
+++ b/internal/lsp/handlers_completion.go
@@ -199,8 +199,8 @@ func (s *Server) prepareCompletionPlan(p CompletionParams, above, current, below
hasExtra: hasExtra,
extraText: extraText,
}
- _, _, openChar, closeChar := s.inlineMarkers()
- plan.inlinePrompt = lineHasInlinePrompt(current, openChar, closeChar)
+ openStr, _, openChar, closeChar := s.inlineMarkers()
+ plan.inlinePrompt = lineHasInlinePrompt(current, openStr, openChar, closeChar)
if !plan.inlinePrompt && !s.isTriggerEvent(p, current) {
logging.Logf("lsp ", "%scompletion skip=no-trigger line=%d char=%d current=%q%s", logging.AnsiYellow, p.Position.Line, p.Position.Character, trimLen(current), logging.AnsiBase)
return plan, []CompletionItem{}, true
@@ -214,7 +214,7 @@ func (s *Server) prepareCompletionPlan(p CompletionParams, above, current, below
if pending := s.takePendingCompletion(plan.cacheKey); len(pending) > 0 {
return plan, pending, true
}
- if isBareDoubleOpen(current, openChar, closeChar) || isBareDoubleOpen(below, openChar, closeChar) {
+ if isBareDoubleOpen(current, openStr, openChar, closeChar) || isBareDoubleOpen(below, openStr, openChar, closeChar) {
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 plan, []CompletionItem{}, true
}
@@ -368,7 +368,7 @@ func (s *Server) tryProviderNativeCompletion(ctx context.Context, plan completio
before, after := s.docBeforeAfter(p.TextDocument.URI, p.Position)
path := strings.TrimPrefix(p.TextDocument.URI, "file://")
cfg := s.currentConfig()
- _, _, openChar, closeChar := s.inlineMarkers()
+ openStr, _, openChar, closeChar := s.inlineMarkers()
prompt := renderTemplate(cfg.PromptNativeCompletion, map[string]string{
"path": path,
"before": before,
@@ -409,7 +409,7 @@ func (s *Server) tryProviderNativeCompletion(ctx context.Context, plan completio
if cleaned == "" {
return nil, false
}
- if strings.TrimSpace(cleaned) != "" && hasDoubleOpenTrigger(current, openChar, closeChar) {
+ if strings.TrimSpace(cleaned) != "" && hasDoubleOpenTrigger(current, openStr, openChar, closeChar) {
indent := leadingIndent(current)
if indent != "" {
cleaned = applyIndent(indent, cleaned)
@@ -537,8 +537,8 @@ func (s *Server) postProcessCompletion(text string, leftOfCursor string, current
if cleaned != "" {
cleaned = stripDuplicateGeneralPrefix(leftOfCursor, cleaned)
}
- _, _, openChar, closeChar := s.inlineMarkers()
- if cleaned != "" && hasDoubleOpenTrigger(currentLine, openChar, closeChar) {
+ openStr, _, openChar, closeChar := s.inlineMarkers()
+ if cleaned != "" && hasDoubleOpenTrigger(currentLine, openStr, openChar, closeChar) {
if indent := leadingIndent(currentLine); indent != "" {
cleaned = applyIndent(indent, cleaned)
}
diff --git a/internal/lsp/handlers_document.go b/internal/lsp/handlers_document.go
index da7db51..a047324 100644
--- a/internal/lsp/handlers_document.go
+++ b/internal/lsp/handlers_document.go
@@ -91,9 +91,9 @@ func (s *Server) detectAndHandleChat(uri string) {
return
}
suffix, prefixes, _ := s.chatConfig()
- _, _, openChar, closeChar := s.inlineMarkers()
+ openStr, _, openChar, closeChar := s.inlineMarkers()
for i, raw := range d.lines {
- if lineHasInlinePrompt(raw, openChar, closeChar) {
+ if lineHasInlinePrompt(raw, openStr, openChar, closeChar) {
if s.currentLLMClient() != nil {
pos := Position{Line: i, Character: len(raw)}
go s.runInlinePrompt(uri, pos)
@@ -221,8 +221,8 @@ func (s *Server) runInlinePrompt(uri string, pos Position) {
return
}
line := d.lines[pos.Line]
- _, _, openChar, closeChar := s.inlineMarkers()
- if !lineHasInlinePrompt(line, openChar, closeChar) {
+ openStr, _, openChar, closeChar := s.inlineMarkers()
+ if !lineHasInlinePrompt(line, openStr, openChar, closeChar) {
return
}
p := CompletionParams{TextDocument: TextDocumentIdentifier{URI: uri}, Position: Position{Line: pos.Line, Character: len(line)}}
diff --git a/internal/lsp/handlers_helpers_test.go b/internal/lsp/handlers_helpers_test.go
index 2bd677e..8a0231a 100644
--- a/internal/lsp/handlers_helpers_test.go
+++ b/internal/lsp/handlers_helpers_test.go
@@ -10,14 +10,14 @@ func TestHasDoubleSemicolonTrigger(t *testing.T) {
line string
want bool
}{
- {">>todo> remove this", true},
- {"prefix >>x> suffix", true},
- {">> spaced >", false},
+ {">>!todo> remove this", true},
+ {"prefix >>!x> suffix", true},
+ {">>! spaced >", false},
{"no markers", false},
- {">>x > space before close", false},
+ {">>!x > space before close", false},
}
for _, tc := range cases {
- got := hasDoubleOpenTrigger(tc.line, '>', '>')
+ got := hasDoubleOpenTrigger(tc.line, ">!", '>', '>')
if got != tc.want {
t.Fatalf("hasDoubleOpenTrigger(%q)=%v want %v", tc.line, got, tc.want)
}
@@ -25,13 +25,13 @@ func TestHasDoubleSemicolonTrigger(t *testing.T) {
}
func TestCollectSemicolonMarkers(t *testing.T) {
- line := "keep >ok> this and >another> that"
- edits := collectSemicolonMarkers(line, 7, '>', '>')
+ line := "keep >!ok> this and >!another> that"
+ edits := collectSemicolonMarkers(line, 7, ">!", '>', '>')
if len(edits) != 2 {
t.Fatalf("expected 2 edits, got %d", len(edits))
}
// Validate the first edit aligns with ;ok;
- start := strings.Index(line, ">ok>")
+ start := strings.Index(line, ">!ok>")
if start < 0 {
t.Fatalf("test setup: missing ;ok;")
}
@@ -41,8 +41,8 @@ func TestCollectSemicolonMarkers(t *testing.T) {
}
func TestPromptRemovalEditsForLine_WholeLine(t *testing.T) {
- line := ">>todo> remove this whole line"
- edits := promptRemovalEditsForLine(line, 3, '>', '>')
+ line := ">>!todo> remove this whole line"
+ edits := promptRemovalEditsForLine(line, 3, ">!", '>', '>')
if len(edits) != 1 {
t.Fatalf("expected 1 whole-line edit, got %d", len(edits))
}
diff --git a/internal/lsp/handlers_test.go b/internal/lsp/handlers_test.go
index 6803d1e..b2b47c0 100644
--- a/internal/lsp/handlers_test.go
+++ b/internal/lsp/handlers_test.go
@@ -16,7 +16,7 @@ func TestFindFirstInstructionInLine_NoMarker(t *testing.T) {
}
func TestFindFirstInstructionInLine_StrictInline_Basic(t *testing.T) {
- line := "prefix >rename var> suffix"
+ line := "prefix >!rename var> suffix"
s := newTestServer()
instr, cleaned, ok := s.findFirstInstructionInLine(line)
if !ok {
@@ -32,7 +32,7 @@ func TestFindFirstInstructionInLine_StrictInline_Basic(t *testing.T) {
}
func TestFindFirstInstructionInLine_StrictInline_TrailingSpacesTrimmed(t *testing.T) {
- line := "code>fix> \t\t"
+ line := "code>!fix> \t\t"
s := newTestServer()
instr, cleaned, ok := s.findFirstInstructionInLine(line)
if !ok {
@@ -48,9 +48,9 @@ func TestFindFirstInstructionInLine_StrictInline_TrailingSpacesTrimmed(t *testin
func TestFindFirstInstructionInLine_Inline_InvalidPatterns(t *testing.T) {
cases := []string{
- "prefix > bad> suffix", // space after first '>' ⇒ invalid
- "prefix >bad > suffix", // space before closing '>' ⇒ invalid
- "prefix > > suffix", // empty inner ⇒ invalid
+ "prefix >! bad> suffix", // space after '!'
+ "prefix >!bad > suffix", // space before closing '>' ⇒ invalid
+ "prefix >! > suffix", // empty inner ⇒ invalid
}
for _, line := range cases {
s := newTestServer()
@@ -136,14 +136,14 @@ func TestFindFirstInstructionInLine_DoubleDash(t *testing.T) {
}
func TestFindFirstInstructionInLine_EarliestWins_CommentOverInline(t *testing.T) {
- line := "aa // comment >not this> trailing"
+ line := "aa // comment >!not this> trailing"
s := newTestServer()
instr, cleaned, ok := s.findFirstInstructionInLine(line)
if !ok {
t.Fatalf("expected ok=true")
}
- if instr != "comment >not this> trailing" {
- t.Fatalf("instr got %q want %q", instr, "comment >not this> trailing")
+ if instr != "comment >!not this> trailing" {
+ t.Fatalf("instr got %q want %q", instr, "comment >!not this> trailing")
}
if cleaned != "aa" {
t.Fatalf("cleaned got %q want %q", cleaned, "aa")
@@ -151,7 +151,7 @@ func TestFindFirstInstructionInLine_EarliestWins_CommentOverInline(t *testing.T)
}
func TestFindFirstInstructionInLine_EarliestWins_InlineOverComment(t *testing.T) {
- line := "aa >short> // comment"
+ line := "aa >!short> // comment"
s := newTestServer()
instr, cleaned, ok := s.findFirstInstructionInLine(line)
if !ok {
@@ -168,19 +168,19 @@ func TestFindFirstInstructionInLine_EarliestWins_InlineOverComment(t *testing.T)
func TestFindStrictInlineTag_Various(t *testing.T) {
// basic
- if text, l, r, ok := findStrictInlineTag("pre>do it>post", '>', '>'); !ok || text != "do it" || l != 3 || r != 10 {
+ if text, l, r, ok := findStrictInlineTag("pre>!do it>post", ">!", '>', '>'); !ok || text != "do it" || l != 3 || r != 11 {
t.Fatalf("unexpected: ok=%v text=%q l=%d r=%d", ok, text, l, r)
}
// at start
- if text, l, r, ok := findStrictInlineTag(">x>", '>', '>'); !ok || text != "x" || l != 0 || r != 3 {
+ if text, l, r, ok := findStrictInlineTag(">!x>", ">!", '>', '>'); !ok || text != "x" || l != 0 || r != 4 {
t.Fatalf("unexpected at start: ok=%v text=%q l=%d r=%d", ok, text, l, r)
}
- // double opening '>>' should still allow a tag starting at the second '>'
- if text, _, _, ok := findStrictInlineTag("prefix >>bad> suffix", '>', '>'); !ok || text != "bad" {
+ // double opening '>>!' should still allow a tag starting after the double marker when configured for '>!'