Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 65 additions & 7 deletions lua/inline/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -649,13 +649,14 @@ end

---Parse AI response in REPLACE format.
---Expected format: "REPLACE start_line end_line\ncode..."
---Handles common model quirks: markdown fences, preamble text, extra whitespace.
---@param response string Raw response text
---@return number|nil start_line Start line for replacement
---@return number|nil end_line End line for replacement
---@return string[]|nil code_lines Lines to insert
---@return string|nil err Error message if parsing failed
local function parse_response(response)
-- strip markdown fences if present
-- strip markdown fences if present (handles ```text, ```lua, etc.)
response = strip_code_fences(response)

-- split into lines
Expand All @@ -673,15 +674,58 @@ local function parse_response(response)
return nil, nil, nil, "empty response"
end

-- parse REPLACE header from first line
local start_line, end_line = lines[1]:match("^REPLACE%s+(%d+)%s+(%d+)$")
if not start_line then
return nil, nil, nil, "missing REPLACE header: " .. lines[1]
-- find the REPLACE header line (may not be first if model added preamble)
-- patterns to try, in order of strictness:
-- 1. exact format: "REPLACE 1 4"
-- 2. with extra whitespace: " REPLACE 1 4 "
-- 3. case-insensitive: "Replace 1 4"
local replace_line_idx = nil
local start_line, end_line

for i, line in ipairs(lines) do
-- try strict match first (trimmed)
local trimmed = line:match("^%s*(.-)%s*$")
start_line, end_line = trimmed:match("^REPLACE%s+(%d+)%s+(%d+)$")
if start_line then
replace_line_idx = i
break
end

-- try case-insensitive match
start_line, end_line = trimmed:match("^[Rr][Ee][Pp][Ll][Aa][Cc][Ee]%s+(%d+)%s+(%d+)$")
if start_line then
replace_line_idx = i
break
end
end

-- extract code lines (everything after header)
if not replace_line_idx then
-- provide helpful error with first non-empty line
local first_content = ""
for _, line in ipairs(lines) do
local trimmed = line:match("^%s*(.-)%s*$")
if trimmed ~= "" then
first_content = trimmed:sub(1, 50)
break
end
end
return nil, nil, nil, "missing REPLACE header, got: " .. first_content
end

-- warn if there was preamble (but still proceed)
if replace_line_idx > 1 then
local preamble_lines = replace_line_idx - 1
vim.schedule(function()
vim.notify(
string.format("ignored %d preamble line(s) before REPLACE header", preamble_lines),
vim.log.levels.DEBUG
)
end)
end

-- extract code lines (everything after REPLACE header)
local code_lines = {}
for i = 2, #lines do
for i = replace_line_idx + 1, #lines do
table.insert(code_lines, lines[i])
end

Expand Down Expand Up @@ -1177,4 +1221,18 @@ function M.validate_config()
end)
end

--------------------------------------------------------------------------------
-- Test Interface (exposed for unit testing only)
--------------------------------------------------------------------------------

--- Internal functions exposed for testing.
--- Not part of the public API - may change without notice.
---@class InlineTestInterface
---@field parse_response fun(response: string): number|nil, number|nil, string[]|nil, string|nil
---@field strip_code_fences fun(text: string): string
M._test = {
parse_response = parse_response,
strip_code_fences = strip_code_fences,
}

return M
64 changes: 55 additions & 9 deletions prompts/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,52 @@

- %s

# Output (model response, strict)
# Output Format (STRICT - machine parsed)

CRITICAL: Your response will be parsed programmatically. Any deviation breaks the editor.
Your response is parsed by a program, not read by a human. Follow this format EXACTLY:

- First line MUST be exactly: `REPLACE <start_line> <end_line>` (nothing before it)
- Second line onward: replacement code only
- No explanations, no commentary, no markdown fences, no preamble
- Do not explain what you're doing—just output the REPLACE header and code
- The `REPLACE` header is a control line for the editor; it is not part of the file.
- When adding new lines (e.g., doc comments), `end_line` equals `start_line` (replace the @ai line, output multiple lines).
```
REPLACE <start_line> <end_line>
<replacement code lines>
```

## Format Rules

1. Line 1: `REPLACE` followed by two integers (start and end line numbers)
2. Line 2+: The replacement code (will be inserted verbatim into the file)
3. Nothing else. No markdown fences. No explanations. No "Here's the code:" preamble.

## What NOT to do

WRONG (has preamble):
```
Here's the fixed code:
REPLACE 1 4
...
```

WRONG (has markdown fence):
```
```go
REPLACE 1 4
...
```
```

WRONG (missing header):
```
func Add(a, b int) int {
return a + b
}
```

CORRECT:
```
REPLACE 1 4
func Add(a, b int) int {
return a + b
}
```

# Examples (Input - Output)

Expand Down Expand Up @@ -220,4 +256,14 @@ func helloWorld() string {
- For annotations, add clear inline comments explaining what each section does.
- If you need more context (types, interfaces, related files), use your tools to read them.

IMPORTANT: Output ONLY the REPLACE header and code. No explanations. No preamble. The first characters of your response must be "REPLACE".
# Final Check Before Responding

Before you output your response, verify:
1. Does your response start with `REPLACE` (no spaces, no other text before it)?
2. Is the REPLACE line followed by exactly two space-separated integers?
3. Is there NO markdown fence (```) wrapping your response?
4. Is there NO explanatory text before or after the code?

If any check fails, fix it. The parser will reject malformed responses.

OUTPUT FORMAT: `REPLACE <start> <end>` on line 1, then code. Nothing else.
Loading