helix-editor/helix · error · anyhow::Error

{err}

Error message

{err}

What it means

expand_inner recursively expands percent tokens inside a %( ... ) / expand-style token. When the inner tokenizer's parse_percent_token fails, the ParseArgsError is wrapped with anyhow!("{err}") — the most common inner error being "'%' was not properly escaped. Please use '%%'". So this error is a re-surface of a tokenizer failure for malformed percent syntax inside the expansion.

Source

Thrown at helix-view/src/expansion.rs:224

    let mut start = 0;

    while let Some(offset) = content[start..].find('%') {
        let idx = start + offset;
        if content.as_bytes().get(idx + '%'.len_utf8()).copied() == Some(b'%') {
            // Treat two percents in a row as an escaped percent.
            escaped.push_str(&content[start..=idx]);
            // Skip over both percents.
            start = idx + ('%'.len_utf8() * 2);
        } else {
            // Otherwise interpret the percent as an expansion. Push up to (but not
            // including) the percent token.
            escaped.push_str(&content[start..idx]);
            // Then parse the expansion,
            let mut tokenizer = Tokenizer::new(&content[idx..], true);
            let token = tokenizer
                .parse_percent_token()
                .unwrap()
                .map_err(|err| anyhow!("{err}"))?;
            // expand it (this is the recursive part),
            let expanded = expand(editor, token)?;
            escaped.push_str(expanded.as_ref());
            // and move forward to the end of the expansion.
            start = idx + tokenizer.pos();
        }
    }

    if escaped.is_empty() {
        Ok(content)
    } else {
        escaped.push_str(&content[start..]);
        Ok(Cow::Owned(escaped))
    }
}

// Note: the lifetime of the expanded variable (the `Cow`) must not be tied to the lifetime of
// the borrow of `Editor`. That would prevent commands from mutating the `Editor` until the

View on GitHub (pinned to 079a789e8c)

Solutions

  1. Escape every literal % as %% inside the expanded content.
  2. Check that all %{...} and %u{...} groups are closed with '}'.
  3. Re-run with the inner content simplified to isolate which percent token fails to parse.

Example fix

# before
 :echo %(echo "100% done")

# after
 :echo %(echo "100%% done")
Defensive patterns

Strategy: try-catch

Validate before calling

use helix_core::command_line::Tokenizer;

// dry-run: every '%' inside content must parse or be doubled
fn percent_syntax_ok(content: &str) -> bool {
    let mut t = Tokenizer::new(content, true);
    loop {
        match t.next() {
            None | Some(Ok(_)) if t.pos() >= content.len() => return true,
            Some(Err(_)) => return false,
            _ => continue,
        }
    }
}

Try / catch

let expanded = expand_inner(editor, content).map_err(|err| {
    anyhow!("expansion of '{content}' failed: {err:#}") // inner err is terse; add the payload
})?;

Prevention

When it happens

Trigger: A single unescaped % inside an expand token's content, an unterminated %{...} or %u{...}, or any percent construct the tokenizer cannot parse at that position — all while command-line validation is enabled so the name token cannot be the culprit.

Common situations: Building format strings that contain literal percent signs (date formats, printf-style templates) without doubling them; nested expansions where an inner %u{...} is missing its closing brace.

Related errors


AI-assisted analysis of helix-editor/helix@079a789e8c (2026-08-16). Data as JSON: /api/errors/4c4b7a34ec4208a0. Report an issue: GitHub.