Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-view/src/expansion.rs')
| -rw-r--r-- | helix-view/src/expansion.rs | 275 |
1 files changed, 0 insertions, 275 deletions
diff --git a/helix-view/src/expansion.rs b/helix-view/src/expansion.rs deleted file mode 100644 index 6a41ae43..00000000 --- a/helix-view/src/expansion.rs +++ /dev/null @@ -1,275 +0,0 @@ -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, - /// Curreng working directory - CurrentWorkingDirectory, - /// Nearest ancestor directory of the current working directory that contains `.git`, `.svn`, `jj` or `.helix` - WorkspaceDirectory, - // The name of current buffers language as set in `languages.toml` - Language, - // Primary selection - Selection, - // The one-indexed line number of the start of the primary selection in the currently focused document. - SelectionLineStart, - // The one-indexed line number of the end of the primary selection in the currently focused document. - SelectionLineEnd, -} - -impl Variable { - pub const VARIANTS: &'static [Self] = &[ - Self::CursorLine, - Self::CursorColumn, - Self::BufferName, - Self::LineEnding, - Self::CurrentWorkingDirectory, - Self::WorkspaceDirectory, - Self::Language, - Self::Selection, - Self::SelectionLineStart, - Self::SelectionLineEnd, - ]; - - 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::CurrentWorkingDirectory => "current_working_directory", - Self::WorkspaceDirectory => "workspace_directory", - Self::Language => "language", - Self::Selection => "selection", - Self::SelectionLineStart => "selection_line_start", - Self::SelectionLineEnd => "selection_line_end", - } - } - - 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), - "workspace_directory" => Some(Self::WorkspaceDirectory), - "current_working_directory" => Some(Self::CurrentWorkingDirectory), - "language" => Some(Self::Language), - "selection" => Some(Self::Selection), - "selection_line_start" => Some(Self::SelectionLineStart), - "selection_line_end" => Some(Self::SelectionLineEnd), - _ => 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::CurrentWorkingDirectory => Ok(std::borrow::Cow::Owned( - helix_stdx::env::current_working_dir() - .to_string_lossy() - .to_string(), - )), - Variable::WorkspaceDirectory => Ok(std::borrow::Cow::Owned( - helix_loader::find_workspace() - .0 - .to_string_lossy() - .to_string(), - )), - 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(), - )), - Variable::SelectionLineStart => { - let start_line = doc.selection(view.id).primary().line_range(text).0; - Ok(Cow::Owned((start_line + 1).to_string())) - } - Variable::SelectionLineEnd => { - let end_line = doc.selection(view.id).primary().line_range(text).1; - Ok(Cow::Owned((end_line + 1).to_string())) - } - } -} |