Unnamed repository; edit this file 'description' to name the repository.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
use std::borrow::Cow;

use helix_core::command_line::{ExpansionKind, Token, TokenKind, Tokenizer};

use anyhow::{anyhow, bail, Result};

use crate::Editor;

/// Variables that can be expanded in the command mode (`:`) via the expansion syntax.
///
/// For example `%{cursor_line}`.
//
// To add a new variable follow these steps:
//
// * Add the new enum member to `Variable` below.
// * Add an item to the `VARIANTS` constant - this enables completion.
// * Add a branch in `Variable::as_str`, converting the name from TitleCase to snake_case.
// * Add a branch in `Variable::from_name` with the reverse association.
// * Add a branch in the `expand_variable` function to read the value from the editor.
// * Add the new variable to the documentation in `book/src/command-line.md`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Variable {
    /// The one-indexed line number of the primary cursor in the currently focused document.
    CursorLine,
    /// The one-indexed column number of the primary cursor in the currently focused document.
    ///
    /// Note that this is the count of grapheme clusters from the start of the line (regardless of
    /// softwrap) - the same as the `position` element in the statusline.
    CursorColumn,
    /// The display name of the currently focused document.
    ///
    /// This corresponds to `crate::Document::display_name`.
    BufferName,
    /// A string containing the line-ending of the currently focused document.
    LineEnding,
    // The name of current buffers language as set in `languages.toml`
    Language,
    // Primary selection
    Selection,
}

impl Variable {
    pub const VARIANTS: &'static [Self] = &[
        Self::CursorLine,
        Self::CursorColumn,
        Self::BufferName,
        Self::LineEnding,
        Self::Language,
        Self::Selection,
    ];

    pub const fn as_str(&self) -> &'static str {
        match self {
            Self::CursorLine => "cursor_line",
            Self::CursorColumn => "cursor_column",
            Self::BufferName => "buffer_name",
            Self::LineEnding => "line_ending",
            Self::Language => "language",
            Self::Selection => "selection",
        }
    }

    pub fn from_name(s: &str) -> Option<Self> {
        match s {
            "cursor_line" => Some(Self::CursorLine),
            "cursor_column" => Some(Self::CursorColumn),
            "buffer_name" => Some(Self::BufferName),
            "line_ending" => Some(Self::LineEnding),
            "language" => Some(Self::Language),
            "selection" => Some(Self::Selection),
            _ => None,
        }
    }
}

/// Expands the given command line token.
///
/// Note that the lifetime of the expanded variable is only bound to the input token and not the
/// `Editor`. See `expand_variable` below for more discussion of lifetimes.
pub fn expand<'a>(editor: &Editor, token: Token<'a>) -> Result<Cow<'a, str>> {
    // Note: see the `TokenKind` documentation for more details on how each branch should expand.
    match token.kind {
        TokenKind::Unquoted | TokenKind::Quoted(_) => Ok(token.content),
        TokenKind::Expansion(ExpansionKind::Variable) => {
            let var = Variable::from_name(&token.content)
                .ok_or_else(|| anyhow!("unknown variable '{}'", token.content))?;

            expand_variable(editor, var)
        }
        TokenKind::Expansion(ExpansionKind::Unicode) => {
            if let Some(ch) = u32::from_str_radix(token.content.as_ref(), 16)
                .ok()
                .and_then(char::from_u32)
            {
                Ok(Cow::Owned(ch.to_string()))
            } else {
                Err(anyhow!(
                    "could not interpret '{}' as a Unicode character code",
                    token.content
                ))
            }
        }
        TokenKind::Expand => expand_inner(editor, token.content),
        TokenKind::Expansion(ExpansionKind::Shell) => expand_shell(editor, token.content),
        // Note: see the docs for this variant.
        TokenKind::ExpansionKind => unreachable!(
            "expansion name tokens cannot be emitted when command line validation is enabled"
        ),
    }
}

/// Expand a shell command.
pub fn expand_shell<'a>(editor: &Editor, content: Cow<'a, str>) -> Result<Cow<'a, str>> {
    use std::process::{Command, Stdio};

    // Recursively expand the expansion's content before executing the shell command.
    let content = expand_inner(editor, content)?;

    let config = editor.config();
    let shell = &config.shell;
    let mut process = Command::new(&shell[0]);
    process
        .args(&shell[1..])
        .arg(content.as_ref())
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());

    // TODO: there is no protection here against a shell command taking a long time.
    // Ideally you should be able to hit `<ret>` in command mode and then be able to
    // cancel the invocation (for example with `<C-c>`) if it takes longer than you'd
    // like.
    let output = match process.spawn() {
        Ok(process) => process.wait_with_output()?,
        Err(err) => {
            bail!("Failed to start shell: {err}");
        }
    };

    let mut text = String::from_utf8_lossy(&output.stdout).into_owned();

    if !output.stderr.is_empty() {
        log::warn!(
            "Shell expansion command `{content}` failed: {}",
            String::from_utf8_lossy(&output.stderr)
        );
    }

    // Trim exactly one trailing line ending if it exists.
    if text.ends_with('\n') {
        text.pop();
        if text.ends_with('\r') {
            text.pop();
        }
    }

    Ok(Cow::Owned(text))
}

/// Expand a token's contents recursively.
fn expand_inner<'a>(editor: &Editor, content: Cow<'a, str>) -> Result<Cow<'a, str>> {
    let mut escaped = String::new();
    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
// command consumed or cloned all arguments - this is poor ergonomics. A sensible thing for this
// function to return then, instead, would normally be a `String`. We can return some statically
// known strings like the scratch buffer name or line ending strings though, so this function
// returns a `Cow<'static, str>` instead.
fn expand_variable(editor: &Editor, variable: Variable) -> Result<Cow<'static, str>> {
    let (view, doc) = current_ref!(editor);
    let text = doc.text().slice(..);

    match variable {
        Variable::CursorLine => {
            let cursor_line = doc.selection(view.id).primary().cursor_line(text);
            Ok(Cow::Owned((cursor_line + 1).to_string()))
        }
        Variable::CursorColumn => {
            let cursor = doc.selection(view.id).primary().cursor(text);
            let position = helix_core::coords_at_pos(text, cursor);
            Ok(Cow::Owned((position.col + 1).to_string()))
        }
        Variable::BufferName => {
            // Note: usually we would use `Document::display_name` but we can statically borrow
            // the scratch buffer name by partially reimplementing `display_name`.
            if let Some(path) = doc.relative_path() {
                Ok(Cow::Owned(path.to_string_lossy().into_owned()))
            } else {
                Ok(Cow::Borrowed(crate::document::SCRATCH_BUFFER_NAME))
            }
        }
        Variable::LineEnding => Ok(Cow::Borrowed(doc.line_ending.as_str())),
        Variable::Language => Ok(match doc.language_name() {
            Some(lang) => Cow::Owned(lang.to_owned()),
            None => Cow::Borrowed("text"),
        }),
        Variable::Selection => Ok(Cow::Owned(
            doc.selection(view.id).primary().fragment(text).to_string(),
        )),
    }
}