Skip to content

Commit d9644f8

Browse files
committed
feat: add %quote{} expansion
1 parent eb49c5e commit d9644f8

4 files changed

Lines changed: 58 additions & 3 deletions

File tree

book/src/command-line.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Aside from editor variables, the following expansions may be used:
6060
* Unicode `%u{..}`. The contents may contain up to six hexadecimal numbers corresponding to a Unicode codepoint value. For example `:echo %u{25CF}` prints `` to the statusline.
6161
* Shell `%sh{..}`. The contents are passed to the configured shell command. For example `:echo %sh{echo "20 * 5" | bc}` may print `100` on the statusline on when using a shell with `echo` and the `bc` calculator installed. Shell expansions are evaluated recursively. `%sh{echo '%{buffer_name}:%{cursor_line}'}` for example executes a command like `echo 'README.md:1'`: the variables within the `%sh{..}` expansion are evaluated before executing the shell command.
6262
* Register `%reg{..}`. The contents should be a single character representing the register name. For example, `:set-register a hello world` followed by `echo %reg{a}` prints `hello world` to the statusline.
63+
* Quote `%quote{..}`. The contents are surrounded with double quotes(`"`), and escapes internal backslashes(`\`) and double quotes(`"`), so that the result is a valid string literal. For example, `:echo %quote{"'"="foo"}` will print `"\"'\"=\"foo\""` to the statusline.
6364

6465
As mentioned above, double quotes can be used to surround arguments containing spaces but also support expansions within the quoted content unlike single quotes or backticks. For example `:echo "circle: %u{25CF}"` prints `circle: ●` to the statusline while `:echo 'circle: %u{25CF}'` prints `circle: %u{25CF}`.
6566

helix-core/src/command_line.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,18 +257,28 @@ pub enum ExpansionKind {
257257
///
258258
/// For example `%reg{a}`.
259259
Register,
260+
/// Quotes token's contents with double quotes, and escapes according to standard escaping conventions.
261+
///
262+
/// For example, `%quote{ "'"="foo" }` -> `"\"'\"=foo\""`.
263+
Quote,
260264
}
261265

262266
impl ExpansionKind {
263-
pub const VARIANTS: &'static [Self] =
264-
&[Self::Variable, Self::Unicode, Self::Shell, Self::Register];
267+
pub const VARIANTS: &'static [Self] = &[
268+
Self::Variable,
269+
Self::Unicode,
270+
Self::Shell,
271+
Self::Register,
272+
Self::Quote,
273+
];
265274

266275
pub const fn as_str(&self) -> &'static str {
267276
match self {
268277
Self::Variable => "",
269278
Self::Unicode => "u",
270279
Self::Shell => "sh",
271280
Self::Register => "reg",
281+
Self::Quote => "quote",
272282
}
273283
}
274284

@@ -278,6 +288,7 @@ impl ExpansionKind {
278288
"u" => Some(Self::Unicode),
279289
"sh" => Some(Self::Shell),
280290
"reg" => Some(Self::Register),
291+
"quote" => Some(Self::Quote),
281292
_ => None,
282293
}
283294
}

helix-term/src/commands/typed.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4260,10 +4260,10 @@ pub fn complete_command_args(
42604260
TokenKind::Expansion(ExpansionKind::Variable) => {
42614261
complete_variable_expansion(&token.content, offset + token.content_start)
42624262
}
4263-
TokenKind::Expansion(ExpansionKind::Unicode) => Vec::new(),
42644263
TokenKind::Expansion(ExpansionKind::Register) => {
42654264
complete_register_expansion(editor, &token.content, offset + token.content_start)
42664265
}
4266+
TokenKind::Expansion(ExpansionKind::Unicode | ExpansionKind::Quote) => Vec::new(),
42674267
TokenKind::ExpansionKind => {
42684268
complete_expansion_kind(&token.content, offset + token.content_start)
42694269
}

helix-view/src/expansion.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ pub fn expand<'a>(editor: &Editor, token: Token<'a>) -> Result<Cow<'a, str>> {
126126
))
127127
}
128128
}
129+
TokenKind::Expansion(ExpansionKind::Quote) => Ok(Cow::Owned({
130+
let expanded = expand_inner(editor, token.content)?;
131+
let escaped = escape(&expanded);
132+
format!("\"{escaped}\"")
133+
})),
129134
TokenKind::Expand => expand_inner(editor, token.content),
130135
TokenKind::Expansion(ExpansionKind::Shell) => expand_shell(editor, token.content),
131136
TokenKind::Expansion(ExpansionKind::Register) => expand_register(editor, token.content),
@@ -136,6 +141,24 @@ pub fn expand<'a>(editor: &Editor, token: Token<'a>) -> Result<Cow<'a, str>> {
136141
}
137142
}
138143

144+
fn escape(string: &str) -> Cow<'_, str> {
145+
if !string.contains('"') && !string.contains('\\') {
146+
return Cow::Borrowed(string);
147+
}
148+
149+
let mut escaped = String::new();
150+
151+
for ch in string.chars() {
152+
match ch {
153+
'"' => escaped.push_str(r#"\""#),
154+
'\\' => escaped.push_str(r"\\"),
155+
_ => escaped.push(ch),
156+
}
157+
}
158+
159+
Cow::Owned(escaped)
160+
}
161+
139162
/// Expand a shell command.
140163
pub fn expand_shell<'a>(editor: &Editor, content: Cow<'a, str>) -> Result<Cow<'a, str>> {
141164
use std::process::{Command, Stdio};
@@ -305,3 +328,23 @@ fn expand_variable(editor: &Editor, variable: Variable) -> Result<Cow<'static, s
305328
}
306329
}
307330
}
331+
332+
#[cfg(test)]
333+
mod test {
334+
use super::*;
335+
336+
#[test]
337+
fn should_escape_only_double_quotes() {
338+
assert_eq!(r#"\"'\"=\"foo\""#, escape(r#""'"="foo""#));
339+
}
340+
341+
#[test]
342+
fn should_escape_double_quotes_and_backslashes() {
343+
assert_eq!(r#"\"'\"=\"f\\oo\""#, escape(r#""'"="f\oo""#));
344+
}
345+
346+
#[test]
347+
fn should_escape_double_quotes_and_backslashes_and_leave_spaces_alone() {
348+
assert_eq!(r#"\"'\"=\"f\\o o\""#, escape(r#""'"="f\o o""#));
349+
}
350+
}

0 commit comments

Comments
 (0)