summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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 '>!'
+ if text, _, _, ok := findStrictInlineTag("prefix >>!bad> suffix", ">!", '>', '>'); !ok || text != "bad" {
t.Fatalf("unexpected double-open handling: ok=%v text=%q", ok, text)
}
// inner spaces directly after first '>' or before last '>' invalidate the tag
- if _, _, _, ok := findStrictInlineTag("a> inner >b", '>', '>'); ok {
+ if _, _, _, ok := findStrictInlineTag("a>! inner >b", ">!", '>', '>'); ok {
t.Fatalf("expected invalid strict tag due to spaces at boundaries")
}
}
diff --git a/internal/lsp/handlers_utils.go b/internal/lsp/handlers_utils.go
index 2748a60..b3056b9 100644
--- a/internal/lsp/handlers_utils.go
+++ b/internal/lsp/handlers_utils.go
@@ -312,11 +312,36 @@ func (s *Server) chatWithStats(ctx context.Context, surface surfaceKind, spec re
// Inline prompt utilities
-func lineHasInlinePrompt(line string, open, close byte) bool {
- if _, _, _, ok := findStrictInlineTag(line, open, close); ok {
+func lineHasInlinePrompt(line string, openStr string, open, close byte) bool {
+ if openStr == "" {
+ openStr = string(open)
+ }
+ if _, _, _, ok := findStrictInlineTag(line, openStr, open, close); ok {
return true
}
- return hasDoubleOpenTrigger(line, open, close)
+ return hasDoubleOpenTrigger(line, openStr, open, close)
+}
+
+func doubleOpenSequences(openStr string, open, close byte) []string {
+ seen := make(map[string]struct{}, 2)
+ var seqs []string
+ if openStr != "" && close != 0 {
+ seq := openStr + string(close)
+ if _, ok := seen[seq]; !ok {
+ seen[seq] = struct{}{}
+ seqs = append(seqs, seq)
+ }
+ }
+ if openStr != "" && open != 0 {
+ seq := string(open) + openStr
+ if len(seq) > len(openStr) {
+ if _, ok := seen[seq]; !ok {
+ seen[seq] = struct{}{}
+ seqs = append(seqs, seq)
+ }
+ }
+ }
+ return seqs
}
func leadingIndent(line string) string {
@@ -353,34 +378,66 @@ func applyIndent(indent, suggestion string) string {
// --- Inline marker parsing and general string utilities ---
-// findStrictInlineTag finds >text> (configurable), with no space after the first
+// findStrictInlineTag finds >!text> (configurable), with no space after the first
// opening marker and no space immediately before the closing marker. Returns the
// text between markers, the start index, the end index just after closing, and ok.
-func findStrictInlineTag(line string, open, close byte) (string, int, int, bool) {
+func findStrictInlineTag(line string, openStr string, open, close byte) (string, int, int, bool) {
+ if openStr == "" {
+ openStr = string(open)
+ }
+ if openStr == "" {
+ return "", 0, 0, false
+ }
+ openChar := open
+ if openChar == 0 {
+ openChar = openStr[0]
+ }
+ doubleSeqs := doubleOpenSequences(openStr, openChar, close)
pos := 0
for pos < len(line) {
- // find opening marker
- j := strings.IndexByte(line[pos:], open)
+ j := strings.IndexByte(line[pos:], openChar)
if j < 0 {
return "", 0, 0, false
}
j += pos
- // ensure single open (not double) and non-space after
- if j+1 >= len(line) || line[j+1] == open || line[j+1] == ' ' {
+ if !strings.HasPrefix(line[j:], openStr) {
pos = j + 1
continue
}
- // find closing marker
- k := strings.IndexByte(line[j+1:], close)
+ contentStart := j + len(openStr)
+ if contentStart >= len(line) {
+ return "", 0, 0, false
+ }
+ doubleHit := false
+ for _, seq := range doubleSeqs {
+ if strings.HasPrefix(line[j:], seq) {
+ doubleHit = true
+ contentStart += len(seq) - len(openStr)
+ if contentStart >= len(line) {
+ return "", 0, 0, false
+ }
+ break
+ }
+ }
+ next := line[contentStart]
+ if next == ' ' {
+ pos = contentStart + 1
+ continue
+ }
+ if !doubleHit && next == close {
+ pos = contentStart + 1
+ continue
+ }
+ k := strings.IndexByte(line[contentStart:], close)
if k < 0 {
return "", 0, 0, false
}
- closeIdx := j + 1 + k
- if closeIdx-1 < 0 || line[closeIdx-1] == ' ' {
+ closeIdx := contentStart + k
+ if closeIdx-1 >= contentStart && line[closeIdx-1] == ' ' {
pos = closeIdx + 1
continue
}
- inner := strings.TrimSpace(line[j+1 : closeIdx])
+ inner := strings.TrimSpace(line[contentStart:closeIdx])
if inner == "" {
pos = closeIdx + 1
continue
@@ -394,20 +451,20 @@ func findStrictInlineTag(line string, open, close byte) (string, int, int, bool)
// 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 isBareDoubleOpen(line string, open, close byte) bool {
+func isBareDoubleOpen(line string, openStr string, open, close byte) bool {
t := strings.TrimSpace(line)
- // check for double-open pattern
- dbl := string([]byte{open, open})
- if !strings.Contains(t, dbl) {
- return false
+ if openStr == "" {
+ openStr = string(open)
}
- if hasDoubleOpenTrigger(t, open, close) {
+ if openStr == "" {
return false
}
- if strings.HasPrefix(t, dbl) {
- rest := strings.TrimSpace(t[len(dbl):])
- if rest == "" || rest == ";" {
- return true
+ for _, seq := range doubleOpenSequences(openStr, open, close) {
+ if strings.HasPrefix(t, seq) {
+ rest := strings.TrimSpace(t[len(seq):])
+ if rest == "" || rest == string(close) {
+ return true
+ }
}
}
return false
@@ -562,40 +619,62 @@ func (s *Server) collectPromptRemovalEdits(uri string) []TextEdit {
return nil
}
var edits []TextEdit
- _, _, openChar, closeChar := s.inlineMarkers()
+ openStr, _, openChar, closeChar := s.inlineMarkers()
for i, line := range d.lines {
- edits = append(edits, promptRemovalEditsForLine(line, i, openChar, closeChar)...)
+ edits = append(edits, promptRemovalEditsForLine(line, i, openStr, openChar, closeChar)...)
}
return edits
}
-func promptRemovalEditsForLine(line string, lineNum int, open, close byte) []TextEdit {
- if hasDoubleOpenTrigger(line, open, close) {
+func promptRemovalEditsForLine(line string, lineNum int, openStr string, open, close byte) []TextEdit {
+ if hasDoubleOpenTrigger(line, openStr, open, close) {
return []TextEdit{{Range: Range{Start: Position{Line: lineNum, Character: 0}, End: Position{Line: lineNum, Character: len(line)}}, NewText: ""}}
}
- return collectSemicolonMarkers(line, lineNum, open, close)
+ return collectSemicolonMarkers(line, lineNum, openStr, open, close)
}
-func hasDoubleOpenTrigger(line string, open, close byte) bool {
+func hasDoubleOpenTrigger(line string, openStr string, open, close byte) bool {
+ if openStr == "" {
+ openStr = string(open)
+ }
+ if openStr == "" {
+ return false
+ }
+ seqs := doubleOpenSequences(openStr, open, close)
+ if len(seqs) == 0 {
+ return false
+ }
pos := 0
for pos < len(line) {
- // look for double-open sequence
- dbl := string([]byte{open, open})
- j := strings.Index(line[pos:], dbl)
- if j < 0 {
+ found := -1
+ var seq string
+ for _, cand := range seqs {
+ if cand == "" {
+ continue
+ }
+ if idx := strings.Index(line[pos:], cand); idx >= 0 {
+ abs := pos + idx
+ if found < 0 || abs < found {
+ found = abs
+ seq = cand
+ }
+ }
+ }
+ if found < 0 {
return false
}
- j += pos
- contentStart := j + len(dbl)
+ contentStart := found + len(seq)
if contentStart >= len(line) {
return false
}
first := line[contentStart]
- if first == ' ' || first == open {
+ if first == ' ' || first == close || first == open {
pos = contentStart + 1
continue
}
- // find closing
+ if contentStart+1 >= len(line) {
+ return false
+ }
k := strings.IndexByte(line[contentStart+1:], close)
if k < 0 {
return false
@@ -610,34 +689,53 @@ func hasDoubleOpenTrigger(line string, open, close byte) bool {
return false
}
-func collectSemicolonMarkers(line string, lineNum int, open, close byte) []TextEdit {
+func collectSemicolonMarkers(line string, lineNum int, openStr string, open, close byte) []TextEdit {
+ if openStr == "" {
+ openStr = string(open)
+ }
+ if openStr == "" {
+ return nil
+ }
var edits []TextEdit
- startSemi := 0
- for startSemi < len(line) {
- j := strings.IndexByte(line[startSemi:], open)
+ start := 0
+ doubleSeqs := doubleOpenSequences(openStr, open, close)
+ for start < len(line) {
+ j := strings.Index(line[start:], openStr)
if j < 0 {
break
}
- j += startSemi
- k := strings.IndexByte(line[j+1:], close)
- if k < 0 {
+ j += start
+ contentStart := j + len(openStr)
+ if contentStart >= len(line) {
break
}
- if j+1 >= len(line) || line[j+1] == ' ' {
- startSemi = j + 1
+ next := line[contentStart]
+ if next == ' ' {
+ start = j + 1
continue
}
- if line[j+1] == open { // skip double-open start
- startSemi = j + 2
+ skipDouble := false
+ for _, seq := range doubleSeqs {
+ if strings.HasPrefix(line[j:], seq) {
+ skipDouble = true
+ break
+ }
+ }
+ if skipDouble {
+ start = j + 1
continue
}
- closeIdx := j + 1 + k
- if closeIdx-1 < 0 || line[closeIdx-1] == ' ' {
- startSemi = closeIdx + 1
+ k := strings.IndexByte(line[contentStart:], close)
+ if k < 0 {
+ break
+ }
+ closeIdx := contentStart + k
+ if closeIdx-1 < contentStart || line[closeIdx-1] == ' ' {
+ start = closeIdx + 1
continue
}
- if closeIdx-(j+1) < 1 {
- startSemi = closeIdx + 1
+ if closeIdx == contentStart {
+ start = closeIdx + 1
continue
}
endChar := closeIdx + 1
@@ -645,7 +743,7 @@ func collectSemicolonMarkers(line string, lineNum int, open, close byte) []TextE
endChar++
}
edits = append(edits, TextEdit{Range: Range{Start: Position{Line: lineNum, Character: j}, End: Position{Line: lineNum, Character: endChar}}, NewText: ""})
- startSemi = endChar
+ start = endChar
}
return edits
}
diff --git a/internal/lsp/helpers_inline_prompt_test.go b/internal/lsp/helpers_inline_prompt_test.go
index 5554d89..5dc698a 100644
--- a/internal/lsp/helpers_inline_prompt_test.go
+++ b/internal/lsp/helpers_inline_prompt_test.go
@@ -7,12 +7,12 @@ import (
func TestLineHasInlinePrompt_BasicAndDoubleOpen(t *testing.T) {
// Basic inline
- if !lineHasInlinePrompt("do >task> now", '>', '>') {
- t.Fatalf("expected inline prompt detection for >text>")
+ if !lineHasInlinePrompt("do >!task> now", ">!", '>', '>') {
+ t.Fatalf("expected inline prompt detection for >!text>")
}
// Double-open variant should be recognized as inline prompt too
- if !lineHasInlinePrompt(">>replace>", '>', '>') {
- t.Fatalf("expected inline prompt detection for >>text>")
+ if !lineHasInlinePrompt(">>!replace>", ">!", '>', '>') {
+ t.Fatalf("expected inline prompt detection for >>!text>")
}
}
diff --git a/internal/lsp/helpers_more_test.go b/internal/lsp/helpers_more_test.go
index 287aa9d..683a69c 100644
--- a/internal/lsp/helpers_more_test.go
+++ b/internal/lsp/helpers_more_test.go
@@ -25,10 +25,10 @@ func TestLeadingAndApplyIndent(t *testing.T) {
}
func TestFindStrictInlineTag(t *testing.T) {
- if _, _, _, ok := findStrictInlineTag(">do this> next", '>', '>'); !ok {
+ if _, _, _, ok := findStrictInlineTag(">!do this> next", ">!", '>', '>'); !ok {
t.Fatalf("expected strict tag")
}
- if _, _, _, ok := findStrictInlineTag("> spaced >", '>', '>'); ok {
+ if _, _, _, ok := findStrictInlineTag(">! spaced >", ">!", '>', '>'); ok {
t.Fatalf("should ignore spaced tag")
}
}
@@ -81,11 +81,11 @@ func TestRangesOverlapAndOrder(t *testing.T) {
}
func TestPromptRemovalEditsForLine(t *testing.T) {
- edits := promptRemovalEditsForLine(">>do thing>", 3, '>', '>')
+ edits := promptRemovalEditsForLine(">>!do thing>", 3, ">!", '>', '>')
if len(edits) != 1 || edits[0].Range.Start.Line != 3 {
t.Fatalf("expected full-line removal for double-semicolon")
}
- edits2 := promptRemovalEditsForLine(">act> and >b>", 1, '>', '>')
+ edits2 := promptRemovalEditsForLine(">!act> and >!b>", 1, ">!", '>', '>')
if len(edits2) == 0 {
t.Fatalf("expected edits to remove strict markers")
}
@@ -94,7 +94,7 @@ func TestPromptRemovalEditsForLine(t *testing.T) {
func TestCollectPromptRemovalEdits_MultiLine(t *testing.T) {
s := newTestServer()
uri := "file:///t.go"
- s.setDocument(uri, "a\n>do> x\n>>wipe>\nend")
+ s.setDocument(uri, "a\n>!do> x\n>>!wipe>\nend")
edits := s.collectPromptRemovalEdits(uri)
if len(edits) < 2 {
t.Fatalf("expected >=2 edits, got %d", len(edits))
@@ -143,10 +143,10 @@ func TestComputeTextEditAndFilter(t *testing.T) {
}
func TestIsBareDoubleOpen(t *testing.T) {
- if !isBareDoubleOpen(">> ", '>', '>') {
+ if !isBareDoubleOpen(">>! ", ">!", '>', '>') {
t.Fatalf("expected true")
}
- if isBareDoubleOpen(">>x>", '>', '>') {
+ if isBareDoubleOpen(">>!x>", ">!", '>', '>') {
t.Fatalf("expected false for content form")
}
}
diff --git a/internal/lsp/init_and_trigger_test.go b/internal/lsp/init_and_trigger_test.go
index 2c5cd62..832d451 100644
--- a/internal/lsp/init_and_trigger_test.go
+++ b/internal/lsp/init_and_trigger_test.go
@@ -72,8 +72,8 @@ func TestIsTriggerEvent_Variants(t *testing.T) {
t.Fatalf("fallback char should trigger")
}
// 4) Bare double-open disables trigger
- p4 := CompletionParams{Position: Position{Line: 0, Character: 2}}
- if s.isTriggerEvent(p4, ">>") {
+ p4 := CompletionParams{Position: Position{Line: 0, Character: 3}}
+ if s.isTriggerEvent(p4, ">>!") {
t.Fatalf("bare double-open should not trigger")
}
}
diff --git a/internal/lsp/inline_prompt_completion_test.go b/internal/lsp/inline_prompt_completion_test.go
index 0b71d13..9599a70 100644
--- a/internal/lsp/inline_prompt_completion_test.go
+++ b/internal/lsp/inline_prompt_completion_test.go
@@ -27,7 +27,7 @@ func TestHandleCompletionInlinePromptDoubleArrow(t *testing.T) {
initServerDefaults(s)
s.llmClient = fakeLLMInline{}
uri := "file:///inline.go"
- line := "hello world >>translate this into bulgarian>"
+ 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 {
diff --git a/internal/lsp/instruction_table_test.go b/internal/lsp/instruction_table_test.go
index a6042b1..542bc68 100644
--- a/internal/lsp/instruction_table_test.go
+++ b/internal/lsp/instruction_table_test.go
@@ -8,7 +8,7 @@ func TestFindFirstInstructionInLine_Table(t *testing.T) {
line string
instr string
}{
- {"strict_inline_marker", ">do> trailing", "do"},
+ {"strict_inline_marker", ">!do> trailing", "do"},
{"c_block", "x /* add docs */ y", "add docs"},
{"html_comment", "<!-- fix --> code", "fix"},
{"slash_slash", "code // please refactor", "please refactor"},
diff --git a/internal/lsp/postprocess_indent_test.go b/internal/lsp/postprocess_indent_test.go
index 28f73a5..f00fb74 100644
--- a/internal/lsp/postprocess_indent_test.go
+++ b/internal/lsp/postprocess_indent_test.go
@@ -4,7 +4,7 @@ import "testing"
func TestPostProcessCompletion_IndentWithDoubleOpen(t *testing.T) {
s := newTestServer()
- cleaned := s.postProcessCompletion("a\nb", "", " >>gen>")
+ cleaned := s.postProcessCompletion("a\nb", "", " >>!gen>")
// Expect each non-empty line to be indented by two spaces
want := " a\n b"
if cleaned != want {
diff --git a/internal/lsp/provider_native_success_test.go b/internal/lsp/provider_native_success_test.go
index e5ab81e..5bfe434 100644
--- a/internal/lsp/provider_native_success_test.go
+++ b/internal/lsp/provider_native_success_test.go
@@ -50,7 +50,7 @@ func TestProviderNativeCompletion_IndentWithDoubleOpen(t *testing.T) {
s := newTestServer()
s.llmClient = fakeCompleterIndent{}
spec := s.buildRequestSpec(surfaceCompletion)
- current := " >>do>" // leading indent + double-open marker
+ current := " >>!do>" // leading indent + double-open marker
p := CompletionParams{TextDocument: TextDocumentIdentifier{URI: "file:///x.go"}, Position: Position{Line: 0, Character: len(current)}}
plan := completionPlan{current: current, params: p, funcCtx: "func f(){}", docStr: "doc", cacheKey: "k"}
items, ok := s.tryProviderNativeCompletion(context.Background(), plan, spec, s.llmClient, "0000")
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index d55a967..e3a21f3 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -448,7 +448,7 @@ func (s *Server) inlineMarkers() (open string, close string, openChar byte, clos
cfg := s.currentConfig()
open = strings.TrimSpace(cfg.InlineOpen)
if open == "" {
- open = ">"
+ open = ">!"
}
close = strings.TrimSpace(cfg.InlineClose)
if close == "" {
diff --git a/internal/lsp/triggers_config_test.go b/internal/lsp/triggers_config_test.go
index 96ac4ba..9ab2752 100644
--- a/internal/lsp/triggers_config_test.go
+++ b/internal/lsp/triggers_config_test.go
@@ -31,7 +31,7 @@ func TestNewServer_AssignsTriggerGlobals_AndParsingUsesThem(t *testing.T) {
s := NewServer(bytes.NewReader(nil), &out, log.New(io.Discard, "", 0), ServerOptions{
InlineOpen: "<", InlineClose: ">", ChatSuffix: ")", ChatPrefixes: []string{":"},
})
- _, _, openChar, closeChar := s.inlineMarkers()
+ openStr, _, openChar, closeChar := s.inlineMarkers()
if openChar != '<' || closeChar != '>' {
t.Fatalf("inline markers not applied: %q %q", string(openChar), string(closeChar))
}
@@ -39,7 +39,7 @@ func TestNewServer_AssignsTriggerGlobals_AndParsingUsesThem(t *testing.T) {
if suffixChar != ')' || len(prefixes) == 0 || prefixes[0] != ":" {
t.Fatalf("chat markers not applied: suffix=%q prefixes=%v", string(suffixChar), prefixes)
}
- if txt, l, r, ok := findStrictInlineTag("x<do>y", openChar, closeChar); !ok || txt != "do" || l != 1 || r != 5 {
+ if txt, l, r, ok := findStrictInlineTag("x<do>y", openStr, openChar, closeChar); !ok || txt != "do" || l != 1 || r != 5 {
t.Fatalf("findStrictInlineTag failed: ok=%v txt=%q l=%d r=%d", ok, txt, l, r)
}
if got := s.stripTrailingTrigger("note:)"); got != "note:" {
diff --git a/internal/runtimeconfig/store_test.go b/internal/runtimeconfig/store_test.go
index 1c05cc9..0a0183a 100644
--- a/internal/runtimeconfig/store_test.go
+++ b/internal/runtimeconfig/store_test.go
@@ -26,6 +26,7 @@ func TestStoreReloadSkipsEnvOverrides(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", tmp)
t.Setenv("HEXAI_MAX_TOKENS", "321")
+ t.Setenv("HEXAI_PROVIDER", "")
initial := appconfig.Load(logger)
if initial.MaxTokens != 321 {
@@ -75,6 +76,7 @@ func TestStoreReloadLogsSummary(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", tmp)
t.Setenv("HEXAI_MAX_TOKENS", "321")
+ t.Setenv("HEXAI_PROVIDER", "")
initial := appconfig.Load(logger)
store := New(initial)
diff --git a/internal/version.go b/internal/version.go
index a28ebba..021cc9d 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.15.0"
+const Version = "0.15.1"