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, } impl Variable { pub const VARIANTS: &'static [Self] = &[ Self::CursorLine, Self::CursorColumn, Self::BufferName, Self::LineEnding, Self::Language, ]; 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", } } pub fn from_name(s: &str) -> Option { 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), _ => 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> { // 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> { 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 `` in command mode and then be able to // cancel the invocation (for example with ``) 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> { 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> { 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"), }), } }